mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
fix: retain import actor attribution
Persist record and actor identities through job completion.\nWrite one bounded transactional completion audit per job.
This commit is contained in:
1 parent
adb932052c
commit
620817b10b
19 files changed
+211
-63
No files matched your search
@@ -98,7 +98,8 @@ async function enqueue(userId: string) {
|
||||
);
|
||||
const job = await prisma.medicationIntakeImportJob.create({
|
||||
data: {
|
||||
userId,
|
||||
recordUserId: userId,
|
||||
actorUserId: userId,
|
||||
// Account-wide: the file covers sixteen medications, so no single one owns
|
||||
// the job and each entry names its own.
|
||||
medicationId: null,
|
||||
|
||||
@@ -92,7 +92,8 @@ describe("medication intake import job — concurrent retry", () => {
|
||||
};
|
||||
const job = await prisma.medicationIntakeImportJob.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
recordUserId: user.id,
|
||||
actorUserId: user.id,
|
||||
medicationId: medication.id,
|
||||
payload: toJson(payload),
|
||||
progress: toJson(progress),
|
||||
@@ -218,7 +219,8 @@ describe("medication intake import job — concurrent retry", () => {
|
||||
};
|
||||
const job = await prisma.medicationIntakeImportJob.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
recordUserId: user.id,
|
||||
actorUserId: user.id,
|
||||
medicationId: medication.id,
|
||||
payload: toJson(payload),
|
||||
progress: toJson(progress),
|
||||
|
||||
@@ -93,7 +93,8 @@ describe("medication intake import — the reported 28-dose export", () => {
|
||||
|
||||
const job = await prisma.medicationIntakeImportJob.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
recordUserId: user.id,
|
||||
actorUserId: user.id,
|
||||
medicationId: medication.id,
|
||||
payload: toJson(payload),
|
||||
progress: toJson({
|
||||
@@ -140,7 +141,8 @@ describe("medication intake import — the reported 28-dose export", () => {
|
||||
|
||||
const first = await prisma.medicationIntakeImportJob.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
recordUserId: user.id,
|
||||
actorUserId: user.id,
|
||||
medicationId: medication.id,
|
||||
payload: toJson(payload),
|
||||
progress: toJson(progress),
|
||||
@@ -150,7 +152,8 @@ describe("medication intake import — the reported 28-dose export", () => {
|
||||
|
||||
const second = await prisma.medicationIntakeImportJob.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
recordUserId: user.id,
|
||||
actorUserId: user.id,
|
||||
medicationId: medication.id,
|
||||
payload: toJson(payload),
|
||||
progress: toJson(progress),
|
||||
@@ -211,7 +214,8 @@ describe("medication intake import — the reported 28-dose export", () => {
|
||||
|
||||
const job = await prisma.medicationIntakeImportJob.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
recordUserId: user.id,
|
||||
actorUserId: user.id,
|
||||
medicationId: medication.id,
|
||||
payload: toJson({ entries }),
|
||||
progress: toJson({
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { GET as getImportStatus } from "@/app/api/medications/[id]/intake/import/[jobId]/status/route";
|
||||
import { ACCOUNT_SELECTOR_HEADER } from "@/lib/api-handler";
|
||||
import { hashToken } from "@/lib/auth/hmac";
|
||||
import { toJson } from "@/lib/db";
|
||||
import {
|
||||
_setMedicationIntakeImportPrismaForTests,
|
||||
@@ -8,6 +12,22 @@ import {
|
||||
} from "@/lib/jobs/medication-intake-import";
|
||||
|
||||
import { getPrismaClient, truncateAllTables } from "./setup";
|
||||
import { cookieJar, headerJar } from "./mock-next-headers";
|
||||
|
||||
vi.mock("next/headers", async () => {
|
||||
const { cookieJar, headerJar } = await import("./mock-next-headers");
|
||||
return {
|
||||
headers: vi.fn(async () => ({
|
||||
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
|
||||
})),
|
||||
cookies: vi.fn(async () => ({
|
||||
get: (name: string) => {
|
||||
const value = cookieJar.get(name);
|
||||
return value ? { name, value } : undefined;
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const EMPTY_PROGRESS: MedicationImportProgress = {
|
||||
processed: 0,
|
||||
@@ -22,6 +42,8 @@ beforeEach(async () => {
|
||||
const prisma = getPrismaClient();
|
||||
_setMedicationIntakeImportPrismaForTests(prisma);
|
||||
await truncateAllTables(prisma);
|
||||
cookieJar.clear();
|
||||
headerJar.clear();
|
||||
});
|
||||
|
||||
describe("shared intake import attribution", () => {
|
||||
@@ -87,4 +109,72 @@ describe("shared intake import attribution", () => {
|
||||
total: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("lets the initiating manager poll the record-scoped job", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
await prisma.user.createMany({
|
||||
data: [
|
||||
{
|
||||
id: "status-record",
|
||||
username: "status-record",
|
||||
email: "status-record@example.test",
|
||||
role: "USER",
|
||||
},
|
||||
{
|
||||
id: "status-manager",
|
||||
username: "status-manager",
|
||||
email: "status-manager@example.test",
|
||||
role: "USER",
|
||||
},
|
||||
],
|
||||
});
|
||||
const medication = await prisma.medication.create({
|
||||
data: {
|
||||
userId: "status-record",
|
||||
name: "Status medication",
|
||||
dose: "1 unit",
|
||||
},
|
||||
});
|
||||
await prisma.accountGrant.create({
|
||||
data: {
|
||||
id: "status-manage-grant",
|
||||
grantorId: "status-record",
|
||||
granteeId: "status-manager",
|
||||
access: "MANAGE",
|
||||
acceptedAt: new Date(),
|
||||
},
|
||||
});
|
||||
const token = "shared-import-status-token".padEnd(28, "0");
|
||||
await prisma.apiToken.create({
|
||||
data: {
|
||||
userId: "status-manager",
|
||||
name: "shared import status",
|
||||
tokenHash: hashToken(token),
|
||||
permissions: ["*"],
|
||||
},
|
||||
});
|
||||
headerJar.set("authorization", `Bearer ${token}`);
|
||||
headerJar.set(ACCOUNT_SELECTOR_HEADER, "status-record");
|
||||
const job = await prisma.medicationIntakeImportJob.create({
|
||||
data: {
|
||||
recordUserId: "status-record",
|
||||
actorUserId: "status-manager",
|
||||
medicationId: medication.id,
|
||||
payload: toJson({ entries: [] }),
|
||||
progress: toJson(EMPTY_PROGRESS),
|
||||
},
|
||||
});
|
||||
|
||||
const response = await getImportStatus(
|
||||
new NextRequest(
|
||||
`http://localhost/api/medications/${medication.id}/intake/import/${job.id}/status`,
|
||||
{
|
||||
},
|
||||
),
|
||||
{ params: Promise.resolve({ id: medication.id, jobId: job.id }) },
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect((await response.json()).data.jobId).toBe(job.id);
|
||||
});
|
||||
});
|
||||
@@ -1603,7 +1603,7 @@ proveManageRoute("POST /api/medications/[id]/intake/import", {
|
||||
auditAction: "medication.intake.import.kickoff",
|
||||
applied: async ({ ownerId }) =>
|
||||
(await getPrismaClient().medicationIntakeImportJob.count({
|
||||
where: { userId: ownerId },
|
||||
where: { recordUserId: ownerId },
|
||||
})) === 1,
|
||||
});
|
||||
|
||||
@@ -1637,7 +1637,7 @@ manageContract("POST /api/medications/intake/dose-history-import", {
|
||||
auditAction: "medication.intake.history.import.kickoff",
|
||||
applied: async ({ ownerId }) =>
|
||||
(await getPrismaClient().medicationIntakeImportJob.count({
|
||||
where: { userId: ownerId },
|
||||
where: { recordUserId: ownerId },
|
||||
})) === 1,
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user