Log raw response when Garmin returns non-JSON
All checks were successful
Deploy / deploy (push) Successful in 1m40s
All checks were successful
Deploy / deploy (push) Successful in 1m40s
Garmin is returning HTML error pages instead of JSON data. This
change reads the response as text first, checks if it starts with
{ or [, and logs the first 1000 chars of the response body if not.
This will help diagnose what page Garmin is returning (login, captcha,
rate limit, etc).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,17 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { GarminTokens } from "@/types";
|
||||
|
||||
// Helper to create mock fetch responses with both text() and json() methods
|
||||
function mockJsonResponse(data: unknown, ok = true, status = 200) {
|
||||
const jsonStr = JSON.stringify(data);
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
text: () => Promise.resolve(jsonStr),
|
||||
json: () => Promise.resolve(data),
|
||||
};
|
||||
}
|
||||
|
||||
import {
|
||||
daysUntilExpiry,
|
||||
fetchBodyBattery,
|
||||
@@ -245,13 +256,11 @@ describe("fetchHrvStatus", () => {
|
||||
});
|
||||
|
||||
it("returns Balanced when API returns BALANCED status", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
hrvSummary: { lastNightAvg: 45, weeklyAvg: 42, status: "BALANCED" },
|
||||
}),
|
||||
});
|
||||
global.fetch = vi.fn().mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
hrvSummary: { lastNightAvg: 45, weeklyAvg: 42, status: "BALANCED" },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await fetchHrvStatus("2024-01-15", "test-token");
|
||||
|
||||
@@ -267,13 +276,11 @@ describe("fetchHrvStatus", () => {
|
||||
});
|
||||
|
||||
it("returns Unbalanced when API returns UNBALANCED status", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
hrvSummary: { lastNightAvg: 25, weeklyAvg: 42, status: "UNBALANCED" },
|
||||
}),
|
||||
});
|
||||
global.fetch = vi.fn().mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
hrvSummary: { lastNightAvg: 25, weeklyAvg: 42, status: "UNBALANCED" },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await fetchHrvStatus("2024-01-15", "test-token");
|
||||
|
||||
@@ -281,10 +288,7 @@ describe("fetchHrvStatus", () => {
|
||||
});
|
||||
|
||||
it("returns Unknown when API returns no data", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
});
|
||||
global.fetch = vi.fn().mockResolvedValue(mockJsonResponse({}));
|
||||
|
||||
const result = await fetchHrvStatus("2024-01-15", "test-token");
|
||||
|
||||
@@ -292,10 +296,9 @@ describe("fetchHrvStatus", () => {
|
||||
});
|
||||
|
||||
it("returns Unknown when API returns null hrvSummary", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ hrvSummary: null }),
|
||||
});
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValue(mockJsonResponse({ hrvSummary: null }));
|
||||
|
||||
const result = await fetchHrvStatus("2024-01-15", "test-token");
|
||||
|
||||
@@ -334,16 +337,14 @@ describe("fetchBodyBattery", () => {
|
||||
});
|
||||
|
||||
it("returns current and yesterday low values on success", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
bodyBatteryValuesArray: [
|
||||
{ date: "2024-01-15", charged: 85, drained: 60 },
|
||||
],
|
||||
bodyBatteryStatList: [{ date: "2024-01-14", min: 25, max: 95 }],
|
||||
}),
|
||||
});
|
||||
global.fetch = vi.fn().mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
bodyBatteryValuesArray: [
|
||||
{ date: "2024-01-15", charged: 85, drained: 60 },
|
||||
],
|
||||
bodyBatteryStatList: [{ date: "2024-01-14", min: 25, max: 95 }],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await fetchBodyBattery("2024-01-15", "test-token");
|
||||
|
||||
@@ -362,14 +363,12 @@ describe("fetchBodyBattery", () => {
|
||||
});
|
||||
|
||||
it("returns null values when data is missing", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
bodyBatteryValuesArray: [],
|
||||
bodyBatteryStatList: [],
|
||||
}),
|
||||
});
|
||||
global.fetch = vi.fn().mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
bodyBatteryValuesArray: [],
|
||||
bodyBatteryStatList: [],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await fetchBodyBattery("2024-01-15", "test-token");
|
||||
|
||||
@@ -380,10 +379,7 @@ describe("fetchBodyBattery", () => {
|
||||
});
|
||||
|
||||
it("returns null values when API returns empty object", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
});
|
||||
global.fetch = vi.fn().mockResolvedValue(mockJsonResponse({}));
|
||||
|
||||
const result = await fetchBodyBattery("2024-01-15", "test-token");
|
||||
|
||||
@@ -419,14 +415,12 @@ describe("fetchBodyBattery", () => {
|
||||
});
|
||||
|
||||
it("handles partial data - only current available", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
bodyBatteryValuesArray: [{ date: "2024-01-15", charged: 70 }],
|
||||
bodyBatteryStatList: [],
|
||||
}),
|
||||
});
|
||||
global.fetch = vi.fn().mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
bodyBatteryValuesArray: [{ date: "2024-01-15", charged: 70 }],
|
||||
bodyBatteryStatList: [],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await fetchBodyBattery("2024-01-15", "test-token");
|
||||
|
||||
@@ -449,16 +443,14 @@ describe("fetchIntensityMinutes", () => {
|
||||
});
|
||||
|
||||
it("returns 7-day intensity minutes total on success", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
weeklyTotal: {
|
||||
moderateIntensityMinutes: 45,
|
||||
vigorousIntensityMinutes: 30,
|
||||
},
|
||||
}),
|
||||
});
|
||||
global.fetch = vi.fn().mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
weeklyTotal: {
|
||||
moderateIntensityMinutes: 45,
|
||||
vigorousIntensityMinutes: 30,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await fetchIntensityMinutes("test-token");
|
||||
|
||||
@@ -474,10 +466,7 @@ describe("fetchIntensityMinutes", () => {
|
||||
});
|
||||
|
||||
it("returns 0 when no intensity data available", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
});
|
||||
global.fetch = vi.fn().mockResolvedValue(mockJsonResponse({}));
|
||||
|
||||
const result = await fetchIntensityMinutes("test-token");
|
||||
|
||||
@@ -485,10 +474,9 @@ describe("fetchIntensityMinutes", () => {
|
||||
});
|
||||
|
||||
it("returns 0 when weeklyTotal is null", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ weeklyTotal: null }),
|
||||
});
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValue(mockJsonResponse({ weeklyTotal: null }));
|
||||
|
||||
const result = await fetchIntensityMinutes("test-token");
|
||||
|
||||
@@ -496,16 +484,14 @@ describe("fetchIntensityMinutes", () => {
|
||||
});
|
||||
|
||||
it("handles only moderate intensity minutes", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
weeklyTotal: {
|
||||
moderateIntensityMinutes: 60,
|
||||
vigorousIntensityMinutes: 0,
|
||||
},
|
||||
}),
|
||||
});
|
||||
global.fetch = vi.fn().mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
weeklyTotal: {
|
||||
moderateIntensityMinutes: 60,
|
||||
vigorousIntensityMinutes: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await fetchIntensityMinutes("test-token");
|
||||
|
||||
@@ -513,16 +499,14 @@ describe("fetchIntensityMinutes", () => {
|
||||
});
|
||||
|
||||
it("handles only vigorous intensity minutes", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
weeklyTotal: {
|
||||
moderateIntensityMinutes: 0,
|
||||
vigorousIntensityMinutes: 45,
|
||||
},
|
||||
}),
|
||||
});
|
||||
global.fetch = vi.fn().mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
weeklyTotal: {
|
||||
moderateIntensityMinutes: 0,
|
||||
vigorousIntensityMinutes: 45,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await fetchIntensityMinutes("test-token");
|
||||
|
||||
|
||||
@@ -72,7 +72,15 @@ export async function fetchHrvStatus(
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const text = await response.text();
|
||||
if (!text.startsWith("{") && !text.startsWith("[")) {
|
||||
logger.error(
|
||||
{ endpoint: "hrv-service", responseBody: text.slice(0, 1000) },
|
||||
"Garmin returned non-JSON response",
|
||||
);
|
||||
return "Unknown";
|
||||
}
|
||||
const data = JSON.parse(text);
|
||||
const status = data?.hrvSummary?.status;
|
||||
|
||||
if (status === "BALANCED") {
|
||||
@@ -120,7 +128,15 @@ export async function fetchBodyBattery(
|
||||
return { current: null, yesterdayLow: null };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const text = await response.text();
|
||||
if (!text.startsWith("{") && !text.startsWith("[")) {
|
||||
logger.error(
|
||||
{ endpoint: "bodyBattery", responseBody: text.slice(0, 1000) },
|
||||
"Garmin returned non-JSON response",
|
||||
);
|
||||
return { current: null, yesterdayLow: null };
|
||||
}
|
||||
const data = JSON.parse(text);
|
||||
|
||||
const currentData = data?.bodyBatteryValuesArray?.[0];
|
||||
const current = currentData?.charged ?? null;
|
||||
@@ -170,7 +186,15 @@ export async function fetchIntensityMinutes(
|
||||
return 0;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const text = await response.text();
|
||||
if (!text.startsWith("{") && !text.startsWith("[")) {
|
||||
logger.error(
|
||||
{ endpoint: "fitnessstats", responseBody: text.slice(0, 1000) },
|
||||
"Garmin returned non-JSON response",
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
const data = JSON.parse(text);
|
||||
const weeklyTotal = data?.weeklyTotal;
|
||||
|
||||
if (!weeklyTotal) {
|
||||
|
||||
Reference in New Issue
Block a user