mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
test: cover managed profile Guardian races
- Exercise concurrent final-Guardian reductions against Postgres\n- Cover recoverable account and profile deletion paths
This commit is contained in:
1 parent
e5e3590ca3
commit
5059bc9b69
3 files changed
+174
No files matched your search
@@ -31,4 +31,25 @@ describe("managed-profile contract", () => {
|
||||
routes.some((entry) => entry.toLowerCase().includes("emancip")),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("uses the same record lock for every Guardian-reducing transition", async () => {
|
||||
const lifecycle = await readFile(
|
||||
path.join(root, "src/lib/managed-profiles/lifecycle.ts"),
|
||||
"utf8",
|
||||
);
|
||||
const grants = await readFile(
|
||||
path.join(root, "src/lib/sharing/grants.ts"),
|
||||
"utf8",
|
||||
);
|
||||
const accountDeletion = await readFile(
|
||||
path.join(root, "src/app/api/settings/account/route.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(lifecycle).toContain("pg_advisory_xact_lock");
|
||||
expect(lifecycle).toContain("clearManagedProfileMarker");
|
||||
expect(lifecycle).toContain("deleteManagedProfile");
|
||||
expect(grants).toContain("reduceManagedProfileGuardian");
|
||||
expect(accountDeletion).toContain("deleteGuardianAccountWithLifecycle");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { createManagedProfile } from "@/lib/managed-profiles/create";
|
||||
import {
|
||||
acceptGrant,
|
||||
inviteGrant,
|
||||
renounceGrant,
|
||||
revokeGrant,
|
||||
} from "@/lib/sharing/grants";
|
||||
|
||||
import { getPrismaClient, truncateAllTables } from "./setup";
|
||||
|
||||
let sequence = 0;
|
||||
|
||||
async function makeAdult(label: string) {
|
||||
const suffix = sequence++;
|
||||
return getPrismaClient().user.create({
|
||||
data: {
|
||||
username: `${label}-${suffix}`,
|
||||
email: `${label}-${suffix}@example.test`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function twoGuardians() {
|
||||
const creator = await makeAdult("creator");
|
||||
const secondGuardian = await makeAdult("second-guardian");
|
||||
const { profile, creatorGrant } = await createManagedProfile({
|
||||
creatorId: creator.id,
|
||||
displayName: "Managed profile",
|
||||
dateOfBirth: null,
|
||||
locale: "en",
|
||||
timezone: "UTC",
|
||||
});
|
||||
const invitation = await inviteGrant({
|
||||
grantorId: profile.id,
|
||||
granteeId: secondGuardian.id,
|
||||
access: "MANAGE",
|
||||
scope: null,
|
||||
});
|
||||
const secondGrant = await acceptGrant({
|
||||
grantId: invitation.id,
|
||||
granteeId: secondGuardian.id,
|
||||
});
|
||||
return { creator, secondGuardian, profile, creatorGrant, secondGrant };
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await truncateAllTables(getPrismaClient());
|
||||
});
|
||||
|
||||
describe("managed profile Guardian concurrency (real Postgres)", () => {
|
||||
it("allows only one concurrent reduction of the final two Guardians", async () => {
|
||||
const { creatorGrant, secondGuardian, secondGrant, profile } =
|
||||
await twoGuardians();
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
revokeGrant({ grantId: creatorGrant.id, grantorId: profile.id }),
|
||||
renounceGrant({
|
||||
grantId: secondGrant.id,
|
||||
granteeId: secondGuardian.id,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(
|
||||
results.filter((result) => result.status === "fulfilled"),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
results.filter((result) => result.status === "rejected"),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
await getPrismaClient().accountGrant.count({
|
||||
where: {
|
||||
grantorId: profile.id,
|
||||
access: "MANAGE",
|
||||
acceptedAt: { not: null },
|
||||
revokedAt: null,
|
||||
},
|
||||
}),
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,11 @@ import {
|
||||
findActiveGrant,
|
||||
inviteGrant,
|
||||
} from "@/lib/sharing/grants";
|
||||
import {
|
||||
LastManagedGuardianError,
|
||||
clearManagedProfileMarker,
|
||||
deleteManagedProfile,
|
||||
} from "@/lib/managed-profiles/lifecycle";
|
||||
|
||||
vi.mock("next/headers", async () => {
|
||||
const { cookieJar, headerJar } = await import("./mock-next-headers");
|
||||
@@ -210,4 +215,70 @@ describe("managed profile lifecycle (real Postgres)", () => {
|
||||
),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it("refuses to delete a Guardian account when it would leave a managed profile behind", async () => {
|
||||
const guardian = await signInWithFreshMfa();
|
||||
const { profile } = await createManagedProfile({
|
||||
creatorId: guardian.id,
|
||||
displayName: "Managed profile",
|
||||
dateOfBirth: null,
|
||||
locale: "en",
|
||||
timezone: "UTC",
|
||||
});
|
||||
const { DELETE } = await import("@/app/api/settings/account/route");
|
||||
|
||||
const response = await DELETE(
|
||||
new NextRequest("http://localhost/api/settings/account", {
|
||||
method: "DELETE",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ confirm: "DELETE_ACCOUNT" }),
|
||||
}),
|
||||
);
|
||||
expect(response.status).toBe(409);
|
||||
expect(
|
||||
await getPrismaClient().user.findUnique({ where: { id: guardian.id } }),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
await getPrismaClient().user.findUnique({ where: { id: profile.id } }),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it("deletes a managed profile only through its fresh-MFA Guardian path", async () => {
|
||||
const guardian = await signInWithFreshMfa();
|
||||
const { profile } = await createManagedProfile({
|
||||
creatorId: guardian.id,
|
||||
displayName: "Managed profile",
|
||||
dateOfBirth: null,
|
||||
locale: "en",
|
||||
timezone: "UTC",
|
||||
});
|
||||
|
||||
await deleteManagedProfile({
|
||||
profileId: profile.id,
|
||||
guardianId: guardian.id,
|
||||
});
|
||||
expect(
|
||||
await getPrismaClient().user.findUnique({ where: { id: profile.id } }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps marker clearing internal while stopping managed behavior", async () => {
|
||||
const guardian = await signInWithFreshMfa();
|
||||
const { profile } = await createManagedProfile({
|
||||
creatorId: guardian.id,
|
||||
displayName: "Managed profile",
|
||||
dateOfBirth: null,
|
||||
locale: "en",
|
||||
timezone: "UTC",
|
||||
});
|
||||
|
||||
await clearManagedProfileMarker({ profileId: profile.id });
|
||||
const cleared = await getPrismaClient().user.findUniqueOrThrow({
|
||||
where: { id: profile.id },
|
||||
});
|
||||
expect(cleared.managedProfileAt).toBeNull();
|
||||
await expect(
|
||||
clearManagedProfileMarker({ profileId: "missing-profile" }),
|
||||
).rejects.toBeInstanceOf(LastManagedGuardianError);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user