mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
Merge branch 'fix/v1437-data' into release/v1.43.7
This commit is contained in:
commit
dee6a27ae4
100 files changed
+2314
-506
No files matched your search
@@ -377,8 +377,9 @@ describe("PUT /api/dashboard/widgets — Save persists full layouts", () => {
|
||||
// v1.4.42 W2 — the route used to return only `issues[0].message`,
|
||||
// which forced one round-trip per wrong field during iOS contract
|
||||
// debugging. The route now returns every issue under `details.issues`
|
||||
// AND writes a `dashboard.widgets.validation-failed` row to the
|
||||
// append-only audit ledger so the operator can grep `/api/admin/audit`.
|
||||
// and keeps the refusal on the wide event (`meta.rejected_attempt`);
|
||||
// since v1.43.7 a refusal on the caller's own record no longer writes an
|
||||
// audit row.
|
||||
describe("PUT 422 — multi-issue envelope (v1.4.42 W2)", () => {
|
||||
it("surfaces TWO simultaneous validation errors", async () => {
|
||||
const { userId } = await seedUser();
|
||||
@@ -406,25 +407,15 @@ describe("PUT /api/dashboard/widgets — Save persists full layouts", () => {
|
||||
const paths = body.details.issues.map((i) => i.path).sort();
|
||||
expect(paths).toEqual(["version", "widgets"]);
|
||||
|
||||
// Audit-ledger breadcrumb. The route emits the row fire-and-forget,
|
||||
// so poll until it materialises instead of asserting after one fixed
|
||||
// tick — under container load the detached insert can land well past
|
||||
// 25 ms, which made this assertion flaky. Bounded at ~2 s.
|
||||
const deadline = Date.now() + 2_000;
|
||||
let audit: { details: string | null } | null = null;
|
||||
while (audit === null && Date.now() < deadline) {
|
||||
audit = await getPrismaClient().auditLog.findFirst({
|
||||
where: { userId, action: "dashboard.widgets.validation-failed" },
|
||||
});
|
||||
if (audit === null) {
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
}
|
||||
expect(audit).not.toBeNull();
|
||||
const details = JSON.parse(audit?.details ?? "{}") as {
|
||||
issues: Array<{ path: string }>;
|
||||
};
|
||||
expect(details.issues.length).toBe(2);
|
||||
// A refusal on the caller's own record is telemetry, not an audit
|
||||
// event: it stays on the wide event and never reaches the ledger
|
||||
// (only a delegate's refusal is filed, for the owner's feed). Give a
|
||||
// detached insert time to land before asserting it did not.
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
const audit = await getPrismaClient().auditLog.findFirst({
|
||||
where: { userId, action: "dashboard.widgets.validation-failed" },
|
||||
});
|
||||
expect(audit).toBeNull();
|
||||
});
|
||||
|
||||
it("surfaces THREE simultaneous validation errors", async () => {
|
||||
|
||||
@@ -29,6 +29,10 @@ import {
|
||||
enqueueBootTimeFreeTextEncryptionBackfill,
|
||||
runFreeTextEncryptionBackfillForUser,
|
||||
} from "@/lib/jobs/free-text-encryption-backfill";
|
||||
import {
|
||||
insightsCacheWrite,
|
||||
openInsightsCachedText,
|
||||
} from "@/lib/insights/cached-text-cipher";
|
||||
import { getPrismaClient, truncateAllTables } from "./setup";
|
||||
|
||||
function makeCapturingBoss() {
|
||||
@@ -135,6 +139,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
|
||||
routeGeometriesMigrated: 0,
|
||||
appointmentAddressesCleared: 0,
|
||||
contactAuditRowsScrubbed: 0,
|
||||
insightsCachesMigrated: 0,
|
||||
});
|
||||
expect(await runFreeTextEncryptionBackfillForUser("ft-b")).toEqual({
|
||||
conversationTitlesMigrated: 0,
|
||||
@@ -143,6 +148,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
|
||||
routeGeometriesMigrated: 0,
|
||||
appointmentAddressesCleared: 0,
|
||||
contactAuditRowsScrubbed: 0,
|
||||
insightsCachesMigrated: 0,
|
||||
});
|
||||
|
||||
const conversations = await prisma.coachConversation.findMany({
|
||||
@@ -191,6 +197,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
|
||||
routeGeometriesMigrated: 0,
|
||||
appointmentAddressesCleared: 0,
|
||||
contactAuditRowsScrubbed: 0,
|
||||
insightsCachesMigrated: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -275,6 +282,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
|
||||
routeGeometriesMigrated: 0,
|
||||
appointmentAddressesCleared: 0,
|
||||
contactAuditRowsScrubbed: 0,
|
||||
insightsCachesMigrated: 0,
|
||||
});
|
||||
expect(await runFreeTextEncryptionBackfillForUser("pc-b")).toEqual({
|
||||
conversationTitlesMigrated: 0,
|
||||
@@ -283,6 +291,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
|
||||
routeGeometriesMigrated: 1,
|
||||
appointmentAddressesCleared: 0,
|
||||
contactAuditRowsScrubbed: 0,
|
||||
insightsCachesMigrated: 0,
|
||||
});
|
||||
|
||||
const practitioners = await prisma.practitioner.findMany({
|
||||
@@ -343,6 +352,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
|
||||
routeGeometriesMigrated: 0,
|
||||
appointmentAddressesCleared: 0,
|
||||
contactAuditRowsScrubbed: 0,
|
||||
insightsCachesMigrated: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -440,6 +450,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
|
||||
expect(summaryA).toMatchObject({
|
||||
appointmentAddressesCleared: 1,
|
||||
contactAuditRowsScrubbed: 1,
|
||||
insightsCachesMigrated: 0,
|
||||
});
|
||||
await runFreeTextEncryptionBackfillForUser("ad-b");
|
||||
|
||||
@@ -487,6 +498,75 @@ describe("free-text encryption backfill (real Postgres)", () => {
|
||||
expect(second.sent).toEqual([]);
|
||||
});
|
||||
|
||||
it("seals the cached insights payload, keeps updatedAt, and converges", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
await Promise.all([seedUser("ic-a"), seedUser("ic-b"), seedUser("ic-c")]);
|
||||
const payload = JSON.stringify({
|
||||
dailyBriefing: { paragraph: "Pressure eased this week." },
|
||||
});
|
||||
// ic-a: written by the previous release, in the clear.
|
||||
await prisma.user.update({
|
||||
where: { id: "ic-a" },
|
||||
data: { insightsCachedText: payload },
|
||||
});
|
||||
// ic-b: written the current way.
|
||||
await prisma.user.update({
|
||||
where: { id: "ic-b" },
|
||||
data: insightsCacheWrite("current"),
|
||||
});
|
||||
const before = await prisma.user.findUniqueOrThrow({
|
||||
where: { id: "ic-a" },
|
||||
select: { updatedAt: true },
|
||||
});
|
||||
|
||||
const { boss, sent } = makeCapturingBoss();
|
||||
setGlobalBoss(boss as never);
|
||||
const discovered = await enqueueBootTimeFreeTextEncryptionBackfill();
|
||||
expect(discovered.error).toBeNull();
|
||||
expect(sent.map((s) => s.userId)).toEqual(["ic-a"]);
|
||||
|
||||
const summary = await runFreeTextEncryptionBackfillForUser("ic-a");
|
||||
expect(summary.insightsCachesMigrated).toBe(1);
|
||||
|
||||
const rowA = await prisma.user.findUniqueOrThrow({
|
||||
where: { id: "ic-a" },
|
||||
select: {
|
||||
insightsCachedText: true,
|
||||
insightsCachedTextEncrypted: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
expect(rowA.insightsCachedText).toBeNull();
|
||||
expect(rowA.insightsCachedTextEncrypted).not.toBeNull();
|
||||
expect(openInsightsCachedText(rowA)).toBe(payload);
|
||||
expect(rowA.updatedAt).toEqual(before.updatedAt);
|
||||
|
||||
const rowB = await prisma.user.findUniqueOrThrow({
|
||||
where: { id: "ic-b" },
|
||||
select: { insightsCachedText: true, insightsCachedTextEncrypted: true },
|
||||
});
|
||||
expect(openInsightsCachedText(rowB)).toBe("current");
|
||||
|
||||
const again = await runFreeTextEncryptionBackfillForUser("ic-a");
|
||||
expect(again.insightsCachesMigrated).toBe(0);
|
||||
const second = makeCapturingBoss();
|
||||
setGlobalBoss(second.boss as never);
|
||||
await enqueueBootTimeFreeTextEncryptionBackfill();
|
||||
expect(second.sent).toEqual([]);
|
||||
});
|
||||
|
||||
it("has the partial index migration 0411 adds", async () => {
|
||||
const indexes = await getPrismaClient().$queryRaw<
|
||||
{ indexname: string; indexdef: string }[]
|
||||
>`
|
||||
SELECT indexname, indexdef FROM pg_indexes
|
||||
WHERE indexname = 'users_insights_cached_text_backfill_idx'`;
|
||||
expect(indexes).toHaveLength(1);
|
||||
expect(indexes[0].indexdef).toContain(
|
||||
"WHERE (insights_cached_text IS NOT NULL)",
|
||||
);
|
||||
});
|
||||
|
||||
it("has the partial indexes migration 0361 adds", async () => {
|
||||
const indexes = await getPrismaClient().$queryRaw<
|
||||
{ indexname: string; indexdef: string }[]
|
||||
|
||||
Reference in new issue
Block a user