diff --git a/tests/integration/routes/parity/__snapshots__/import-json.01-first-file.json.snap b/tests/integration/routes/parity/__snapshots__/import-json.01-first-file.json.snap new file mode 100644 index 000000000..6729dbe05 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-json.01-first-file.json.snap @@ -0,0 +1,205 @@ +{ + "status": 200, + "body": { + "data": { + "measurements": 4, + "moodEntries": 4, + "skipped": 2 + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "import.upload" + }, + "meta": { + "import_stats": { + "measurements": 4, + "moodEntries": 4, + "skipped": 2 + }, + "external_id_rejected": 2, + "external_id_shapes": "object_description", + "external_id_surface": "import.json" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "emitDataArrival", + "args": [ + { + "userId": "", + "kind": "weight", + "newestSampleAt": "2026-10-11T06:30:00.000Z", + "insertedCount": 2, + "refId": "", + "source": "json_import" + } + ] + }, + { + "effect": "maybeEnqueueMorningRefresh", + "args": [ + "", + [ + "2026-10-12T06:00:00.000Z" + ] + ] + } + ], + "stored": { + "measurements": [ + { + "type": "WEIGHT", + "value": 81.4, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-1", + "measuredAt": "2026-10-10T06:30:00.000Z", + "notes": null, + "notesEncrypted": "", + "glucoseContext": null + }, + { + "type": "WEIGHT", + "value": 81.9, + "unit": "kg", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-11T06:30:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + }, + { + "type": "BLOOD_GLUCOSE", + "value": 104, + "unit": "mg/dL", + "source": "IMPORT", + "externalId": "exp-g-1", + "measuredAt": "2026-10-11T08:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": "FASTING" + }, + { + "type": "SLEEP_DURATION", + "value": 412, + "unit": "min", + "source": "IMPORT", + "externalId": "exp-s-1", + "measuredAt": "2026-10-12T06:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + } + ], + "mood": [ + { + "date": "2026-10-10", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "tags": "slept well", + "source": "IMPORT", + "externalId": "daylio-1", + "moodLoggedAt": "2026-10-10T07:00:00.000Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:08.803Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:32.701Z" + }, + { + "date": "2026-10-12", + "mood": "SCHLECHT", + "score": 2, + "moodA1": 3, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-12T20:00:00.000Z" + } + ], + "measurementRollups": [ + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.4 + }, + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.9 + }, + { + "type": "SLEEP_DURATION", + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 412 + }, + { + "type": "BLOOD_GLUCOSE", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 104 + } + ], + "moodRollups": [ + { + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "count": 1, + "mean": 4 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 2, + "mean": 3 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 2 + } + ], + "audit": [ + { + "action": "import.upload", + "details": "{\"measurements\":4,\"moodEntries\":4,\"skipped\":2}" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/import-json.02-same-file.json.snap b/tests/integration/routes/parity/__snapshots__/import-json.02-same-file.json.snap new file mode 100644 index 000000000..b7e74b5fe --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-json.02-same-file.json.snap @@ -0,0 +1,194 @@ +{ + "status": 200, + "body": { + "data": { + "measurements": 3, + "moodEntries": 1, + "skipped": 6 + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "import.upload" + }, + "meta": { + "import_stats": { + "measurements": 3, + "moodEntries": 1, + "skipped": 6 + }, + "external_id_rejected": 2, + "external_id_shapes": "object_description", + "external_id_surface": "import.json" + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "maybeEnqueueMorningRefresh", + "args": [ + "", + [] + ] + } + ], + "stored": { + "measurements": [ + { + "type": "WEIGHT", + "value": 81.4, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-1", + "measuredAt": "2026-10-10T06:30:00.000Z", + "notes": null, + "notesEncrypted": "", + "glucoseContext": null + }, + { + "type": "WEIGHT", + "value": 81.9, + "unit": "kg", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-11T06:30:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + }, + { + "type": "BLOOD_GLUCOSE", + "value": 104, + "unit": "mg/dL", + "source": "IMPORT", + "externalId": "exp-g-1", + "measuredAt": "2026-10-11T08:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": "FASTING" + }, + { + "type": "SLEEP_DURATION", + "value": 412, + "unit": "min", + "source": "IMPORT", + "externalId": "exp-s-1", + "measuredAt": "2026-10-12T06:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + } + ], + "mood": [ + { + "date": "2026-10-10", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "tags": "slept well", + "source": "IMPORT", + "externalId": "daylio-1", + "moodLoggedAt": "2026-10-10T07:00:00.000Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:08.803Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:32.701Z" + }, + { + "date": "2026-10-12", + "mood": "SCHLECHT", + "score": 2, + "moodA1": 3, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-12T20:00:00.000Z" + } + ], + "measurementRollups": [ + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.4 + }, + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.9 + }, + { + "type": "SLEEP_DURATION", + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 412 + }, + { + "type": "BLOOD_GLUCOSE", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 104 + } + ], + "moodRollups": [ + { + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "count": 1, + "mean": 4 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 2, + "mean": 3 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 2 + } + ], + "audit": [ + { + "action": "import.upload", + "details": "{\"measurements\":4,\"moodEntries\":4,\"skipped\":2}" + }, + { + "action": "import.upload", + "details": "{\"measurements\":3,\"moodEntries\":1,\"skipped\":6}" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/import-json.03-out-of-range.json.snap b/tests/integration/routes/parity/__snapshots__/import-json.03-out-of-range.json.snap new file mode 100644 index 000000000..7e544cc07 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-json.03-out-of-range.json.snap @@ -0,0 +1,186 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Wert ausserhalb des plausiblen Bereichs", + "details": { + "issues": [ + { + "path": "measurements.0", + "code": "custom", + "message": "Wert ausserhalb des plausiblen Bereichs" + } + ] + }, + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "import.upload" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "measurements": [ + { + "type": "WEIGHT", + "value": 81.4, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-1", + "measuredAt": "2026-10-10T06:30:00.000Z", + "notes": null, + "notesEncrypted": "", + "glucoseContext": null + }, + { + "type": "WEIGHT", + "value": 81.9, + "unit": "kg", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-11T06:30:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + }, + { + "type": "BLOOD_GLUCOSE", + "value": 104, + "unit": "mg/dL", + "source": "IMPORT", + "externalId": "exp-g-1", + "measuredAt": "2026-10-11T08:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": "FASTING" + }, + { + "type": "SLEEP_DURATION", + "value": 412, + "unit": "min", + "source": "IMPORT", + "externalId": "exp-s-1", + "measuredAt": "2026-10-12T06:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + } + ], + "mood": [ + { + "date": "2026-10-10", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "tags": "slept well", + "source": "IMPORT", + "externalId": "daylio-1", + "moodLoggedAt": "2026-10-10T07:00:00.000Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:08.803Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:32.701Z" + }, + { + "date": "2026-10-12", + "mood": "SCHLECHT", + "score": 2, + "moodA1": 3, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-12T20:00:00.000Z" + } + ], + "measurementRollups": [ + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.4 + }, + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.9 + }, + { + "type": "SLEEP_DURATION", + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 412 + }, + { + "type": "BLOOD_GLUCOSE", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 104 + } + ], + "moodRollups": [ + { + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "count": 1, + "mean": 4 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 2, + "mean": 3 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 2 + } + ], + "audit": [ + { + "action": "import.upload", + "details": "{\"measurements\":4,\"moodEntries\":4,\"skipped\":2}" + }, + { + "action": "import.upload", + "details": "{\"measurements\":3,\"moodEntries\":1,\"skipped\":6}" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/import-json.04-forward-dated.json.snap b/tests/integration/routes/parity/__snapshots__/import-json.04-forward-dated.json.snap new file mode 100644 index 000000000..bfa999b93 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/import-json.04-forward-dated.json.snap @@ -0,0 +1,186 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Timestamp must not be in the future", + "details": { + "issues": [ + { + "path": "moodEntries.0.loggedAt", + "code": "custom", + "message": "Timestamp must not be in the future" + } + ] + }, + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "import.upload" + }, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "measurements": [ + { + "type": "WEIGHT", + "value": 81.4, + "unit": "kg", + "source": "IMPORT", + "externalId": "exp-w-1", + "measuredAt": "2026-10-10T06:30:00.000Z", + "notes": null, + "notesEncrypted": "", + "glucoseContext": null + }, + { + "type": "WEIGHT", + "value": 81.9, + "unit": "kg", + "source": "IMPORT", + "externalId": null, + "measuredAt": "2026-10-11T06:30:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + }, + { + "type": "BLOOD_GLUCOSE", + "value": 104, + "unit": "mg/dL", + "source": "IMPORT", + "externalId": "exp-g-1", + "measuredAt": "2026-10-11T08:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": "FASTING" + }, + { + "type": "SLEEP_DURATION", + "value": 412, + "unit": "min", + "source": "IMPORT", + "externalId": "exp-s-1", + "measuredAt": "2026-10-12T06:00:00.000Z", + "notes": null, + "notesEncrypted": null, + "glucoseContext": null + } + ], + "mood": [ + { + "date": "2026-10-10", + "mood": "GUT", + "score": 4, + "moodA1": 7, + "tags": "slept well", + "source": "IMPORT", + "externalId": "daylio-1", + "moodLoggedAt": "2026-10-10T07:00:00.000Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:08.803Z" + }, + { + "date": "2026-10-11", + "mood": "OKAY", + "score": 3, + "moodA1": 5, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-10T22:02:32.701Z" + }, + { + "date": "2026-10-12", + "mood": "SCHLECHT", + "score": 2, + "moodA1": 3, + "tags": null, + "source": "IMPORT", + "externalId": null, + "moodLoggedAt": "2026-10-12T20:00:00.000Z" + } + ], + "measurementRollups": [ + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.4 + }, + { + "type": "WEIGHT", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 81.9 + }, + { + "type": "SLEEP_DURATION", + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 412 + }, + { + "type": "BLOOD_GLUCOSE", + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "source": "IMPORT", + "count": 1, + "mean": 104 + } + ], + "moodRollups": [ + { + "granularity": "DAY", + "bucketStart": "2026-10-10T00:00:00.000Z", + "count": 1, + "mean": 4 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-11T00:00:00.000Z", + "count": 2, + "mean": 3 + }, + { + "granularity": "DAY", + "bucketStart": "2026-10-12T00:00:00.000Z", + "count": 1, + "mean": 2 + } + ], + "audit": [ + { + "action": "import.upload", + "details": "{\"measurements\":4,\"moodEntries\":4,\"skipped\":2}" + }, + { + "action": "import.upload", + "details": "{\"measurements\":3,\"moodEntries\":1,\"skipped\":6}" + } + ] + } +} diff --git a/tests/integration/routes/parity/import-json.parity.test.ts b/tests/integration/routes/parity/import-json.parity.test.ts new file mode 100644 index 000000000..103565382 --- /dev/null +++ b/tests/integration/routes/parity/import-json.parity.test.ts @@ -0,0 +1,282 @@ +/** + * Parity for `POST /api/import` while its writers move into + * `src/lib/import/json/`. + * + * A first file of mixed rows (measurements with and without an external id, + * with a note and a glucose context, an unstable id; mood entries with and + * without a logged instant, a twin pair that has to land on two distinct + * fallback instants, an external id, an unstable id), the same file again, + * and the whole-file refusals. After each request the snapshot holds the + * response, the wide event, the recorded arrival and refresh tails, and the + * stored measurements, mood entries, rollups and audit rows. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { POST } from "@/app/api/import/route"; + +import { callRoute, type RouteAuth } from "../../../kit/call-route"; +import { makeSession, makeUser } from "../../../kit/factories"; +import { + effectRecorder, + eventFacts, + expectParity, + stable, + wideEventRecorder, +} from "../../../kit/parity"; +import { getPrismaClient, truncateAllTables } from "../../setup"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); +vi.mock("@/lib/arrivals/emit-shared", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + emitDataArrival: async (...args: unknown[]) => { + effectRecorder.record("emitDataArrival", args); + }, + }; +}); +vi.mock("@/lib/daily/morning-refresh-trigger", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/daily/morning-refresh-trigger") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + maybeEnqueueMorningRefresh: async (...args: unknown[]) => { + effectRecorder.record("maybeEnqueueMorningRefresh", args); + }, + }; +}); +vi.mock("@/lib/jobs/pr-detection", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + enqueuePrDetection: async (...args: unknown[]) => { + effectRecorder.record("enqueuePrDetection", args); + }, + }; +}); + +const prisma = getPrismaClient(); +const NOW = new Date("2026-10-15T12:00:00.000Z"); + +let userId: string; +let owner: RouteAuth; + +async function stored() { + const [measurements, mood, measurementRollups, moodRollups, audit] = + await Promise.all([ + prisma.measurement.findMany({ + where: { userId }, + orderBy: [{ measuredAt: "asc" }, { type: "asc" }, { value: "asc" }], + select: { + type: true, + value: true, + unit: true, + source: true, + externalId: true, + measuredAt: true, + notes: true, + notesEncrypted: true, + glucoseContext: true, + }, + }), + prisma.moodEntry.findMany({ + where: { userId }, + orderBy: [{ moodLoggedAt: "asc" }, { mood: "asc" }], + select: { + date: true, + mood: true, + score: true, + moodA1: true, + tags: true, + source: true, + externalId: true, + moodLoggedAt: true, + }, + }), + prisma.measurementRollup.findMany({ + where: { userId }, + orderBy: [ + { type: "asc" }, + { granularity: "asc" }, + { bucketStart: "asc" }, + ], + select: { + type: true, + granularity: true, + bucketStart: true, + source: true, + count: true, + mean: true, + }, + }), + prisma.moodEntryRollup.findMany({ + where: { userId }, + orderBy: [{ granularity: "asc" }, { bucketStart: "asc" }], + select: { + granularity: true, + bucketStart: true, + count: true, + mean: true, + }, + }), + prisma.auditLog.findMany({ + where: { userId }, + orderBy: { createdAt: "asc" }, + select: { action: true, details: true }, + }), + ]); + return { measurements, mood, measurementRollups, moodRollups, audit }; +} + +async function step(name: string, body: unknown): Promise { + wideEventRecorder.take(); + effectRecorder.take(); + const res = await callRoute(POST, { + method: "POST", + route: "/api/import", + body, + auth: owner, + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + await expectParity( + `import-json.${name}`, + stable( + { + status: res.status, + body: res.body, + events: events.map(eventFacts), + effects: effectRecorder.take(), + stored: await stored(), + }, + { since: NOW }, + ), + ); +} + +const FILE = { + measurements: [ + { + type: "WEIGHT", + value: 81.4, + unit: "kg", + measuredAt: "2026-10-10T06:30:00.000Z", + notes: "after the run", + externalId: "exp-w-1", + }, + { + type: "WEIGHT", + value: 81.9, + unit: "kg", + measuredAt: "2026-10-11T06:30:00.000Z", + }, + { + type: "BLOOD_GLUCOSE", + value: 104, + unit: "mg/dL", + measuredAt: "2026-10-11T08:00:00.000Z", + glucoseContext: "FASTING", + externalId: "exp-g-1", + }, + { + type: "SLEEP_DURATION", + value: 412, + unit: "min", + measuredAt: "2026-10-12T06:00:00.000Z", + externalId: "exp-s-1", + }, + { + type: "PULSE", + value: 64, + unit: "bpm", + measuredAt: "2026-10-12T07:00:00.000Z", + externalId: "", + }, + ], + moodEntries: [ + { + date: "2026-10-10", + mood: "GUT", + score: 4, + tags: "slept well", + loggedAt: "2026-10-10T07:00:00.000Z", + externalId: "daylio-1", + }, + { date: "2026-10-11", mood: "OKAY", score: 3 }, + { date: "2026-10-11", mood: "OKAY", score: 3 }, + { + date: "2026-10-12", + mood: "SCHLECHT", + score: 2, + loggedAt: "2026-10-12T20:00:00.000Z", + }, + { + date: "2026-10-13", + mood: "GUT", + score: 5, + externalId: "", + }, + ], +}; + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const user = await makeUser({ timezone: "Europe/Berlin" }); + userId = user.id; + owner = await makeSession(user); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("POST /api/import parity", () => { + it("01 a first file", async () => { + await step("01-first-file", FILE); + }); + + it("02 the same file again", async () => { + await step("02-same-file", FILE); + }); + + it("03 the whole-file refusals", async () => { + await step("03-out-of-range", { + measurements: [ + { + type: "WEIGHT", + value: 4_000, + unit: "kg", + measuredAt: "2026-10-10T06:30:00.000Z", + }, + ], + }); + await step("04-forward-dated", { + moodEntries: [ + { + date: "2026-10-20", + mood: "GUT", + score: 4, + loggedAt: "2026-10-20T07:00:00.000Z", + }, + ], + }); + }); +});