Files
HealthLog/tests/integration/managed-profile-surface.test.ts
Marc-André Bombeck b6da360856 refactor(db): drop the runtime schema mirror
ensureDbCompatibility ran ALTER TABLE / CREATE TABLE IF NOT EXISTS on the
login, register, token and session paths, mirroring columns every
migration under prisma/migrations already creates. The entrypoint applies
those migrations before the server listens and stops the boot when one
fails, so the mirror never had work to do, and DDL on a request path
needs the table's owner: a connection that may only read and write rows
fails every login on it. Drop the module, its call sites and the mocks
that stubbed it out.
2026-11-03 20:32:35 +01:00

994 lines
37 KiB
TypeScript

/**
* Every mutation and read the managed-profile surface can issue, through the
* real routes and a real Postgres.
*
* The browser card is proved from a static render in
* `src/components/settings/access/__tests__/managed-profile-affordances.test.tsx`,
* which can say that a control exists and nothing about what happens when it is
* pressed. This file is the other half: the same calls the card makes, against
* the handlers themselves, including every refusal the card renders a sentence
* for.
*
* The roster read is where most of the weight sits, because it is the one
* capability this release adds to the server. Its field set is asserted with
* `Object.keys` rather than by presence: an over-disclosing route passes every
* "contains" assertion ever written about it, and the decision that the roster
* publishes no e-mail address is a privacy decision rather than plumbing.
*/
import { NextRequest } from "next/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { cookieJar, headerJar, queuedSessionIds } from "./mock-next-headers";
import { getPrismaClient, switchSessionTo, truncateAllTables } from "./setup";
import { createManagedProfile } from "@/lib/managed-profiles/create";
import { acceptGrant, inviteGrant } from "@/lib/sharing/grants";
import { bodyWithoutRequestId } from "@/__tests__/helpers/error-envelope";
process.env.API_TOKEN_HMAC_KEY ??=
"test-hmac-key-managed-profile-surface-32-bytes-min-1234509876";
const { hashToken } = await import("@/lib/auth/hmac");
vi.mock("next/headers", async () => {
const { cookieJar, headerJar, queuedSessionIds } =
await import("./mock-next-headers");
return {
headers: vi.fn(async () => ({
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
})),
cookies: vi.fn(async () => {
const snapshot = new Map(cookieJar);
const queuedSessionId = queuedSessionIds.shift();
if (queuedSessionId) {
snapshot.set("healthlog_session", queuedSessionId);
}
return {
get: (name: string) => {
const value = snapshot.get(name);
return value ? { name, value } : undefined;
},
set: (name: string, value: string) => {
cookieJar.set(name, value);
},
delete: (name: string) => {
cookieJar.delete(name);
},
};
}),
};
});
let sequence = 0;
interface Person {
id: string;
username: string;
sessionId: string;
}
/** An account with a second factor and a session that has just proved it. */
async function person(label: string): Promise<Person> {
const suffix = sequence++;
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: {
username: `${label}-${suffix}`,
email: `${label}-${suffix}@example.test`,
totpConfirmedAt: new Date(),
},
});
const session = await prisma.session.create({
data: {
userId: user.id,
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
mfaVerifiedAt: new Date(),
},
});
return { id: user.id, username: user.username, sessionId: session.id };
}
function signIn(who: Person): void {
headerJar.delete("authorization");
cookieJar.set("healthlog_session", who.sessionId);
}
/** A wildcard Bearer token for `who`, with no cookie session armed. */
async function signInWithBearer(who: Person): Promise<void> {
const raw = `hlk_surface_${"0".repeat(48)}`;
await getPrismaClient().apiToken.create({
data: {
userId: who.id,
name: "surface",
tokenHash: hashToken(raw),
permissions: ["*"],
},
});
cookieJar.clear();
headerJar.set("authorization", `Bearer ${raw}`);
}
async function listGuardians(profileId: string) {
const { GET } =
await import("@/app/api/managed-profiles/[id]/guardians/route");
return GET(
new NextRequest(
`http://localhost/api/managed-profiles/${profileId}/guardians`,
{ method: "GET" },
),
{ params: Promise.resolve({ id: profileId }) },
);
}
/** `POST /api/managed-profiles`, with the body the card composes. */
async function createProfile(body: unknown) {
const { POST } = await import("@/app/api/managed-profiles/route");
return POST(
new NextRequest("http://localhost/api/managed-profiles", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
}),
);
}
async function deleteProfile(profileId: string) {
const { DELETE } = await import("@/app/api/managed-profiles/[id]/route");
return DELETE(
new NextRequest(`http://localhost/api/managed-profiles/${profileId}`, {
method: "DELETE",
}),
{ params: Promise.resolve({ id: profileId }) },
);
}
async function inviteGuardian(
profileId: string,
body: { identifier: string; expiresAt?: string | null },
) {
const { POST } =
await import("@/app/api/managed-profiles/[id]/guardians/route");
return POST(
new NextRequest(
`http://localhost/api/managed-profiles/${profileId}/guardians`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
},
),
{ params: Promise.resolve({ id: profileId }) },
);
}
async function removeGuardian(profileId: string, grantId: string) {
const { DELETE } =
await import("@/app/api/managed-profiles/[id]/guardians/[grantId]/route");
return DELETE(
new NextRequest(
`http://localhost/api/managed-profiles/${profileId}/guardians/${grantId}`,
{ method: "DELETE" },
),
{ params: Promise.resolve({ id: profileId, grantId }) },
);
}
async function errorCodeOf(response: Response): Promise<unknown> {
return (await response.json()).meta?.errorCode;
}
/** A profile with its creator as sole Guardian, plus a pending second one. */
async function profileWithPendingSecondGuardian() {
const creator = await person("creator");
const invitee = await person("invitee");
const { profile } = await createManagedProfile({
creatorId: creator.id,
displayName: "Managed record",
dateOfBirth: null,
locale: "en",
timezone: "UTC",
});
const invitation = await inviteGrant({
grantorId: profile.id,
granteeId: invitee.id,
access: "MANAGE",
scope: null,
});
return { creator, invitee, profile, invitation };
}
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
cookieJar.clear();
headerJar.clear();
queuedSessionIds.length = 0;
});
describe("who looks after a managed profile (real Postgres)", () => {
it("shows an active Guardian every live grant, pending and accepted", async () => {
const { creator, invitee, profile } =
await profileWithPendingSecondGuardian();
signIn(creator);
const response = await listGuardians(profile.id);
expect(response.status).toBe(200);
const roster = (await response.json()).data as Array<
Record<string, unknown>
>;
expect(roster).toHaveLength(2);
const byName = new Map(
roster.map((row) => [
(row.account as { username: string }).username,
row,
]),
);
expect(byName.get(creator.username)?.state).toBe("ACTIVE");
expect(byName.get(creator.username)?.acceptedAt).toEqual(
expect.any(String),
);
expect(byName.get(invitee.username)?.state).toBe("PENDING");
expect(byName.get(invitee.username)?.acceptedAt).toBeNull();
});
it("discloses exactly five fields, and the party block carries no e-mail", async () => {
// `Object.keys`, not `toContain`. A route that also published the
// invitee's e-mail address, their notification channels or their last
// access would satisfy every presence assertion in this file.
const { profile, creator } = await profileWithPendingSecondGuardian();
signIn(creator);
const roster = (await (await listGuardians(profile.id)).json())
.data as Array<Record<string, unknown>>;
expect(roster.length).toBeGreaterThan(0);
for (const row of roster) {
expect(Object.keys(row).sort()).toEqual([
"acceptedAt",
"account",
"grantId",
"invitedAt",
"state",
]);
expect(Object.keys(row.account as object).sort()).toEqual([
"displayName",
"id",
"username",
]);
}
});
it("answers a second Guardian with the same roster", async () => {
const { creator, invitee, profile, invitation } =
await profileWithPendingSecondGuardian();
await acceptGrant({ grantId: invitation.id, granteeId: invitee.id });
signIn(creator);
const first = (await (await listGuardians(profile.id)).json()).data;
signIn(invitee);
const second = (await (await listGuardians(profile.id)).json()).data;
expect(second).toEqual(first);
expect(
(second as Array<{ state: string }>).every((r) => r.state === "ACTIVE"),
).toBe(true);
});
it("refuses somebody who is not a Guardian of that profile", async () => {
const { profile } = await profileWithPendingSecondGuardian();
const stranger = await person("stranger");
signIn(stranger);
const response = await listGuardians(profile.id);
expect(response.status).toBe(404);
expect((await response.json()).meta?.errorCode).toBe(
"managed_profile.not_found",
);
});
it("answers an unknown id with the same bytes as a refused one", async () => {
// The refusal must not be an enumeration oracle: "no such profile" and
// "not yours" have to be indistinguishable from the outside.
const { profile } = await profileWithPendingSecondGuardian();
const stranger = await person("stranger");
signIn(stranger);
// `meta.requestId` is a fresh id per request and says nothing about the
// profile; every other byte has to match.
const refusedRes = await listGuardians(profile.id);
const unknownRes = await listGuardians("does-not-exist");
expect(unknownRes.status).toBe(refusedRes.status);
const refused = await refusedRes.clone().json();
const unknown = await unknownRes.clone().json();
expect(typeof refused.meta?.requestId).toBe("string");
expect(typeof unknown.meta?.requestId).toBe("string");
expect(await bodyWithoutRequestId(unknownRes)).toBe(
await bodyWithoutRequestId(refusedRes),
);
});
it.each(["READ", "WRITE", "MANAGE"] as const)(
"refuses a %s delegate of an ordinary record against that record",
async (access) => {
// Holding a grant is not holding this read. An adult record whose owner
// granted MANAGE is not a managed profile, and must 404 exactly as an
// unknown id does.
const owner = await person("owner");
const delegate = await person("delegate");
const invitation = await inviteGrant({
grantorId: owner.id,
granteeId: delegate.id,
access,
scope: null,
});
await acceptGrant({ grantId: invitation.id, granteeId: delegate.id });
signIn(delegate);
const response = await listGuardians(owner.id);
expect(response.status).toBe(404);
expect((await response.json()).meta?.errorCode).toBe(
"managed_profile.not_found",
);
},
);
it("drops a revoked Guardian from the roster and refuses them the read", async () => {
const { creator, invitee, profile, invitation } =
await profileWithPendingSecondGuardian();
await acceptGrant({ grantId: invitation.id, granteeId: invitee.id });
await getPrismaClient().accountGrant.update({
where: { id: invitation.id },
data: { revokedAt: new Date(), revokedBy: "GRANTOR" },
});
signIn(invitee);
expect((await listGuardians(profile.id)).status).toBe(404);
signIn(creator);
const roster = (await (await listGuardians(profile.id)).json())
.data as Array<{ account: { username: string } }>;
expect(roster).toHaveLength(1);
expect(roster[0].account.username).toBe(creator.username);
});
it("drops an expired Guardian grant from the roster", async () => {
const { creator, profile, invitation } =
await profileWithPendingSecondGuardian();
await getPrismaClient().accountGrant.update({
where: { id: invitation.id },
data: { expiresAt: new Date(Date.now() - 60_000) },
});
signIn(creator);
const roster = (await (await listGuardians(profile.id)).json())
.data as Array<{ account: { username: string } }>;
expect(roster).toHaveLength(1);
expect(roster[0].account.username).toBe(creator.username);
});
it("refuses a Bearer caller, because the family is cookie-only", async () => {
const { creator, profile } = await profileWithPendingSecondGuardian();
await signInWithBearer(creator);
// The same account, the same wildcard reach it has everywhere else, and
// still no answer here: `requireCookieAuth` never falls through to the
// Bearer branch.
expect((await listGuardians(profile.id)).status).toBe(401);
});
});
describe("every call the managed-profile card can make", () => {
it("cannot be reached with a token, however wide its permissions", async () => {
// The native half of the contract, and the reason the freeze can promise
// it: `requireFreshMfa` resolves the session cookie and never falls
// through to the Bearer branch, so a wildcard token — the widest a client
// can hold — cannot mint a credential-less person and a permanent
// management relationship. The positive control is the very next case:
// the same body, from a cookie session that has proved its factor,
// creates the record.
const creator = await person("creator");
await signInWithBearer(creator);
const response = await createProfile({
displayName: "Managed record",
dateOfBirth: null,
locale: "en",
timezone: "UTC",
});
expect(response.status).toBe(401);
expect(
await getPrismaClient().user.count({
where: { managedProfileAt: { not: null } },
}),
).toBe(0);
});
it("creates a profile from exactly the body the form composes", async () => {
const creator = await person("creator");
signIn(creator);
const response = await createProfile({
displayName: "Managed record",
dateOfBirth: null,
locale: "de",
timezone: "Europe/Zurich",
});
expect(response.status).toBe(201);
const created = (await response.json()).data;
expect(created.recordKind).toBe("managed");
// The creator is its Guardian, accepted, from the same transaction — which
// is why the card can read the profile off the account payload rather than
// from a list endpoint that does not exist.
signIn(creator);
const roster = (await (await listGuardians(created.id)).json())
.data as Array<{ state: string; account: { id: string } }>;
expect(roster).toEqual([
expect.objectContaining({
state: "ACTIVE",
account: expect.objectContaining({ id: creator.id }),
}),
]);
});
it("refuses a body carrying a field the form does not collect", async () => {
// The schema is `.strict()`. This is the reason the client names its four
// fields one at a time instead of spreading its form state: a helpful
// extra key is a 422 about something nobody filled in.
signIn(await person("creator"));
const response = await createProfile({
displayName: "Managed record",
locale: "en",
timezone: "UTC",
heightCm: 120,
});
expect(response.status).toBe(422);
});
it("refuses a display name that is only whitespace", async () => {
// The bound the form mirrors before it sends: the route trims first.
signIn(await person("creator"));
expect(
(
await createProfile({
displayName: " ",
locale: "en",
timezone: "UTC",
})
).status,
).toBe(422);
});
it("deletes a profile, and answers a second attempt with the same 404", async () => {
const { creator, profile } = await profileWithPendingSecondGuardian();
signIn(creator);
expect((await deleteProfile(profile.id)).status).toBe(200);
expect(
await getPrismaClient().user.findUnique({ where: { id: profile.id } }),
).toBeNull();
const second = await deleteProfile(profile.id);
expect(second.status).toBe(404);
expect(await errorCodeOf(second)).toBe("managed_profile.not_found");
});
it("refuses deletion by somebody who is not a Guardian", async () => {
const { profile } = await profileWithPendingSecondGuardian();
signIn(await person("stranger"));
const response = await deleteProfile(profile.id);
expect(response.status).toBe(404);
expect(await errorCodeOf(response)).toBe("managed_profile.not_found");
// And the record is still there.
expect(
await getPrismaClient().user.findUnique({ where: { id: profile.id } }),
).not.toBeNull();
});
it("invites a second Guardian as PENDING, with the offset expiry the form sends", async () => {
const creator = await person("creator");
const invitee = await person("invitee");
signIn(creator);
const { profile } = await createManagedProfile({
creatorId: creator.id,
displayName: "Managed record",
dateOfBirth: null,
locale: "en",
timezone: "UTC",
});
// The shape `endOfDayIso` produces: an ISO instant, offset-bearing. A bare
// `YYYY-MM-DD` is a 422 here, which is why the client never sends the date
// field's raw value.
const expiresAt = new Date(Date.now() + 86_400_000).toISOString();
const response = await inviteGuardian(profile.id, {
identifier: invitee.username,
expiresAt,
});
expect(response.status).toBe(201);
const grant = (await response.json()).data;
expect(grant.acceptedAt).toBeNull();
// Read the stored row, because the entry the route answers with does not
// carry the expiry: a Guardian is published in one shape everywhere, and
// that shape is the roster's five fields. Asserting the column is the
// stronger claim anyway — it proves the offset instant was persisted
// rather than that it was echoed.
const stored = await getPrismaClient().accountGrant.findUniqueOrThrow({
where: { id: grant.grantId },
select: { expiresAt: true },
});
expect(stored.expiresAt?.toISOString()).toBe(expiresAt);
// PENDING, and therefore not counted by the floor — which is the sentence
// the card states before anybody tries to leave.
const roster = (await (await listGuardians(profile.id)).json())
.data as Array<{ state: string }>;
expect(roster.filter((r) => r.state === "ACTIVE")).toHaveLength(1);
expect(roster.filter((r) => r.state === "PENDING")).toHaveLength(1);
});
it("accepts the body the client actually composes, field names and all", async () => {
// Both ends and the pipe between them. The route's schema is `.strict()`,
// so a client that renamed `identifier` would be refused with a 422 — and
// a test that posted a hand-written copy of the body would stay green
// through exactly that rename. This posts the composer's own output.
const { guardianInviteBody } =
await import("@/lib/queries/use-managed-profiles");
const creator = await person("creator");
const invitee = await person("invitee");
signIn(creator);
const { profile } = await createManagedProfile({
creatorId: creator.id,
displayName: "Managed record",
dateOfBirth: null,
locale: "en",
timezone: "UTC",
});
const body = guardianInviteBody({
profileId: profile.id,
identifier: invitee.username,
expiresAt: null,
});
// Non-zero proof, deliberately name-agnostic: an empty body would be
// refused for the wrong reason and would make the 201 below prove nothing.
// The NAMES are proved by the route accepting it, not by a second copy of
// them written here.
expect(Object.keys(body)).toHaveLength(2);
const response = await inviteGuardian(
profile.id,
body as { identifier: string; expiresAt: string | null },
);
expect(response.status).toBe(201);
expect((await response.json()).data.acceptedAt).toBeNull();
});
it("answers an invitation with the roster's shape, not the grant row", async () => {
// `Object.keys`, for the same reason the roster read asserts it that way:
// the handler used to answer with the `AccountGrant` row straight out of
// Prisma — `grantorId`, `granteeId`, `scopeJson`, `lastUsedAt` and the
// rest — and every presence assertion in this file passed while it did.
// The invitation now answers with the entry the roster publishes, so one
// shape describes a Guardian everywhere and the published contract does
// not pin a table's columns to the wire.
const creator = await person("creator");
const invitee = await person("invitee");
signIn(creator);
const { profile } = await createManagedProfile({
creatorId: creator.id,
displayName: "Managed record",
dateOfBirth: null,
locale: "en",
timezone: "UTC",
});
const response = await inviteGuardian(profile.id, {
identifier: invitee.username,
expiresAt: null,
});
expect(response.status).toBe(201);
const entry = (await response.json()).data as Record<string, unknown>;
expect(Object.keys(entry).sort()).toEqual([
"acceptedAt",
"account",
"grantId",
"invitedAt",
"state",
]);
expect(Object.keys(entry.account as object).sort()).toEqual([
"displayName",
"id",
"username",
]);
expect(entry.state).toBe("PENDING");
expect((entry.account as { username: string }).username).toBe(
invitee.username,
);
// The pipe, not the ends: the entry the invitation answered with is the
// entry the roster read answers with a moment later.
const roster = (await (await listGuardians(profile.id)).json())
.data as Array<Record<string, unknown>>;
expect(roster).toContainEqual(entry);
});
it("answers a removal with the access it ended and when", async () => {
const { creator, profile, invitation } =
await profileWithPendingSecondGuardian();
signIn(creator);
const response = await removeGuardian(profile.id, invitation.id);
expect(response.status).toBe(200);
const ended = (await response.json()).data as Record<string, unknown>;
expect(Object.keys(ended).sort()).toEqual(["grantId", "revokedAt"]);
expect(ended.grantId).toBe(invitation.id);
expect(ended.revokedAt).toEqual(expect.any(String));
});
it("refuses a bare calendar date as an expiry", async () => {
const creator = await person("creator");
const invitee = await person("invitee");
signIn(creator);
const { profile } = await createManagedProfile({
creatorId: creator.id,
displayName: "Managed record",
dateOfBirth: null,
locale: "en",
timezone: "UTC",
});
expect(
(
await inviteGuardian(profile.id, {
identifier: invitee.username,
expiresAt: "2027-01-30",
})
).status,
).toBe(422);
});
it("answers an invitation addressed to yourself with the duplicate refusal", async () => {
// NOT `managed_profile.guardian.self`, and the plan predicted otherwise.
// `self_grant` fires when the grantor equals the grantee, and the grantor
// here is the PROFILE — never the caller. So inviting yourself is simply
// an account that already has access, which is what the person is told,
// and the route's 422 self arm cannot be reached. Recorded as a finding;
// the client has no arm for a code the server cannot send.
const { creator, profile } = await profileWithPendingSecondGuardian();
signIn(creator);
const response = await inviteGuardian(profile.id, {
identifier: creator.username,
});
expect(response.status).toBe(409);
expect(await errorCodeOf(response)).toBe(
"managed_profile.guardian.duplicate",
);
});
it("cannot be made to answer the self-invitation 422 either", async () => {
// The other door to `self_grant` is naming the profile itself, and that
// one is closed by the managed-invitee check one line earlier. Both paths
// are asserted so the arm is known to be dead rather than assumed to be.
const { creator, profile } = await profileWithPendingSecondGuardian();
signIn(creator);
const response = await inviteGuardian(profile.id, {
identifier: profile.username,
});
expect(response.status).toBe(422);
expect(await errorCodeOf(response)).toBe(
"managed_profile.guardian.managed_invitee",
);
});
it("refuses an invitation to somebody who already has access", async () => {
const { creator, invitee, profile } =
await profileWithPendingSecondGuardian();
signIn(creator);
const response = await inviteGuardian(profile.id, {
identifier: invitee.username,
});
expect(response.status).toBe(409);
expect(await errorCodeOf(response)).toBe(
"managed_profile.guardian.duplicate",
);
});
it("refuses a managed profile as a Guardian", async () => {
const { creator, profile } = await profileWithPendingSecondGuardian();
const { profile: other } = await createManagedProfile({
creatorId: creator.id,
displayName: "Second record",
dateOfBirth: null,
locale: "en",
timezone: "UTC",
});
signIn(creator);
const response = await inviteGuardian(profile.id, {
identifier: other.username,
});
expect(response.status).toBe(422);
expect(await errorCodeOf(response)).toBe(
"managed_profile.guardian.managed_invitee",
);
});
it("answers an unknown identifier with a 404 that names itself", async () => {
// This route has a second 404 (`managed_profile.not_found`) with a
// different next step, so the unknown name carries its own code and a
// client never has to tell the two apart by status.
const { creator, profile } = await profileWithPendingSecondGuardian();
signIn(creator);
const response = await inviteGuardian(profile.id, {
identifier: "nobody-here",
});
expect(response.status).toBe(404);
expect(await errorCodeOf(response)).toBe("sharing.invite.no_account");
});
it("refuses an invitation from somebody who is not a Guardian", async () => {
const { profile, invitee } = await profileWithPendingSecondGuardian();
signIn(await person("stranger"));
const response = await inviteGuardian(profile.id, {
identifier: invitee.username,
});
expect(response.status).toBe(404);
expect(await errorCodeOf(response)).toBe("managed_profile.not_found");
});
it("ends another Guardian's access and clears their switched sessions", async () => {
const { creator, invitee, profile, invitation } =
await profileWithPendingSecondGuardian();
await acceptGrant({ grantId: invitation.id, granteeId: invitee.id });
signIn(creator);
const response = await removeGuardian(profile.id, invitation.id);
expect(response.status).toBe(200);
expect(
await getPrismaClient().accountGrant.findUniqueOrThrow({
where: { id: invitation.id },
}),
).toMatchObject({ revokedBy: "GRANTOR" });
// And the roster the card re-reads no longer carries them.
const roster = (await (await listGuardians(profile.id)).json())
.data as unknown[];
expect(roster).toHaveLength(1);
});
it("withdraws a pending invitation without touching the accepted count", async () => {
const { creator, profile, invitation } =
await profileWithPendingSecondGuardian();
signIn(creator);
expect((await removeGuardian(profile.id, invitation.id)).status).toBe(200);
const roster = (await (await listGuardians(profile.id)).json())
.data as Array<{ state: string }>;
expect(roster).toHaveLength(1);
expect(roster[0].state).toBe("ACTIVE");
});
it("refuses a Guardian ending their OWN access, and leaves the grant alone", async () => {
// This is the shape the card's missing self-removal control comes from.
// `revokeManagedProfileGuardian` refuses a target whose grantee is the
// caller, and the route turns that into `managed_profile.not_found` — NOT
// into the last-Guardian 409. A self-removal button would therefore be one
// that always fails, with a message that does not describe what happened.
// Stepping away is the renounce control on the shared-access panel.
const { creator, profile } = await profileWithPendingSecondGuardian();
const own = await getPrismaClient().accountGrant.findFirstOrThrow({
where: { grantorId: profile.id, granteeId: creator.id },
});
signIn(creator);
const response = await removeGuardian(profile.id, own.id);
expect(response.status).toBe(404);
expect(await errorCodeOf(response)).toBe("managed_profile.not_found");
expect(
await getPrismaClient().accountGrant.findUniqueOrThrow({
where: { id: own.id },
}),
).toMatchObject({ revokedAt: null, revokedBy: null });
});
it("refuses removal by somebody who is not a Guardian of the profile", async () => {
const { profile, invitation } = await profileWithPendingSecondGuardian();
signIn(await person("stranger"));
const response = await removeGuardian(profile.id, invitation.id);
expect(response.status).toBe(404);
expect(await errorCodeOf(response)).toBe("managed_profile.not_found");
});
it("cannot be made to answer the last-Guardian 409 through this route", async () => {
// Recorded as a property rather than as a gap in coverage. The refusal
// fires only when the TARGET is active and the accepted count is at most
// one — and the caller must be an accepted Guardian to get this far, and
// may not target themselves. Two accepted Guardians is therefore the
// minimum for the branch to be evaluated at all, and two is never one.
//
// The floor is real; it is enforced on the renounce route, on account
// deletion and on a data wipe. It is this ROUTE that cannot express it,
// which is why the card states the floor from the roster instead of
// waiting for a refusal that will not come.
const { creator, invitee, profile, invitation } =
await profileWithPendingSecondGuardian();
await acceptGrant({ grantId: invitation.id, granteeId: invitee.id });
signIn(creator);
// Two accepted Guardians: the removal is allowed rather than refused.
expect((await removeGuardian(profile.id, invitation.id)).status).toBe(200);
// And with one left, the only target available to that Guardian is
// themselves — which is the 404 above, not the 409.
const own = await getPrismaClient().accountGrant.findFirstOrThrow({
where: { grantorId: profile.id, granteeId: creator.id },
});
const self = await removeGuardian(profile.id, own.id);
expect(self.status).toBe(404);
expect(await errorCodeOf(self)).not.toBe(
"managed_profile.guardian.required",
);
});
});
describe("the three caller states the roster read has to refuse", () => {
it("refuses a Guardian whose invitation has not been accepted", async () => {
// PENDING confers nothing. `activeGuardianWhere` requires `acceptedAt`, so
// an invitee who can see the invitation on their own panel still cannot
// read the roster of the record it is for — and must not be able to, or
// an invitation would disclose the household before it was accepted.
const { invitee, profile } = await profileWithPendingSecondGuardian();
signIn(invitee);
const response = await listGuardians(profile.id);
expect(response.status).toBe(404);
expect(await errorCodeOf(response)).toBe("managed_profile.not_found");
});
it("refuses a Guardian whose grant has expired", async () => {
// The other half of the same predicate, and the one a `revokedAt IS NULL`
// check alone would miss: nobody ended this grant, the clock did.
const { invitee, profile, invitation } =
await profileWithPendingSecondGuardian();
await acceptGrant({ grantId: invitation.id, granteeId: invitee.id });
await getPrismaClient().accountGrant.update({
where: { id: invitation.id },
data: { expiresAt: new Date(Date.now() - 60_000) },
});
signIn(invitee);
const response = await listGuardians(profile.id);
expect(response.status).toBe(404);
expect(await errorCodeOf(response)).toBe("managed_profile.not_found");
});
it("refuses a holder of MANAGE on an ordinary record, against a real managed profile", async () => {
// Distinct from the leg above that points a delegate at THEIR OWN grantor:
// this caller holds a live MANAGE grant somewhere, and aims it at somebody
// else's managed profile. Holding the level is not holding this record.
const { profile } = await profileWithPendingSecondGuardian();
const owner = await person("owner");
const manager = await person("manager");
const invitation = await inviteGrant({
grantorId: owner.id,
granteeId: manager.id,
access: "MANAGE",
scope: null,
});
await acceptGrant({ grantId: invitation.id, granteeId: manager.id });
signIn(manager);
const response = await listGuardians(profile.id);
expect(response.status).toBe(404);
expect(await errorCodeOf(response)).toBe("managed_profile.not_found");
});
});
describe("what deletion does to a Guardian who is standing inside the record", () => {
it("puts their session back in their own account rather than nowhere", async () => {
// The composition worth an integration leg: `Session.actingAsUserId` is a
// foreign key with ON DELETE SET NULL, and the epoch is bumped by a
// database trigger on that column. Deleting the profile therefore has to
// leave the other Guardian's browser in a state it can prove — back in
// their own record, on a NEW epoch — rather than pointed at a row that is
// gone. A session left holding a dangling selector is the shape that
// strands somebody behind the fence with no way out.
const { creator, invitee, profile, invitation } =
await profileWithPendingSecondGuardian();
await acceptGrant({ grantId: invitation.id, granteeId: invitee.id });
await switchSessionTo(invitee.sessionId, profile.id);
const before = await getPrismaClient().session.findUniqueOrThrow({
where: { id: invitee.sessionId },
});
// Non-zero proof: they really were inside the record before the delete.
expect(before.actingAsUserId).toBe(profile.id);
signIn(creator);
expect((await deleteProfile(profile.id)).status).toBe(200);
const after = await getPrismaClient().session.findUniqueOrThrow({
where: { id: invitee.sessionId },
});
// The session survives — deleting a record must not sign anybody out —
// and it is back in its own account.
expect(after.actingAsUserId).toBeNull();
// And the context moved, so a request formed inside the deleted record is
// refused as stale rather than served against nothing.
expect(after.recordEpoch).toBeGreaterThan(before.recordEpoch);
});
});
describe("how many profiles one account may create in an hour", () => {
it("refuses the eleventh, with the same ceiling the guardian route carries", async () => {
// Ten an hour. The guardian invitation has been bounded since it shipped;
// this route creates a real account row per call and was not, which is the
// kind of asymmetry between two endpoints of one family that nobody
// notices until it is used.
const creator = await person("creator");
signIn(creator);
for (let i = 0; i < 10; i += 1) {
const allowed = await createProfile({
displayName: `Managed record ${i}`,
locale: "en",
timezone: "UTC",
});
expect(allowed.status, `creation ${i}`).toBe(201);
}
const refused = await createProfile({
displayName: "One too many",
locale: "en",
timezone: "UTC",
});
expect(refused.status).toBe(429);
// The status default. A 429 has one meaning in this family (wait), so the
// client branches on the status and needs no code of its own.
expect(await errorCodeOf(refused)).toBe("http.rate_limited");
// And nothing was created by the refused call.
expect(
await getPrismaClient().user.count({
where: { managedProfileAt: { not: null } },
}),
).toBe(10);
});
it("bounds the caller and not the instance", async () => {
// A per-user bucket, so one busy account cannot stop another from creating
// a record. `checkRateLimit` is keyed by caller id; this proves the key
// really carries it rather than being a single global bucket.
const first = await person("creator");
signIn(first);
for (let i = 0; i < 10; i += 1) {
await createProfile({
displayName: `Managed record ${i}`,
locale: "en",
timezone: "UTC",
});
}
expect(
(
await createProfile({
displayName: "One too many",
locale: "en",
timezone: "UTC",
})
).status,
).toBe(429);
const second = await person("creator");
signIn(second);
expect(
(
await createProfile({
displayName: "Somebody else's first",
locale: "en",
timezone: "UTC",
})
).status,
).toBe(201);
});
});