@@ -0,0 +1,114 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// The account module's data-subject rights on the mariadb and mongodb profiles: the same store
|
||||
// (modules/account/adapters/store.ts) over the profile's real data source, with the kernel's and the
|
||||
// modules' committed dialect migrations applied first. A person's export holds their rows and nobody
|
||||
// else's, and their deletion erases them and leaves another person's account as it was.
|
||||
//
|
||||
// It needs the engines: ACCOUNT_TEST_MARIADB_URL (MariaDB LTS) and ACCOUNT_TEST_MONGODB_URL (MongoDB 8 as
|
||||
// a single-node replica set, AT-D15), each naming a database of its own that the test may fill. Without
|
||||
// them each block is skipped by name; `scripts/vendor/test-template.sh app --profile <name>` starts both.
|
||||
|
||||
import { type DialectProfile, expectedDialectMigrations } from "@app/config"
|
||||
import { sessions } from "@conusai-kit/auth"
|
||||
import { buildRegistry } from "@conusai-kit/core"
|
||||
import { entitiesOf } from "@conusai-kit/data"
|
||||
import { KIT_JOBS_ENTITIES } from "@conusai-kit/jobs-pgboss"
|
||||
import { orgMemberships, users } from "@conusai-kit/tenancy"
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest"
|
||||
import type { DialectData } from "../../apps/worker/src/dialect/engine.js"
|
||||
import { engineFor } from "../../apps/worker/src/dialect/runtime.js"
|
||||
import { makeApp, type Person } from "../../modules/account/__tests__/harness.js"
|
||||
import type { AccountExport, DeletionView } from "../../modules/account/index.js"
|
||||
import { accountDeletions } from "../../modules/account/index.js"
|
||||
import { freshDialectModules } from "./fixture.js"
|
||||
|
||||
const ENGINES: readonly [DialectProfile, string, string | undefined][] = [
|
||||
["mariadb", "MARIADB_URL", process.env.ACCOUNT_TEST_MARIADB_URL],
|
||||
["mongodb", "MONGODB_URL", process.env.ACCOUNT_TEST_MONGODB_URL],
|
||||
]
|
||||
|
||||
/** What the migrate job of the profile applies (apps/worker/src/dialect/), on the engine under test. */
|
||||
async function migrate(
|
||||
profile: DialectProfile,
|
||||
env: Record<string, string>,
|
||||
registry: ReturnType<typeof buildRegistry>,
|
||||
): Promise<string[]> {
|
||||
const outcome = await engineFor(profile).migrate(env, registry)
|
||||
return [...outcome.applied, ...outcome.skipped]
|
||||
}
|
||||
|
||||
async function orgOf(person: Person, slug: string): Promise<string> {
|
||||
const created = (await person.call().callByName("tenancy.org.create", { slug, name: slug })) as { orgId: string }
|
||||
return created.orgId
|
||||
}
|
||||
|
||||
for (const [profile, variable, url] of ENGINES) {
|
||||
const modules = freshDialectModules(profile)
|
||||
const registry = buildRegistry(modules)
|
||||
describe.skipIf(!url)(`account data-subject rights on ${profile} (${variable})`, () => {
|
||||
let data: DialectData
|
||||
const env = { [variable]: url ?? "" }
|
||||
|
||||
beforeAll(async () => {
|
||||
// The migrate job's whole list applies on the engine, and it is the list readiness expects.
|
||||
expect((await migrate(profile, env, registry)).sort()).toEqual(
|
||||
(await expectedDialectMigrations(registry, profile)).sort(),
|
||||
)
|
||||
const entities = [...entitiesOf(registry, profile), ...KIT_JOBS_ENTITIES]
|
||||
expect(entities.map(({ table }) => table)).toEqual(
|
||||
expect.arrayContaining(["account_deletion", "account_session_revocation"]),
|
||||
)
|
||||
data = await engineFor(profile).connect(env, entities)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await data?.close()
|
||||
})
|
||||
|
||||
it("exports the person's own rows and deletes the account, leaving another person's as it was", async () => {
|
||||
const app = makeApp({ data, modules })
|
||||
const tag = Math.random().toString(36).slice(2, 8)
|
||||
const ada = await app.signUp(`ada-${tag}@example.test`)
|
||||
const grace = await app.signUp(`grace-${tag}@example.test`)
|
||||
const adaOrg = await orgOf(ada, `solo-${tag}`)
|
||||
await orgOf(grace, `other-${tag}`)
|
||||
|
||||
const exported = (await ada.call().callByName("account.data.export", {})) as AccountExport
|
||||
expect(exported).toMatchObject({ subject: ada.actor.userId, complete: true, failed: [] })
|
||||
expect(exported.sections.account).toMatchObject({
|
||||
status: "ok",
|
||||
data: { profile: { userId: ada.actor.userId, email: ada.email }, organizations: [{ slug: `solo-${tag}` }] },
|
||||
})
|
||||
// Negative control: nothing of the other person is in it.
|
||||
expect(JSON.stringify(exported)).not.toContain(grace.email)
|
||||
expect(JSON.stringify(exported)).not.toContain(`other-${tag}`)
|
||||
|
||||
const view = (await ada.call().callByName("account.deletion.request", { confirm: ada.email })) as DeletionView
|
||||
expect(view).toMatchObject({ state: "completed", pending: [], abandoned: [] })
|
||||
|
||||
expect(await app.data.global(users).get(ada.actor.userId)).toBeNull()
|
||||
expect(
|
||||
await app.data.forScope({ level: "org", orgId: adaOrg, actor: ada.actor }).of(orgMemberships).count(),
|
||||
).toBe(0)
|
||||
const adaSessions = (
|
||||
await app.data.global(sessions).list({ where: [{ field: "userId", op: "eq", value: ada.actor.userId }] })
|
||||
).items
|
||||
expect(adaSessions).toEqual([])
|
||||
const ledger = (
|
||||
await app.data
|
||||
.global(accountDeletions)
|
||||
.list({ where: [{ field: "userId", op: "eq", value: ada.actor.userId }] })
|
||||
).items
|
||||
expect(ledger).toMatchObject([{ state: "completed" }])
|
||||
expect(JSON.stringify(ledger)).not.toContain(ada.email)
|
||||
|
||||
// The other person keeps everything: their user row, their membership and their export.
|
||||
expect((await app.data.global(users).get(grace.actor.userId))?.email).toBe(grace.email)
|
||||
const graceExport = (await grace.call().callByName("account.data.export", {})) as AccountExport
|
||||
expect(graceExport.sections.account).toMatchObject({
|
||||
status: "ok",
|
||||
data: { organizations: [{ slug: `other-${tag}` }] },
|
||||
})
|
||||
}, 60_000)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user