mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
chore: align hardening tests and formatting
This commit is contained in:
1 parent
5d784a639c
commit
2716f302c7
35 files changed
+1044
-999
No files matched your search
@@ -39,6 +39,7 @@ vi.mock("@/lib/crypto", () => ({ decrypt: vi.fn(() => "{}") }));
|
||||
vi.mock("@/lib/crypto/note-cipher", () => ({ encryptNote: vi.fn() }));
|
||||
vi.mock("@/lib/validations/backup", () => ({
|
||||
parseBackupPayload: vi.fn(),
|
||||
isCompatibleSchemaVersion: vi.fn(() => true),
|
||||
summarizeBackup: vi.fn(() => ({})),
|
||||
}));
|
||||
vi.mock("@/lib/auth/audit", () => ({ auditLog: vi.fn() }));
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -85,8 +85,8 @@ beforeEach(() => {
|
||||
vi.mocked(prisma.device.deleteMany).mockResolvedValue({ count: 0 });
|
||||
vi.mocked(prisma.refreshToken.updateMany).mockResolvedValue({ count: 0 });
|
||||
vi.mocked(prisma.$queryRaw).mockResolvedValue([] as never);
|
||||
vi.mocked(prisma.$transaction).mockImplementation(
|
||||
async (callback) => callback(prisma as never),
|
||||
vi.mocked(prisma.$transaction).mockImplementation(async (callback) =>
|
||||
callback(prisma as never),
|
||||
);
|
||||
vi.mocked(prisma.notificationChannel.upsert).mockResolvedValue({
|
||||
id: "ch-1",
|
||||
|
||||
@@ -234,11 +234,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
// the APNs row with the newly presented metadata.
|
||||
let canonicalDevice = apnsDevice ?? legacyDevice;
|
||||
let duplicateDevice: typeof legacyDevice = null;
|
||||
if (
|
||||
apnsDevice &&
|
||||
legacyDevice &&
|
||||
apnsDevice.id !== legacyDevice.id
|
||||
) {
|
||||
if (apnsDevice && legacyDevice && apnsDevice.id !== legacyDevice.id) {
|
||||
duplicateDevice = legacyDevice;
|
||||
await tx.device.deleteMany({
|
||||
where: { id: legacyDevice.id, userId: user.id },
|
||||
|
||||
@@ -77,7 +77,9 @@ vi.mock("@/lib/measurements/reconcile-external-measurement", () => ({
|
||||
});
|
||||
return { status: "updated", row: { ...exact, ...input } };
|
||||
}
|
||||
const [inserted] = await tx.measurement.createManyAndReturn({ data: input });
|
||||
const [inserted] = await tx.measurement.createManyAndReturn({
|
||||
data: input,
|
||||
});
|
||||
if (inserted) return { status: "inserted", row: inserted };
|
||||
if (options.exactExternalMatch === "update") {
|
||||
await tx.measurement.updateMany({ where: {}, data: input });
|
||||
|
||||
@@ -58,7 +58,9 @@ vi.mock("@/lib/measurements/reconcile-external-measurement", () => ({
|
||||
await tx.measurement.updateMany({ where: { id: exact.id }, data: input });
|
||||
return { status: "updated", row: { ...exact, ...input } };
|
||||
}
|
||||
const [inserted] = await tx.measurement.createManyAndReturn({ data: input });
|
||||
const [inserted] = await tx.measurement.createManyAndReturn({
|
||||
data: input,
|
||||
});
|
||||
if (inserted) return { status: "inserted", row: inserted };
|
||||
if (options.exactExternalMatch === "update") {
|
||||
await tx.measurement.updateMany({ where: {}, data: input });
|
||||
|
||||
@@ -38,7 +38,9 @@ vi.mock("@/lib/measurements/reconcile-external-measurement", () => ({
|
||||
) => {
|
||||
const override = await reconcileOverrideMock(tx, input);
|
||||
if (override !== undefined) return override;
|
||||
const [inserted] = await tx.measurement.createManyAndReturn({ data: input });
|
||||
const [inserted] = await tx.measurement.createManyAndReturn({
|
||||
data: input,
|
||||
});
|
||||
return inserted
|
||||
? { status: "inserted", row: inserted }
|
||||
: { status: "duplicate" };
|
||||
|
||||
@@ -177,12 +177,7 @@ type BatchEntry = z.infer<typeof batchEntrySchema>;
|
||||
* sample is a canonical, immutable reading. `skipped` is a validation no-op;
|
||||
* `failed` is a hard database verdict and must not be checkpointed as landed.
|
||||
*/
|
||||
type EntryStatus =
|
||||
| "inserted"
|
||||
| "updated"
|
||||
| "duplicate"
|
||||
| "skipped"
|
||||
| "failed";
|
||||
type EntryStatus = "inserted" | "updated" | "duplicate" | "skipped" | "failed";
|
||||
|
||||
// v1.5.0 issue #213 — `stats:*` externalIds carry per-day cumulative
|
||||
// totals that the iOS HealthKit observer re-posts as the day progresses.
|
||||
@@ -475,103 +470,101 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
}
|
||||
|
||||
for (const p of prepared) {
|
||||
const externalId = p.row.externalId as string;
|
||||
const statsRow = isStatsExternalId(externalId);
|
||||
const key = `${p.row.type}::${p.row.source}::${externalId}`;
|
||||
if (statsRow && lastStatsIndexByKey.get(key) !== p.index) {
|
||||
results[p.index] = {
|
||||
index: p.index,
|
||||
status: "duplicate",
|
||||
reason: "superseded_in_batch",
|
||||
};
|
||||
duplicateCount++;
|
||||
continue;
|
||||
}
|
||||
const externalId = p.row.externalId as string;
|
||||
const statsRow = isStatsExternalId(externalId);
|
||||
const key = `${p.row.type}::${p.row.source}::${externalId}`;
|
||||
if (statsRow && lastStatsIndexByKey.get(key) !== p.index) {
|
||||
results[p.index] = {
|
||||
index: p.index,
|
||||
status: "duplicate",
|
||||
reason: "superseded_in_batch",
|
||||
};
|
||||
duplicateCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!statsRow && hasSameReadingSibling(p)) {
|
||||
results[p.index] = {
|
||||
index: p.index,
|
||||
status: "duplicate",
|
||||
reason: "cross_source_merge",
|
||||
};
|
||||
duplicateCount++;
|
||||
crossSourceMergedCount++;
|
||||
continue;
|
||||
}
|
||||
if (!statsRow && hasSameReadingSibling(p)) {
|
||||
results[p.index] = {
|
||||
index: p.index,
|
||||
status: "duplicate",
|
||||
reason: "cross_source_merge",
|
||||
};
|
||||
duplicateCount++;
|
||||
crossSourceMergedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const verdict = await prisma.$transaction((tx) =>
|
||||
reconcileExternalMeasurement(
|
||||
tx,
|
||||
{
|
||||
userId: user.id,
|
||||
type: p.row.type as MeasurementType,
|
||||
value: p.row.value as number,
|
||||
valueMin: p.row.valueMin as number | null,
|
||||
valueMax: p.row.valueMax as number | null,
|
||||
unit: p.row.unit as string,
|
||||
source:
|
||||
p.row.source as Prisma.MeasurementUncheckedCreateInput["source"],
|
||||
measuredAt: p.row.measuredAt as Date,
|
||||
externalId,
|
||||
externalSourceVersion: p.row
|
||||
.externalSourceVersion as string | null,
|
||||
glucoseContext:
|
||||
p.row.glucoseContext as Prisma.MeasurementUncheckedCreateInput["glucoseContext"],
|
||||
sleepStage:
|
||||
p.row.sleepStage as Prisma.MeasurementUncheckedCreateInput["sleepStage"],
|
||||
rhythmClassification:
|
||||
p.row
|
||||
.rhythmClassification as Prisma.MeasurementUncheckedCreateInput["rhythmClassification"],
|
||||
deviceType: p.row.deviceType as string | null,
|
||||
},
|
||||
{ exactExternalMatch: statsRow ? "update" : "duplicate" },
|
||||
),
|
||||
);
|
||||
|
||||
switch (verdict.status) {
|
||||
case "inserted":
|
||||
results[p.index] = { index: p.index, status: "inserted" };
|
||||
insertedCount++;
|
||||
insertedPrepared.push(p);
|
||||
writtenIdentities.push({
|
||||
type: p.row.type as MeasurementType,
|
||||
measuredAt: p.row.measuredAt as Date,
|
||||
});
|
||||
break;
|
||||
case "updated":
|
||||
case "resurrected":
|
||||
results[p.index] = { index: p.index, status: "updated" };
|
||||
updatedCount++;
|
||||
writtenIdentities.push(
|
||||
{
|
||||
type: p.row.type as MeasurementType,
|
||||
measuredAt: p.row.measuredAt as Date,
|
||||
},
|
||||
...(verdict.dirtyIdentities ?? []),
|
||||
);
|
||||
break;
|
||||
case "duplicate":
|
||||
results[p.index] = { index: p.index, status: "duplicate" };
|
||||
duplicateCount++;
|
||||
break;
|
||||
case "failed":
|
||||
results[p.index] = {
|
||||
index: p.index,
|
||||
status: "failed",
|
||||
reason: verdict.error.code ?? "write_failed",
|
||||
};
|
||||
failedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isMergeableSource(p.row.source as string)) {
|
||||
inBatchCandidates.push({
|
||||
const verdict = await prisma.$transaction((tx) =>
|
||||
reconcileExternalMeasurement(
|
||||
tx,
|
||||
{
|
||||
userId: user.id,
|
||||
type: p.row.type as MeasurementType,
|
||||
source: p.row.source as string,
|
||||
value: p.row.value as number,
|
||||
valueMin: p.row.valueMin as number | null,
|
||||
valueMax: p.row.valueMax as number | null,
|
||||
unit: p.row.unit as string,
|
||||
source: p.row
|
||||
.source as Prisma.MeasurementUncheckedCreateInput["source"],
|
||||
measuredAt: p.row.measuredAt as Date,
|
||||
externalId,
|
||||
externalSourceVersion: p.row.externalSourceVersion as string | null,
|
||||
glucoseContext: p.row
|
||||
.glucoseContext as Prisma.MeasurementUncheckedCreateInput["glucoseContext"],
|
||||
sleepStage: p.row
|
||||
.sleepStage as Prisma.MeasurementUncheckedCreateInput["sleepStage"],
|
||||
rhythmClassification: p.row
|
||||
.rhythmClassification as Prisma.MeasurementUncheckedCreateInput["rhythmClassification"],
|
||||
deviceType: p.row.deviceType as string | null,
|
||||
},
|
||||
{ exactExternalMatch: statsRow ? "update" : "duplicate" },
|
||||
),
|
||||
);
|
||||
|
||||
switch (verdict.status) {
|
||||
case "inserted":
|
||||
results[p.index] = { index: p.index, status: "inserted" };
|
||||
insertedCount++;
|
||||
insertedPrepared.push(p);
|
||||
writtenIdentities.push({
|
||||
type: p.row.type as MeasurementType,
|
||||
measuredAt: p.row.measuredAt as Date,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "updated":
|
||||
case "resurrected":
|
||||
results[p.index] = { index: p.index, status: "updated" };
|
||||
updatedCount++;
|
||||
writtenIdentities.push(
|
||||
{
|
||||
type: p.row.type as MeasurementType,
|
||||
measuredAt: p.row.measuredAt as Date,
|
||||
},
|
||||
...(verdict.dirtyIdentities ?? []),
|
||||
);
|
||||
break;
|
||||
case "duplicate":
|
||||
results[p.index] = { index: p.index, status: "duplicate" };
|
||||
duplicateCount++;
|
||||
break;
|
||||
case "failed":
|
||||
results[p.index] = {
|
||||
index: p.index,
|
||||
status: "failed",
|
||||
reason: verdict.error.code ?? "write_failed",
|
||||
};
|
||||
failedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isMergeableSource(p.row.source as string)) {
|
||||
inBatchCandidates.push({
|
||||
type: p.row.type as MeasurementType,
|
||||
source: p.row.source as string,
|
||||
value: p.row.value as number,
|
||||
measuredAt: p.row.measuredAt as Date,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ const txClient = vi.hoisted(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
prisma: {
|
||||
moodEntry: {
|
||||
@@ -222,9 +221,7 @@ describe("PUT /api/mood-entries/[id] — split tag replacement", () => {
|
||||
error: null;
|
||||
};
|
||||
expect(body.data.tagKeys).toEqual(["happy"]);
|
||||
expect(body.data.ratedFactors).toEqual([
|
||||
{ key: "factor_work", rating: 4 },
|
||||
]);
|
||||
expect(body.data.ratedFactors).toEqual([{ key: "factor_work", rating: 4 }]);
|
||||
expect(body.error).toBeNull();
|
||||
});
|
||||
|
||||
@@ -254,9 +251,7 @@ describe("PUT /api/mood-entries/[id] — split tag replacement", () => {
|
||||
};
|
||||
};
|
||||
expect(body.data.tagKeys).toEqual(["happy"]);
|
||||
expect(body.data.ratedFactors).toEqual([
|
||||
{ key: "factor_work", rating: 5 },
|
||||
]);
|
||||
expect(body.data.ratedFactors).toEqual([{ key: "factor_work", rating: 5 }]);
|
||||
});
|
||||
|
||||
it("returns 422 when a factor score is outside its catalog scale", async () => {
|
||||
@@ -333,18 +328,18 @@ describe("PUT /api/mood-entries/[id] — split tag replacement", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ ratedFactors: null },
|
||||
{ ratedFactors: [] },
|
||||
])("clears RATED links for an explicit $ratedFactors", async (payload) => {
|
||||
const res = await PUT(putReq(payload), ROUTE_CTX);
|
||||
it.each([{ ratedFactors: null }, { ratedFactors: [] }])(
|
||||
"clears RATED links for an explicit $ratedFactors",
|
||||
async (payload) => {
|
||||
const res = await PUT(putReq(payload), ROUTE_CTX);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(replaceRatedFactorLinks).toHaveBeenCalledWith(
|
||||
"me1",
|
||||
"user-1",
|
||||
[],
|
||||
txClient,
|
||||
);
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(replaceRatedFactorLinks).toHaveBeenCalledWith(
|
||||
"me1",
|
||||
"user-1",
|
||||
[],
|
||||
txClient,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -46,9 +46,7 @@ function makePrisma() {
|
||||
externalId: "moodlog-42",
|
||||
moodLoggedAt: new Date("2026-07-19T08:00:00.000Z"),
|
||||
deletedAt,
|
||||
tagLinks: [
|
||||
{ rating: 4, moodTag: { key: "sleep_quality" } },
|
||||
],
|
||||
tagLinks: [{ rating: 4, moodTag: { key: "sleep_quality" } }],
|
||||
},
|
||||
]),
|
||||
},
|
||||
@@ -94,10 +92,14 @@ describe("buildFullBackupPayload disaster-recovery mode", () => {
|
||||
installSectionMocks();
|
||||
const prisma = makePrisma();
|
||||
|
||||
const { payload } = await buildFullBackupPayload(prisma as never, "user-1", {
|
||||
purpose: "disaster-recovery",
|
||||
exportedAt: new Date("2026-07-20T00:00:00.000Z"),
|
||||
});
|
||||
const { payload } = await buildFullBackupPayload(
|
||||
prisma as never,
|
||||
"user-1",
|
||||
{
|
||||
purpose: "disaster-recovery",
|
||||
exportedAt: new Date("2026-07-20T00:00:00.000Z"),
|
||||
},
|
||||
);
|
||||
|
||||
expect(mocks.findMeasurementsPaged).toHaveBeenCalledWith(
|
||||
prisma,
|
||||
|
||||
@@ -368,9 +368,7 @@ export async function buildRecordsBackupSection(
|
||||
mimeType: d.mimeType,
|
||||
byteSize: d.byteSize,
|
||||
status: d.status,
|
||||
reportDate: d.reportDate
|
||||
? d.reportDate.toISOString().slice(0, 10)
|
||||
: null,
|
||||
reportDate: d.reportDate ? d.reportDate.toISOString().slice(0, 10) : null,
|
||||
documentDate: d.documentDate
|
||||
? d.documentDate.toISOString().slice(0, 10)
|
||||
: null,
|
||||
@@ -415,8 +413,7 @@ export async function buildRecordsBackupSection(
|
||||
documents: disasterRecovery
|
||||
? {
|
||||
included: "encrypted-content",
|
||||
note:
|
||||
"Document metadata and encrypted stored bytes are included for disaster recovery.",
|
||||
note: "Document metadata and encrypted stored bytes are included for disaster recovery.",
|
||||
}
|
||||
: { included: "metadata-only", note: DOCUMENTS_MANIFEST_NOTE },
|
||||
workouts: { included: "summary-only", note: WORKOUTS_MANIFEST_NOTE },
|
||||
|
||||
@@ -3,12 +3,13 @@ import type { Job } from "pg-boss";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
const { syncUserEcg } = vi.hoisted(() => ({
|
||||
syncUserEcg: vi.fn<
|
||||
(
|
||||
userId: string,
|
||||
options?: { startdate?: number; enddate?: number },
|
||||
) => Promise<number>
|
||||
>(),
|
||||
syncUserEcg:
|
||||
vi.fn<
|
||||
(
|
||||
userId: string,
|
||||
options?: { startdate?: number; enddate?: number },
|
||||
) => Promise<number>
|
||||
>(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/withings/sync-ecg", () => ({
|
||||
@@ -70,10 +71,7 @@ beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
const registrarSource = readFileSync(
|
||||
join(
|
||||
process.cwd(),
|
||||
"src/lib/jobs/reminder/register-integration-sync.ts",
|
||||
),
|
||||
join(process.cwd(), "src/lib/jobs/reminder/register-integration-sync.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ const documentCiphertext = Buffer.from([1, 2, 3, 4]).toString("base64");
|
||||
function buildPrismaMock() {
|
||||
return {
|
||||
user: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{ id: "user-dr", username: "backup-owner" },
|
||||
]),
|
||||
findMany: vi
|
||||
.fn()
|
||||
.mockResolvedValue([{ id: "user-dr", username: "backup-owner" }]),
|
||||
},
|
||||
dataBackup: { upsert: mocks.upsert },
|
||||
};
|
||||
|
||||
@@ -58,11 +58,9 @@ export async function handleDataBackup(jobs: Job<DataBackupPayload>[]) {
|
||||
let backed = 0;
|
||||
for (const user of users) {
|
||||
try {
|
||||
const { payload } = await buildFullBackupPayload(
|
||||
prisma,
|
||||
user.id,
|
||||
{ purpose: "disaster-recovery" },
|
||||
);
|
||||
const { payload } = await buildFullBackupPayload(prisma, user.id, {
|
||||
purpose: "disaster-recovery",
|
||||
});
|
||||
const backupJson = JSON.stringify(payload);
|
||||
|
||||
// Encrypt the backup data (contains sensitive health information)
|
||||
|
||||
@@ -17,7 +17,6 @@ import { enqueueReminderSatisfy } from "@/lib/jobs/reminder-satisfy";
|
||||
import { getWorkerPrisma } from "./shared";
|
||||
export type { WithingsEcgSyncPayload } from "@/lib/jobs/withings-ecg-queue";
|
||||
|
||||
|
||||
export interface WithingsSyncPayload {
|
||||
triggeredAt: string;
|
||||
}
|
||||
|
||||
@@ -49,9 +49,7 @@ export async function enqueueWithingsEcgSync(
|
||||
userId: identity.userId,
|
||||
eventId,
|
||||
triggeredAt: new Date().toISOString(),
|
||||
...(parsedStartdate?.success
|
||||
? { startdate: parsedStartdate.data }
|
||||
: {}),
|
||||
...(parsedStartdate?.success ? { startdate: parsedStartdate.data } : {}),
|
||||
...(parsedEnddate?.success ? { enddate: parsedEnddate.data } : {}),
|
||||
} satisfies WithingsEcgSyncPayload,
|
||||
{
|
||||
|
||||
@@ -24,9 +24,7 @@ function desired(
|
||||
};
|
||||
}
|
||||
|
||||
function row(
|
||||
overrides: Record<string, unknown> = {},
|
||||
): Record<string, unknown> {
|
||||
function row(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return {
|
||||
id: "row-1",
|
||||
userId: "user-1",
|
||||
|
||||
@@ -36,10 +36,7 @@ export type ReconciledMeasurementRow = Pick<
|
||||
"id" | "type" | "measuredAt" | "externalId"
|
||||
>;
|
||||
|
||||
export type DirtyMeasurementIdentity = Pick<
|
||||
Measurement,
|
||||
"type" | "measuredAt"
|
||||
>;
|
||||
export type DirtyMeasurementIdentity = Pick<Measurement, "type" | "measuredAt">;
|
||||
|
||||
export type MeasurementReconciliationVerdict =
|
||||
| {
|
||||
@@ -126,7 +123,9 @@ async function lockUser(
|
||||
async function lockAndReloadCandidates(
|
||||
tx: PrismaTypes.TransactionClient,
|
||||
desired: ExternalMeasurementWrite,
|
||||
): Promise<Array<PrismaTypes.MeasurementGetPayload<{ select: typeof rowSelect }>>> {
|
||||
): Promise<
|
||||
Array<PrismaTypes.MeasurementGetPayload<{ select: typeof rowSelect }>>
|
||||
> {
|
||||
const where = {
|
||||
userId: desired.userId,
|
||||
OR: [
|
||||
@@ -262,6 +261,7 @@ export async function reconcileExternalMeasurement(
|
||||
}
|
||||
|
||||
const { userId: _ownerId, ...mutable } = desired;
|
||||
void _ownerId;
|
||||
const updated = await tx.measurement.update({
|
||||
where: { id: canonical.id },
|
||||
data: {
|
||||
|
||||
@@ -272,7 +272,6 @@ describe("replaceTagLinks — archived-tag links survive an entry edit", () => {
|
||||
skipDuplicates: true,
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("entry-ownership guard", () => {
|
||||
|
||||
@@ -142,17 +142,22 @@ beforeEach(() => {
|
||||
transactionMock
|
||||
.mockReset()
|
||||
.mockImplementation(async (run: (tx: unknown) => unknown) => run({}));
|
||||
reconcileMock.mockReset().mockImplementation(
|
||||
async (_tx: unknown, input: { type: string; measuredAt: Date; externalId: string }) => ({
|
||||
status: "inserted",
|
||||
row: {
|
||||
id: `inserted:${input.externalId}`,
|
||||
type: input.type,
|
||||
measuredAt: input.measuredAt,
|
||||
externalId: input.externalId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
reconcileMock
|
||||
.mockReset()
|
||||
.mockImplementation(
|
||||
async (
|
||||
_tx: unknown,
|
||||
input: { type: string; measuredAt: Date; externalId: string },
|
||||
) => ({
|
||||
status: "inserted",
|
||||
row: {
|
||||
id: `inserted:${input.externalId}`,
|
||||
type: input.type,
|
||||
measuredAt: input.measuredAt,
|
||||
externalId: input.externalId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
createManyAndReturnMock.mockReset().mockImplementation(
|
||||
async ({
|
||||
data,
|
||||
|
||||
+26
-23
@@ -461,30 +461,33 @@ export async function upsertOuraMeasurements(
|
||||
InsertedMeasurementArrivalRow & { externalId: string | null }
|
||||
> = [];
|
||||
|
||||
const verdicts = await prisma.$transaction(async (tx) => {
|
||||
const outcomes = [];
|
||||
for (const r of readings) {
|
||||
const verdict = await reconcileExternalMeasurement(
|
||||
tx,
|
||||
{
|
||||
userId,
|
||||
type: r.type as MeasurementType,
|
||||
source: "OURA",
|
||||
value: r.value,
|
||||
unit: r.unit,
|
||||
measuredAt: r.measuredAt,
|
||||
externalId: r.externalId,
|
||||
sleepStage: r.sleepStage ?? null,
|
||||
},
|
||||
{ exactExternalMatch: "update" },
|
||||
);
|
||||
if (verdict.status === "failed") {
|
||||
throw new MeasurementReconciliationError(verdict);
|
||||
const verdicts = await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const outcomes = [];
|
||||
for (const r of readings) {
|
||||
const verdict = await reconcileExternalMeasurement(
|
||||
tx,
|
||||
{
|
||||
userId,
|
||||
type: r.type as MeasurementType,
|
||||
source: "OURA",
|
||||
value: r.value,
|
||||
unit: r.unit,
|
||||
measuredAt: r.measuredAt,
|
||||
externalId: r.externalId,
|
||||
sleepStage: r.sleepStage ?? null,
|
||||
},
|
||||
{ exactExternalMatch: "update" },
|
||||
);
|
||||
if (verdict.status === "failed") {
|
||||
throw new MeasurementReconciliationError(verdict);
|
||||
}
|
||||
outcomes.push(verdict);
|
||||
}
|
||||
outcomes.push(verdict);
|
||||
}
|
||||
return outcomes;
|
||||
}, { timeout: 60_000 });
|
||||
return outcomes;
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
|
||||
for (let index = 0; index < readings.length; index++) {
|
||||
const reading = readings[index]!;
|
||||
|
||||
@@ -106,17 +106,22 @@ beforeEach(() => {
|
||||
transactionMock
|
||||
.mockReset()
|
||||
.mockImplementation(async (run: (tx: unknown) => unknown) => run({}));
|
||||
reconcileMock.mockReset().mockImplementation(
|
||||
async (_tx: unknown, input: { type: string; measuredAt: Date; externalId: string }) => ({
|
||||
status: "inserted",
|
||||
row: {
|
||||
id: `inserted:${input.externalId}`,
|
||||
type: input.type,
|
||||
measuredAt: input.measuredAt,
|
||||
externalId: input.externalId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
reconcileMock
|
||||
.mockReset()
|
||||
.mockImplementation(
|
||||
async (
|
||||
_tx: unknown,
|
||||
input: { type: string; measuredAt: Date; externalId: string },
|
||||
) => ({
|
||||
status: "inserted",
|
||||
row: {
|
||||
id: `inserted:${input.externalId}`,
|
||||
type: input.type,
|
||||
measuredAt: input.measuredAt,
|
||||
externalId: input.externalId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
createManyAndReturnMock.mockReset().mockImplementation(
|
||||
async ({
|
||||
data,
|
||||
|
||||
+26
-23
@@ -210,30 +210,33 @@ export async function upsertPolarMeasurements(
|
||||
InsertedMeasurementArrivalRow & { externalId: string | null }
|
||||
> = [];
|
||||
|
||||
const verdicts = await prisma.$transaction(async (tx) => {
|
||||
const outcomes = [];
|
||||
for (const r of readings) {
|
||||
const verdict = await reconcileExternalMeasurement(
|
||||
tx,
|
||||
{
|
||||
userId,
|
||||
type: r.type as MeasurementType,
|
||||
source: "POLAR",
|
||||
value: r.value,
|
||||
unit: r.unit,
|
||||
measuredAt: r.measuredAt,
|
||||
externalId: r.externalId,
|
||||
sleepStage: r.sleepStage ?? null,
|
||||
},
|
||||
{ exactExternalMatch: "update" },
|
||||
);
|
||||
if (verdict.status === "failed") {
|
||||
throw new MeasurementReconciliationError(verdict);
|
||||
const verdicts = await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const outcomes = [];
|
||||
for (const r of readings) {
|
||||
const verdict = await reconcileExternalMeasurement(
|
||||
tx,
|
||||
{
|
||||
userId,
|
||||
type: r.type as MeasurementType,
|
||||
source: "POLAR",
|
||||
value: r.value,
|
||||
unit: r.unit,
|
||||
measuredAt: r.measuredAt,
|
||||
externalId: r.externalId,
|
||||
sleepStage: r.sleepStage ?? null,
|
||||
},
|
||||
{ exactExternalMatch: "update" },
|
||||
);
|
||||
if (verdict.status === "failed") {
|
||||
throw new MeasurementReconciliationError(verdict);
|
||||
}
|
||||
outcomes.push(verdict);
|
||||
}
|
||||
outcomes.push(verdict);
|
||||
}
|
||||
return outcomes;
|
||||
}, { timeout: 60_000 });
|
||||
return outcomes;
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
|
||||
for (let index = 0; index < readings.length; index++) {
|
||||
const reading = readings[index]!;
|
||||
|
||||
@@ -68,7 +68,10 @@ export function classifyStravaFailure(err: unknown): FailureKind {
|
||||
return toFailureKind(classifyStravaError(err));
|
||||
}
|
||||
|
||||
async function recordStravaFailure(userId: string, err: unknown): Promise<void> {
|
||||
async function recordStravaFailure(
|
||||
userId: string,
|
||||
err: unknown,
|
||||
): Promise<void> {
|
||||
await recordSyncFailure({
|
||||
userId,
|
||||
integration: "strava",
|
||||
|
||||
@@ -46,7 +46,6 @@ import {
|
||||
SecondarySymptom,
|
||||
} from "@/generated/prisma/enums";
|
||||
|
||||
|
||||
export const BACKUP_SCHEMA_VERSION = "1" as const;
|
||||
|
||||
const isoDateTime = z
|
||||
@@ -351,8 +350,7 @@ const base64BytesSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(
|
||||
(value) =>
|
||||
value.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value),
|
||||
(value) => value.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value),
|
||||
{ message: "Expected base64-encoded encrypted bytes" },
|
||||
);
|
||||
|
||||
|
||||
@@ -118,7 +118,10 @@ beforeEach(() => {
|
||||
async (run: (tx: unknown) => unknown) => run(prismaMock),
|
||||
);
|
||||
reconcileMock.mockImplementation(
|
||||
async (_tx: unknown, input: { type: string; measuredAt: Date; externalId: string }) => ({
|
||||
async (
|
||||
_tx: unknown,
|
||||
input: { type: string; measuredAt: Date; externalId: string },
|
||||
) => ({
|
||||
status: "inserted",
|
||||
row: {
|
||||
id: `inserted:${input.externalId}`,
|
||||
@@ -167,7 +170,6 @@ describe("getValidToken — rotating refresh", () => {
|
||||
expect(prismaMock.$queryRaw).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
it("re-reads token state under an advisory lock and reuses a peer winner", async () => {
|
||||
prismaMock.whoopConnection.findUnique
|
||||
.mockResolvedValueOnce({
|
||||
@@ -549,7 +551,10 @@ describe("syncUserRecovery — durable cursor ordering", () => {
|
||||
});
|
||||
fetchRecoveriesMock.mockResolvedValue([recovery]);
|
||||
reconcileMock.mockImplementation(
|
||||
async (_tx: unknown, input: { type: string; measuredAt: Date; externalId: string }) => ({
|
||||
async (
|
||||
_tx: unknown,
|
||||
input: { type: string; measuredAt: Date; externalId: string },
|
||||
) => ({
|
||||
status: "updated",
|
||||
row: {
|
||||
id: `existing:${input.externalId}`,
|
||||
|
||||
@@ -107,11 +107,14 @@ function installFetchMock(segments: FakeSegment[]) {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
transactionMock.mockImplementation(
|
||||
async (run: (tx: unknown) => unknown) => run({}),
|
||||
transactionMock.mockImplementation(async (run: (tx: unknown) => unknown) =>
|
||||
run({}),
|
||||
);
|
||||
reconcileMock.mockImplementation(
|
||||
async (_tx: unknown, input: { type: string; measuredAt: Date; externalId: string }) => ({
|
||||
async (
|
||||
_tx: unknown,
|
||||
input: { type: string; measuredAt: Date; externalId: string },
|
||||
) => ({
|
||||
status: "inserted",
|
||||
row: {
|
||||
id: `inserted:${input.externalId}`,
|
||||
@@ -502,10 +505,7 @@ describe("syncUserSleep — segment writes + idempotency", () => {
|
||||
externalId: string;
|
||||
value: number;
|
||||
};
|
||||
return [
|
||||
row.type,
|
||||
{ externalId: row.externalId, value: row.value },
|
||||
];
|
||||
return [row.type, { externalId: row.externalId, value: row.value }];
|
||||
}),
|
||||
);
|
||||
expect(byType.get("RESTING_HEART_RATE")).toEqual({
|
||||
|
||||
@@ -496,7 +496,6 @@ export async function syncUserEcg(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
annotate({
|
||||
action: { name: "withings.ecg.sync" },
|
||||
meta: {
|
||||
|
||||
@@ -412,48 +412,51 @@ export async function syncUserSleep(
|
||||
// and are excluded (their `no-id` prefix would span every id-less night in
|
||||
// history, not just this fetch).
|
||||
const freshBySession = new Map<number, string[]>();
|
||||
await prisma.$transaction(async (tx) => {
|
||||
for (const segment of segments) {
|
||||
const stage = mapWithingsSleepState(segment.state);
|
||||
if (!stage) continue;
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
for (const segment of segments) {
|
||||
const stage = mapWithingsSleepState(segment.state);
|
||||
if (!stage) continue;
|
||||
|
||||
const measuredAt = new Date(segment.enddate * 1000);
|
||||
const durationSec = Math.max(0, segment.enddate - segment.startdate);
|
||||
const minutes = Math.round(durationSec / 60);
|
||||
const externalId = `withings:sleep:${userId}:${segment.id ?? "no-id"}:${segment.startdate}`;
|
||||
if (typeof segment.id === "number") {
|
||||
const fresh = freshBySession.get(segment.id) ?? [];
|
||||
fresh.push(externalId);
|
||||
freshBySession.set(segment.id, fresh);
|
||||
}
|
||||
const measuredAt = new Date(segment.enddate * 1000);
|
||||
const durationSec = Math.max(0, segment.enddate - segment.startdate);
|
||||
const minutes = Math.round(durationSec / 60);
|
||||
const externalId = `withings:sleep:${userId}:${segment.id ?? "no-id"}:${segment.startdate}`;
|
||||
if (typeof segment.id === "number") {
|
||||
const fresh = freshBySession.get(segment.id) ?? [];
|
||||
fresh.push(externalId);
|
||||
freshBySession.set(segment.id, fresh);
|
||||
}
|
||||
|
||||
const verdict = await reconcileExternalMeasurement(
|
||||
tx,
|
||||
{
|
||||
userId,
|
||||
type: SLEEP_TYPE,
|
||||
value: minutes,
|
||||
unit: getUnitForType(SLEEP_TYPE),
|
||||
measuredAt,
|
||||
source: "WITHINGS",
|
||||
sleepStage: stage,
|
||||
externalId,
|
||||
},
|
||||
{ exactExternalMatch: "update" },
|
||||
);
|
||||
if (verdict.status === "failed") {
|
||||
throw new MeasurementReconciliationError(verdict);
|
||||
const verdict = await reconcileExternalMeasurement(
|
||||
tx,
|
||||
{
|
||||
userId,
|
||||
type: SLEEP_TYPE,
|
||||
value: minutes,
|
||||
unit: getUnitForType(SLEEP_TYPE),
|
||||
measuredAt,
|
||||
source: "WITHINGS",
|
||||
sleepStage: stage,
|
||||
externalId,
|
||||
},
|
||||
{ exactExternalMatch: "update" },
|
||||
);
|
||||
if (verdict.status === "failed") {
|
||||
throw new MeasurementReconciliationError(verdict);
|
||||
}
|
||||
for (const dirty of verdict.dirtyIdentities ?? []) {
|
||||
touched.push(dirty);
|
||||
}
|
||||
if (verdict.status === "inserted") {
|
||||
insertedSleepMeasuredAts.push(measuredAt);
|
||||
}
|
||||
touched.push({ type: SLEEP_TYPE, measuredAt });
|
||||
imported++;
|
||||
}
|
||||
for (const dirty of verdict.dirtyIdentities ?? []) {
|
||||
touched.push(dirty);
|
||||
}
|
||||
if (verdict.status === "inserted") {
|
||||
insertedSleepMeasuredAts.push(measuredAt);
|
||||
}
|
||||
touched.push({ type: SLEEP_TYPE, measuredAt });
|
||||
imported++;
|
||||
}
|
||||
}, { timeout: 60_000 });
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
|
||||
// Session-scoped sweep (mirrors Google Health's replace-by-window): for each
|
||||
// session this fetch re-produced, soft-delete any live WITHINGS
|
||||
@@ -486,35 +489,38 @@ export async function syncUserSleep(
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
for (const summary of summaries) {
|
||||
const measuredAt = new Date(summary.enddate * 1000);
|
||||
const sessionId = summary.id ?? "no-id";
|
||||
for (const vital of mapWithingsSleepSummary(summary.data)) {
|
||||
const verdict = await reconcileExternalMeasurement(
|
||||
tx,
|
||||
{
|
||||
userId,
|
||||
type: vital.type,
|
||||
value: vital.value,
|
||||
unit: getUnitForType(vital.type),
|
||||
measuredAt,
|
||||
source: "WITHINGS",
|
||||
externalId: `withings:sleep:${userId}:${sessionId}:${vital.fieldTag}`,
|
||||
},
|
||||
{ exactExternalMatch: "update" },
|
||||
);
|
||||
if (verdict.status === "failed") {
|
||||
throw new MeasurementReconciliationError(verdict);
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
for (const summary of summaries) {
|
||||
const measuredAt = new Date(summary.enddate * 1000);
|
||||
const sessionId = summary.id ?? "no-id";
|
||||
for (const vital of mapWithingsSleepSummary(summary.data)) {
|
||||
const verdict = await reconcileExternalMeasurement(
|
||||
tx,
|
||||
{
|
||||
userId,
|
||||
type: vital.type,
|
||||
value: vital.value,
|
||||
unit: getUnitForType(vital.type),
|
||||
measuredAt,
|
||||
source: "WITHINGS",
|
||||
externalId: `withings:sleep:${userId}:${sessionId}:${vital.fieldTag}`,
|
||||
},
|
||||
{ exactExternalMatch: "update" },
|
||||
);
|
||||
if (verdict.status === "failed") {
|
||||
throw new MeasurementReconciliationError(verdict);
|
||||
}
|
||||
for (const dirty of verdict.dirtyIdentities ?? []) {
|
||||
touched.push(dirty);
|
||||
}
|
||||
touched.push({ type: vital.type, measuredAt });
|
||||
imported++;
|
||||
}
|
||||
for (const dirty of verdict.dirtyIdentities ?? []) {
|
||||
touched.push(dirty);
|
||||
}
|
||||
touched.push({ type: vital.type, measuredAt });
|
||||
imported++;
|
||||
}
|
||||
}
|
||||
}, { timeout: 60_000 });
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
|
||||
// v1.4.39.1 — refresh the persistent rollup table for every distinct
|
||||
// (type, day) the sync touched. Sleep segments collapse heavily —
|
||||
|
||||
@@ -12,7 +12,6 @@ process.env.BACKUP_S3_ACCESS_KEY = "test-access";
|
||||
process.env.BACKUP_S3_SECRET_KEY = "test-secret";
|
||||
process.env.BACKUP_ENCRYPTION_KEY = OFFHOST_ENCRYPTION_KEY;
|
||||
|
||||
|
||||
import { encrypt, encryptBytes } from "@/lib/crypto";
|
||||
import { encryptToBytes, decryptFromBytes } from "@/lib/ai/coach/bytes-codec";
|
||||
import {
|
||||
@@ -337,15 +336,10 @@ describe("canonical disaster-recovery backup round-trip", () => {
|
||||
failed: 0,
|
||||
totalUsers: 1,
|
||||
});
|
||||
const offhostObject = s3.objects.get(
|
||||
`2026-07-02/user-${ownerId}.json.enc`,
|
||||
);
|
||||
const offhostObject = s3.objects.get(`2026-07-02/user-${ownerId}.json.enc`);
|
||||
expect(offhostObject).toBeDefined();
|
||||
const payload = parseBackupPayload(
|
||||
decryptBackup(
|
||||
offhostObject!,
|
||||
Buffer.from(OFFHOST_ENCRYPTION_KEY, "hex"),
|
||||
),
|
||||
decryptBackup(offhostObject!, Buffer.from(OFFHOST_ENCRYPTION_KEY, "hex")),
|
||||
);
|
||||
expect(payload).toMatchObject({
|
||||
schemaVersion: "1",
|
||||
@@ -379,14 +373,26 @@ describe("canonical disaster-recovery backup round-trip", () => {
|
||||
|
||||
expect((await restore()).status).toBe(200);
|
||||
|
||||
expect(await prisma.labResult.count({ where: { userId: ownerId } })).toBe(1);
|
||||
expect(await prisma.biomarker.count({ where: { userId: ownerId } })).toBe(1);
|
||||
expect(await prisma.illnessEpisode.count({ where: { userId: ownerId } })).toBe(1);
|
||||
expect(await prisma.illnessDayLog.count({ where: { userId: ownerId } })).toBe(1);
|
||||
expect(await prisma.labResult.count({ where: { userId: ownerId } })).toBe(
|
||||
1,
|
||||
);
|
||||
expect(await prisma.biomarker.count({ where: { userId: ownerId } })).toBe(
|
||||
1,
|
||||
);
|
||||
expect(
|
||||
await prisma.illnessEpisode.count({ where: { userId: ownerId } }),
|
||||
).toBe(1);
|
||||
expect(
|
||||
await prisma.illnessDayLog.count({ where: { userId: ownerId } }),
|
||||
).toBe(1);
|
||||
expect(await prisma.allergy.count({ where: { userId: ownerId } })).toBe(1);
|
||||
expect(await prisma.familyHistoryEntry.count({ where: { userId: ownerId } })).toBe(1);
|
||||
expect(
|
||||
await prisma.familyHistoryEntry.count({ where: { userId: ownerId } }),
|
||||
).toBe(1);
|
||||
expect(await prisma.workout.count({ where: { userId: ownerId } })).toBe(1);
|
||||
expect(await prisma.inboundDocument.count({ where: { userId: ownerId } })).toBe(1);
|
||||
expect(
|
||||
await prisma.inboundDocument.count({ where: { userId: ownerId } }),
|
||||
).toBe(1);
|
||||
expect(
|
||||
await prisma.measurement.findUniqueOrThrow({
|
||||
where: { id: measurement.id },
|
||||
@@ -466,7 +472,9 @@ describe("canonical disaster-recovery backup round-trip", () => {
|
||||
where: { id: illnessDay.id },
|
||||
include: { symptomLinks: true },
|
||||
});
|
||||
expect(decryptFromBytes(restoredIllnessDay.noteEncrypted!)).toBe("Rest day");
|
||||
expect(decryptFromBytes(restoredIllnessDay.noteEncrypted!)).toBe(
|
||||
"Rest day",
|
||||
);
|
||||
expect(restoredIllnessDay.symptomLinks).toEqual([
|
||||
expect.objectContaining({ symptomId: symptom.id, severity: 2 }),
|
||||
]);
|
||||
@@ -625,55 +633,57 @@ describe("canonical disaster-recovery backup round-trip", () => {
|
||||
payload.schemaVersion = "999";
|
||||
},
|
||||
},
|
||||
])("rejects $label with 422 before writes or invalidation", async ({ mutate }) => {
|
||||
const prisma = getPrismaClient();
|
||||
const admin = await seedAdminSession();
|
||||
const existing = await prisma.measurement.create({
|
||||
data: {
|
||||
id: "pre-validation-measurement",
|
||||
])(
|
||||
"rejects $label with 422 before writes or invalidation",
|
||||
async ({ mutate }) => {
|
||||
const prisma = getPrismaClient();
|
||||
const admin = await seedAdminSession();
|
||||
const existing = await prisma.measurement.create({
|
||||
data: {
|
||||
id: "pre-validation-measurement",
|
||||
userId: admin.id,
|
||||
type: "WEIGHT",
|
||||
value: 91,
|
||||
unit: "kg",
|
||||
source: "MANUAL",
|
||||
measuredAt: new Date("2026-07-03T06:00:00.000Z"),
|
||||
},
|
||||
});
|
||||
const payload: Record<string, unknown> = {
|
||||
schemaVersion: "1",
|
||||
exportedAt: "2026-07-03T08:00:00.000Z",
|
||||
userId: admin.id,
|
||||
type: "WEIGHT",
|
||||
value: 91,
|
||||
unit: "kg",
|
||||
source: "MANUAL",
|
||||
measuredAt: new Date("2026-07-03T06:00:00.000Z"),
|
||||
},
|
||||
});
|
||||
const payload: Record<string, unknown> = {
|
||||
schemaVersion: "1",
|
||||
exportedAt: "2026-07-03T08:00:00.000Z",
|
||||
userId: admin.id,
|
||||
measurements: [],
|
||||
moodEntries: [],
|
||||
};
|
||||
mutate(payload);
|
||||
const backup = await prisma.dataBackup.create({
|
||||
data: {
|
||||
userId: admin.id,
|
||||
type: `INVALID_BOUNDARY_${String(payload.schemaVersion)}`,
|
||||
data: encrypt(JSON.stringify(payload)),
|
||||
},
|
||||
});
|
||||
measurements: [],
|
||||
moodEntries: [],
|
||||
};
|
||||
mutate(payload);
|
||||
const backup = await prisma.dataBackup.create({
|
||||
data: {
|
||||
userId: admin.id,
|
||||
type: `INVALID_BOUNDARY_${String(payload.schemaVersion)}`,
|
||||
data: encrypt(JSON.stringify(payload)),
|
||||
},
|
||||
});
|
||||
|
||||
const response = await POST(
|
||||
makeRequest(backup.id) as unknown as Parameters<typeof POST>[0],
|
||||
{ params: Promise.resolve({ id: backup.id }) },
|
||||
);
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
expect(await response.json()).toMatchObject({
|
||||
data: null,
|
||||
error: expect.any(String),
|
||||
});
|
||||
expect(
|
||||
await prisma.measurement.findMany({
|
||||
where: { userId: admin.id },
|
||||
select: { id: true },
|
||||
}),
|
||||
).toEqual([{ id: existing.id }]);
|
||||
expect(invalidateUserData).not.toHaveBeenCalled();
|
||||
});
|
||||
const response = await POST(
|
||||
makeRequest(backup.id) as unknown as Parameters<typeof POST>[0],
|
||||
{ params: Promise.resolve({ id: backup.id }) },
|
||||
);
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
expect(await response.json()).toMatchObject({
|
||||
data: null,
|
||||
error: expect.any(String),
|
||||
});
|
||||
expect(
|
||||
await prisma.measurement.findMany({
|
||||
where: { userId: admin.id },
|
||||
select: { id: true },
|
||||
}),
|
||||
).toEqual([{ id: existing.id }]);
|
||||
expect(invalidateUserData).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects metadata-only documents before mutation or invalidation", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
@@ -784,9 +794,9 @@ describe("canonical disaster-recovery backup round-trip", () => {
|
||||
select: { id: true },
|
||||
}),
|
||||
).toEqual([{ id: measurement.id }]);
|
||||
expect(
|
||||
await prisma.moodEntry.count({ where: { userId: admin.id } }),
|
||||
).toBe(0);
|
||||
expect(await prisma.moodEntry.count({ where: { userId: admin.id } })).toBe(
|
||||
0,
|
||||
);
|
||||
expect(invalidateUserData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -239,34 +239,43 @@ describe("POST /api/admin/backups/upload", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
["measurement type", (payload: UploadPayloadFixture) => {
|
||||
payload.measurements[0]!.type = "NOT_A_MEASUREMENT";
|
||||
}],
|
||||
["measurement source", (payload: UploadPayloadFixture) => {
|
||||
payload.measurements[0]!.source = "NOT_A_SOURCE";
|
||||
}],
|
||||
])("rejects an invalid %s with 422 and zero backup writes", async (_label, mutate) => {
|
||||
const prisma = getPrismaClient();
|
||||
const admin = await seedAdminSession();
|
||||
const payload = buildPayload(admin.id);
|
||||
mutate(payload);
|
||||
const file = new File([JSON.stringify(payload)], "invalid-enum.json", {
|
||||
type: "application/json",
|
||||
});
|
||||
[
|
||||
"measurement type",
|
||||
(payload: UploadPayloadFixture) => {
|
||||
payload.measurements[0]!.type = "NOT_A_MEASUREMENT";
|
||||
},
|
||||
],
|
||||
[
|
||||
"measurement source",
|
||||
(payload: UploadPayloadFixture) => {
|
||||
payload.measurements[0]!.source = "NOT_A_SOURCE";
|
||||
},
|
||||
],
|
||||
])(
|
||||
"rejects an invalid %s with 422 and zero backup writes",
|
||||
async (_label, mutate) => {
|
||||
const prisma = getPrismaClient();
|
||||
const admin = await seedAdminSession();
|
||||
const payload = buildPayload(admin.id);
|
||||
mutate(payload);
|
||||
const file = new File([JSON.stringify(payload)], "invalid-enum.json", {
|
||||
type: "application/json",
|
||||
});
|
||||
|
||||
const { POST } = await import("@/app/api/admin/backups/upload/route");
|
||||
const res = await POST(
|
||||
buildMultipart(file) as unknown as Parameters<typeof POST>[0],
|
||||
);
|
||||
const { POST } = await import("@/app/api/admin/backups/upload/route");
|
||||
const res = await POST(
|
||||
buildMultipart(file) as unknown as Parameters<typeof POST>[0],
|
||||
);
|
||||
|
||||
expect(res.status).toBe(422);
|
||||
expect(await res.json()).toMatchObject({
|
||||
data: null,
|
||||
error: "Backup payload failed schema validation",
|
||||
meta: { issues: expect.any(Array) },
|
||||
});
|
||||
expect(await prisma.dataBackup.count()).toBe(0);
|
||||
});
|
||||
expect(res.status).toBe(422);
|
||||
expect(await res.json()).toMatchObject({
|
||||
data: null,
|
||||
error: "Backup payload failed schema validation",
|
||||
meta: { issues: expect.any(Array) },
|
||||
});
|
||||
expect(await prisma.dataBackup.count()).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects an incompatible schemaVersion with 422", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
|
||||
@@ -175,7 +175,9 @@ describe("POST /api/devices", () => {
|
||||
const secondBody = (await second.json()) as { data: { id: string } };
|
||||
|
||||
expect(secondBody.data.id).toBe(firstBody.data.id);
|
||||
const devices = await prisma.device.findMany({ where: { userId: user.id } });
|
||||
const devices = await prisma.device.findMany({
|
||||
where: { userId: user.id },
|
||||
});
|
||||
expect(devices).toHaveLength(1);
|
||||
expect(devices[0]).toMatchObject({
|
||||
id: firstBody.data.id,
|
||||
@@ -214,7 +216,7 @@ describe("POST /api/devices", () => {
|
||||
liveActivityPushToken: "feedface".repeat(8),
|
||||
},
|
||||
});
|
||||
const legacyDevice = await prisma.device.create({
|
||||
await prisma.device.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
platform: "ios",
|
||||
@@ -252,7 +254,9 @@ describe("POST /api/devices", () => {
|
||||
const body = (await response.json()) as { data: { id: string } };
|
||||
expect(body.data.id).toBe(apnsDevice.id);
|
||||
|
||||
const devices = await prisma.device.findMany({ where: { userId: user.id } });
|
||||
const devices = await prisma.device.findMany({
|
||||
where: { userId: user.id },
|
||||
});
|
||||
expect(devices).toHaveLength(1);
|
||||
expect(devices[0]).toMatchObject({
|
||||
id: apnsDevice.id,
|
||||
|
||||
@@ -110,9 +110,9 @@ describe("external measurement identity reconciliation (real Postgres)", () => {
|
||||
status: "updated",
|
||||
row: { id: occupied.id, externalId: "sleep:new" },
|
||||
});
|
||||
expect(
|
||||
await prisma.measurement.count({ where: { userId: user.id } }),
|
||||
).toBe(1);
|
||||
expect(await prisma.measurement.count({ where: { userId: user.id } })).toBe(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("resurrects and re-keys a tombstone occupying the natural key", async () => {
|
||||
|
||||
@@ -347,7 +347,9 @@ describe("MoodEntry — external-id re-import idempotency (v1.12.1 / 0122)", ()
|
||||
mood: "OKAY",
|
||||
score: 3,
|
||||
});
|
||||
expect(await prisma.moodEntry.count({ where: { userId: user.id } })).toBe(1);
|
||||
expect(await prisma.moodEntry.count({ where: { userId: user.id } })).toBe(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("entries without an id still dedup on the legacy wall-clock key", async () => {
|
||||
|
||||
@@ -79,10 +79,7 @@ async function seedProviderUser(provider: "whoop" | "withings") {
|
||||
return userId;
|
||||
}
|
||||
|
||||
async function runContenders(
|
||||
provider: "whoop" | "withings",
|
||||
userId: string,
|
||||
) {
|
||||
async function runContenders(provider: "whoop" | "withings", userId: string) {
|
||||
const getToken =
|
||||
provider === "whoop" ? getValidWhoopToken : getValidWithingsToken;
|
||||
return Promise.all(
|
||||
@@ -98,62 +95,73 @@ beforeEach(async () => {
|
||||
describe.each([
|
||||
["whoop", whoopRefresh],
|
||||
["withings", withingsRefresh],
|
||||
] as const)("%s rotating token refresh serialization (real Postgres)", (provider, refresh) => {
|
||||
it("lets exactly one contender refresh a stale token and shares the winner", async () => {
|
||||
const userId = await seedProviderUser(provider);
|
||||
refresh.mockImplementation(async (refreshToken: string) => {
|
||||
expect(refreshToken).toBe("stale-refresh");
|
||||
return {
|
||||
access_token: "winner-access",
|
||||
refresh_token: "winner-refresh",
|
||||
expires_in: 3600,
|
||||
};
|
||||
});
|
||||
|
||||
const results = await runContenders(provider, userId);
|
||||
|
||||
expect(refresh).toHaveBeenCalledTimes(1);
|
||||
expect(results).toHaveLength(CONTENDERS);
|
||||
expect(results.every((result) => result?.accessToken === "winner-access")).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
const prisma = getPrismaClient();
|
||||
const row =
|
||||
provider === "whoop"
|
||||
? await prisma.whoopConnection.findUniqueOrThrow({ where: { userId } })
|
||||
: await prisma.withingsConnection.findUniqueOrThrow({ where: { userId } });
|
||||
expect(decrypt(row.accessToken)).toBe("winner-access");
|
||||
expect(decrypt(row.refreshToken)).toBe("winner-refresh");
|
||||
});
|
||||
|
||||
it("rolls back a failed winner so a later contender can retry", async () => {
|
||||
const userId = await seedProviderUser(provider);
|
||||
refresh
|
||||
.mockRejectedValueOnce(new Error("injected refresh failure"))
|
||||
.mockResolvedValue({
|
||||
access_token: "retry-access",
|
||||
refresh_token: "retry-refresh",
|
||||
expires_in: 3600,
|
||||
] as const)(
|
||||
"%s rotating token refresh serialization (real Postgres)",
|
||||
(provider, refresh) => {
|
||||
it("lets exactly one contender refresh a stale token and shares the winner", async () => {
|
||||
const userId = await seedProviderUser(provider);
|
||||
refresh.mockImplementation(async (refreshToken: string) => {
|
||||
expect(refreshToken).toBe("stale-refresh");
|
||||
return {
|
||||
access_token: "winner-access",
|
||||
refresh_token: "winner-refresh",
|
||||
expires_in: 3600,
|
||||
};
|
||||
});
|
||||
|
||||
const results = await runContenders(provider, userId);
|
||||
const results = await runContenders(provider, userId);
|
||||
|
||||
expect(refresh).toHaveBeenCalledTimes(2);
|
||||
expect(results.filter((result) => result === null)).toHaveLength(1);
|
||||
expect(
|
||||
results.filter((result) => result?.accessToken === "retry-access"),
|
||||
).toHaveLength(CONTENDERS - 1);
|
||||
expect(refresh).toHaveBeenCalledTimes(1);
|
||||
expect(results).toHaveLength(CONTENDERS);
|
||||
expect(
|
||||
results.every((result) => result?.accessToken === "winner-access"),
|
||||
).toBe(true);
|
||||
|
||||
const prisma = getPrismaClient();
|
||||
const row =
|
||||
provider === "whoop"
|
||||
? await prisma.whoopConnection.findUniqueOrThrow({ where: { userId } })
|
||||
: await prisma.withingsConnection.findUniqueOrThrow({ where: { userId } });
|
||||
expect(decrypt(row.accessToken)).toBe("retry-access");
|
||||
expect(decrypt(row.refreshToken)).toBe("retry-refresh");
|
||||
});
|
||||
});
|
||||
const prisma = getPrismaClient();
|
||||
const row =
|
||||
provider === "whoop"
|
||||
? await prisma.whoopConnection.findUniqueOrThrow({
|
||||
where: { userId },
|
||||
})
|
||||
: await prisma.withingsConnection.findUniqueOrThrow({
|
||||
where: { userId },
|
||||
});
|
||||
expect(decrypt(row.accessToken)).toBe("winner-access");
|
||||
expect(decrypt(row.refreshToken)).toBe("winner-refresh");
|
||||
});
|
||||
|
||||
it("rolls back a failed winner so a later contender can retry", async () => {
|
||||
const userId = await seedProviderUser(provider);
|
||||
refresh
|
||||
.mockRejectedValueOnce(new Error("injected refresh failure"))
|
||||
.mockResolvedValue({
|
||||
access_token: "retry-access",
|
||||
refresh_token: "retry-refresh",
|
||||
expires_in: 3600,
|
||||
});
|
||||
|
||||
const results = await runContenders(provider, userId);
|
||||
|
||||
expect(refresh).toHaveBeenCalledTimes(2);
|
||||
expect(results.filter((result) => result === null)).toHaveLength(1);
|
||||
expect(
|
||||
results.filter((result) => result?.accessToken === "retry-access"),
|
||||
).toHaveLength(CONTENDERS - 1);
|
||||
|
||||
const prisma = getPrismaClient();
|
||||
const row =
|
||||
provider === "whoop"
|
||||
? await prisma.whoopConnection.findUniqueOrThrow({
|
||||
where: { userId },
|
||||
})
|
||||
: await prisma.withingsConnection.findUniqueOrThrow({
|
||||
where: { userId },
|
||||
});
|
||||
expect(decrypt(row.accessToken)).toBe("retry-access");
|
||||
expect(decrypt(row.refreshToken)).toBe("retry-refresh");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe("WHOOP recovery cursor durability (real Postgres)", () => {
|
||||
const oldCursor = "2026-06-01T00:00:00.000Z";
|
||||
|
||||
Reference in new issue
Block a user