Prove the ECG ingest, the dedupe and the migration on dirty data

A round trip through the real handlers: post a recording, read it back
through the list and the detail routes, and check what a client actually
receives rather than a fixture rebuilt at each end. Post it twice and
the second call says updated, not inserted, and the table does not grow.
Feed the same strip to the export-archive importer afterwards and it
says skipped; feed it to the importer first and the route says
duplicate. Both directions leave one row.

The migration test drops the index, manufactures the collisions the
resolution step exists for, and runs the migration file through the same
executor the deploy uses. It checks which row survived each tiebreak,
that the salvaged event link followed the keeper, that rows differing
only by user, source or sampling rate were left alone, and that the
constraint rejects a new twin afterwards. Applying the migration to the
empty testcontainer proves none of that.

The Withings ECG tests now hand the transaction the same delegate the
shared writer uses, so they still watch the real write instead of a stub
standing in for it.
This commit is contained in:
Marc-André Bombeck committed 2026-08-01 19:26:47 +02:00
1 parent 0e301b580c
commit 8f90fd3174
4 files changed
+882 -13

No files matched your search

@@ -0,0 +1,163 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/lib/withings/ecg-waveform-codec", () => ({
encryptWaveformToBytes: vi.fn(),
}));
import { encryptWaveformToBytes } from "@/lib/withings/ecg-waveform-codec";
import {
ecgDurationSeconds,
persistEcgRecording,
} from "@/lib/ecg/persist-recording";
function fakeDb() {
const findUnique = vi.fn();
const upsert = vi.fn();
const $transaction = vi.fn(
async (fn: (tx: unknown) => unknown) =>
await fn({ ecgRecording: { findUnique, upsert } }),
);
return { db: { $transaction }, findUnique, upsert, $transaction };
}
function input(overrides: Record<string, unknown> = {}) {
return {
userId: "user-1",
source: "APPLE_HEALTH" as const,
externalRecordingId: "hk-uuid-1",
recordedAt: new Date("2026-07-18T06:14:03.000Z"),
samples: [12, -7, 3],
samplingFrequency: 512,
...overrides,
};
}
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(encryptWaveformToBytes).mockImplementation(
() => new Uint8Array(new ArrayBuffer(4)),
);
});
describe("ecgDurationSeconds", () => {
it("divides the sample count by the sampling rate", () => {
expect(ecgDurationSeconds(15_360, 512)).toBe(30);
});
it("is null when the source reported no sampling rate", () => {
expect(ecgDurationSeconds(15_360, 0)).toBeNull();
});
});
describe("persistEcgRecording", () => {
it("fails closed on a crypto error without ever opening a write transaction", async () => {
vi.mocked(encryptWaveformToBytes).mockImplementation(() => {
throw new Error("no encryption key");
});
const { db, $transaction } = fakeDb();
await expect(persistEcgRecording(input(), db as never)).rejects.toThrow(
"no encryption key",
);
expect($transaction).not.toHaveBeenCalled();
});
it("inserts when neither identity matches an existing row", async () => {
const { db, findUnique, upsert } = fakeDb();
findUnique.mockResolvedValue(null);
upsert.mockResolvedValue({ id: "row-new" });
const result = await persistEcgRecording(input(), db as never);
expect(result).toMatchObject({
outcome: "inserted",
id: "row-new",
sampleCount: 3,
});
expect(upsert).toHaveBeenCalledTimes(1);
});
it("updates in place when the source id is already stored", async () => {
const { db, findUnique, upsert } = fakeDb();
findUnique.mockResolvedValueOnce({ id: "row-existing" });
upsert.mockResolvedValue({ id: "row-existing" });
const result = await persistEcgRecording(input(), db as never);
expect(result.outcome).toBe("updated");
expect(result.id).toBe("row-existing");
// The source-id lookup answered; the recording-identity lookup is not
// needed and must not run.
expect(findUnique).toHaveBeenCalledTimes(1);
});
it("writes nothing when the recording is already stored under another id", async () => {
const { db, findUnique, upsert } = fakeDb();
findUnique
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({ id: "row-from-the-archive" });
const result = await persistEcgRecording(input(), db as never);
expect(result.outcome).toBe("duplicate");
expect(result.id).toBe("row-from-the-archive");
expect(upsert).not.toHaveBeenCalled();
});
it("looks the twin up by the recording's own identity, not by the source id", async () => {
const { db, findUnique, upsert } = fakeDb();
findUnique.mockResolvedValue(null);
upsert.mockResolvedValue({ id: "row-new" });
await persistEcgRecording(input(), db as never);
expect(findUnique.mock.calls[1][0].where).toEqual({
userId_source_recordedAt_samplingFrequency: {
userId: "user-1",
source: "APPLE_HEALTH",
recordedAt: new Date("2026-07-18T06:14:03.000Z"),
samplingFrequency: 512,
},
});
});
it("builds the row field by field and narrows the owner to the caller's user", async () => {
const { db, findUnique, upsert } = fakeDb();
findUnique.mockResolvedValue(null);
upsert.mockResolvedValue({ id: "row-new" });
await persistEcgRecording(
input({
lead: "I",
averageHeartRate: 64,
rhythmClassification: "IRREGULAR",
}),
db as never,
);
const created = upsert.mock.calls[0][0].create;
expect(created).toMatchObject({
userId: "user-1",
source: "APPLE_HEALTH",
externalRecordingId: "hk-uuid-1",
samplingFrequency: 512,
sampleCount: 3,
lead: "I",
averageHeartRate: 64,
rhythmClassification: "IRREGULAR",
measurementId: null,
});
expect(created.durationSeconds).toBeCloseTo(3 / 512, 10);
});
it("leaves no plaintext samples behind in the caller's array", async () => {
const { db, findUnique, upsert } = fakeDb();
findUnique.mockResolvedValue(null);
upsert.mockResolvedValue({ id: "row-new" });
const payload = input();
await persistEcgRecording(payload, db as never);
expect(payload.samples).toEqual([0, 0, 0]);
});
});
+32 -13
View File
@@ -8,19 +8,31 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/lib/db", () => ({
prisma: {
measurement: {
upsert: vi.fn(),
// The waveform write goes through the shared writer
// (`@/lib/ecg/persist-recording`), which opens a transaction and looks the
// recording up under both identities before upserting. The mock hands the
// transaction the same `ecgRecording` delegate, so these tests still watch the
// real assembly rather than a stub standing in for it.
vi.mock("@/lib/db", () => {
const ecgRecording = {
upsert: vi.fn(),
findUnique: vi.fn(),
};
return {
prisma: {
measurement: {
upsert: vi.fn(),
},
ecgRecording,
withingsConnection: {
findUnique: vi.fn(),
},
$transaction: vi.fn(
async (fn: (tx: unknown) => unknown) => await fn({ ecgRecording }),
),
},
ecgRecording: {
upsert: vi.fn(),
},
withingsConnection: {
findUnique: vi.fn(),
},
},
}));
};
});
vi.mock("../ecg-waveform-codec", () => ({
encryptWaveformToBytes: vi.fn((samples: number[]) =>
@@ -126,7 +138,14 @@ beforeEach(() => {
vi.mocked(prisma.measurement.upsert).mockResolvedValue({
id: "m-1",
} as never);
vi.mocked(prisma.ecgRecording.upsert).mockResolvedValue({} as never);
vi.mocked(prisma.ecgRecording.findUnique).mockResolvedValue(null as never);
vi.mocked(prisma.ecgRecording.upsert).mockResolvedValue({
id: "ecg-1",
} as never);
vi.mocked(prisma.$transaction).mockImplementation(
(async (fn: (tx: unknown) => unknown) =>
await fn({ ecgRecording: prisma.ecgRecording })) as never,
);
});
afterEach(() => {