Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138YqDj3ttfayRwuMKKrqVj
429 lines
17 KiB
TypeScript
429 lines
17 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import type { MediaRepos } from "./media.js";
|
|
import { createMediaRepos } from "./media.js";
|
|
import type { MediaTestDb, SeededMediaSpace } from "./media-fixtures.js";
|
|
import {
|
|
openMediaTestDb,
|
|
seedMediaSpace,
|
|
sourceItem,
|
|
testBrandKit,
|
|
testEdl,
|
|
vectorBytes,
|
|
} from "./media-fixtures.js";
|
|
import { IsolationError } from "./scope.js";
|
|
|
|
const databaseUrl = process.env.DATABASE_URL;
|
|
const describePostgres =
|
|
process.env.VERIFY_DATABASE && databaseUrl ? describe.sequential : describe.skip;
|
|
|
|
describePostgres("media repositories: space isolation (PostgreSQL)", () => {
|
|
let db: MediaTestDb;
|
|
let media: MediaRepos;
|
|
let a: SeededMediaSpace;
|
|
let owner: { userId: string; spaceId: string };
|
|
let intruder: { userId: string; spaceId: string };
|
|
|
|
beforeAll(async () => {
|
|
db = openMediaTestDb(databaseUrl!);
|
|
media = createMediaRepos(db.prisma);
|
|
owner = await db.newUser();
|
|
intruder = await db.newUser();
|
|
a = await seedMediaSpace(media, owner.spaceId, owner.userId);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await db?.cleanup();
|
|
});
|
|
|
|
async function snapshot() {
|
|
const where = { spaceId: owner.spaceId };
|
|
const p = db.prisma;
|
|
return {
|
|
sources: await p.mediaSource.findMany({ where, orderBy: { id: "asc" } }),
|
|
assets: await p.mediaAsset.findMany({ where, orderBy: { id: "asc" } }),
|
|
segments: await p.mediaSegment.findMany({ where, orderBy: { id: "asc" } }),
|
|
analyses: await p.mediaAnalysis.findMany({ where, orderBy: { id: "asc" } }),
|
|
collections: await p.mediaCollection.findMany({ where, orderBy: { id: "asc" } }),
|
|
items: await p.mediaCollectionItem.findMany({
|
|
where: { collection: where },
|
|
orderBy: [{ collectionId: "asc" }, { assetId: "asc" }],
|
|
}),
|
|
people: await p.mediaPerson.findMany({ where, orderBy: { id: "asc" } }),
|
|
tracks: await p.mediaFaceTrack.findMany({ where, orderBy: { id: "asc" } }),
|
|
marks: await p.mediaMark.findMany({ where, orderBy: { id: "asc" } }),
|
|
kits: await p.mediaBrandKit.findMany({ where, orderBy: { id: "asc" } }),
|
|
projects: await p.mediaProject.findMany({ where, orderBy: { id: "asc" } }),
|
|
plans: await p.mediaEditPlan.findMany({ where, orderBy: { id: "asc" } }),
|
|
renders: await p.mediaRender.findMany({ where, orderBy: { id: "asc" } }),
|
|
publications: await p.mediaPublication.findMany({ where, orderBy: { id: "asc" } }),
|
|
metrics: await p.mediaMetric.findMany({ where, orderBy: { id: "asc" } }),
|
|
mappings: await p.mediaMetricMapping.findMany({ where, orderBy: { id: "asc" } }),
|
|
teaser: await p.mediaTeaserModel.findMany({ where }),
|
|
uploads: await p.mediaUpload.findMany({ where, orderBy: { id: "asc" } }),
|
|
};
|
|
}
|
|
|
|
it("rejects every read and write of another space's ids and leaves its rows untouched", async () => {
|
|
const before = await snapshot();
|
|
const b = intruder.spaceId;
|
|
const denied: Array<[string, () => Promise<unknown>]> = [
|
|
["getSource", () => media.getSource(b, a.sourceId)],
|
|
["updateSourceSync", () => media.updateSourceSync(b, a.sourceId, { status: "error" })],
|
|
["removeSource", () => media.removeSource(b, a.sourceId)],
|
|
["upsertSourceItems", () => media.upsertSourceItems(b, a.sourceId, [sourceItem(99)])],
|
|
["getAsset", () => media.getAsset(b, a.assetId)],
|
|
["getAssetRecord", () => media.getAssetRecord(b, a.assetId)],
|
|
[
|
|
"transitionIngestState",
|
|
() => media.transitionIngestState(b, a.assetId, { from: ["indexed"], to: "proxying" }),
|
|
],
|
|
[
|
|
"recordProxy",
|
|
() => media.recordProxy(b, a.assetId, { contentHash: "x", proxyPath: "p", thumbPath: "t" }),
|
|
],
|
|
["recordAnalysisVersion", () => media.recordAnalysisVersion(b, a.assetId, 9, 1)],
|
|
["requestUpload", () => media.requestUpload(b, [a.assetId])],
|
|
[
|
|
"upsertAnalysis",
|
|
() =>
|
|
media.upsertAnalysis(b, a.assetId, {
|
|
kind: "embeddings",
|
|
modelId: "m",
|
|
modelVersion: "1",
|
|
vectors: vectorBytes([9]),
|
|
}),
|
|
],
|
|
["latestAnalysis", () => media.latestAnalysis(b, a.assetId, "embeddings")],
|
|
[
|
|
"replaceSegments",
|
|
() => media.replaceSegments(b, a.assetId, 2, [{ kind: "shot", startMs: 0, endMs: 1 }]),
|
|
],
|
|
["setSegmentsPaidOnly", () => media.setSegmentsPaidOnly(b, [a.segmentId], true)],
|
|
[
|
|
"beginUpload",
|
|
() =>
|
|
media.beginUpload(b, {
|
|
assetId: a.assetId,
|
|
userId: intruder.userId,
|
|
sizeBytes: 1,
|
|
sha256: "b".repeat(64),
|
|
fileName: "x",
|
|
}),
|
|
],
|
|
["uploadStatus", () => media.uploadStatus(b, a.uploadId)],
|
|
["getUploadRecord", () => media.getUploadRecord(b, a.uploadId)],
|
|
["markChunkReceived", () => media.markChunkReceived(b, a.uploadId, 0)],
|
|
["completeUpload", () => media.completeUpload(b, a.uploadId, { storagePath: "x" })],
|
|
["failUpload", () => media.failUpload(b, a.uploadId)],
|
|
[
|
|
"replaceClusters",
|
|
() =>
|
|
media.replaceClusters(b, "session", [
|
|
{
|
|
key: "s1",
|
|
title: "Stolen",
|
|
labelSource: "rule",
|
|
startAt: null,
|
|
endAt: null,
|
|
centroid: null,
|
|
coverAssetId: null,
|
|
assetIds: [a.assetId],
|
|
},
|
|
]),
|
|
],
|
|
["getCollection", () => media.getCollection(b, a.collectionId)],
|
|
["collectionAssetIds", () => media.collectionAssetIds(b, [a.collectionId])],
|
|
["renameCollection", () => media.renameCollection(b, a.collectionId, "Mine")],
|
|
["mergeCollections", () => media.mergeCollections(b, [a.collectionId, `${a.collectionId}x`])],
|
|
["splitCollection", () => media.splitCollection(b, a.collectionId, a.secondAssetId)],
|
|
["createManualCollection", () => media.createManualCollection(b, "Mine", [a.assetId])],
|
|
["getPerson", () => media.getPerson(b, a.personId)],
|
|
[
|
|
"createPersonSuggestions",
|
|
() =>
|
|
media.createPersonSuggestions(b, [{ centroid: vectorBytes([1]), trackIds: [a.trackId] }]),
|
|
],
|
|
["updatePerson", () => media.updatePerson(b, a.personId, { displayName: "Mine" })],
|
|
["mergePeople", () => media.mergePeople(b, [a.personId, `${a.personId}x`])],
|
|
["assignTrack", () => media.assignTrack(b, a.trackId, null)],
|
|
[
|
|
"recordConsent (foreign person)",
|
|
() =>
|
|
media.recordConsent(
|
|
{ spaceId: b, recordedByUserId: intruder.userId },
|
|
{
|
|
personId: a.personId,
|
|
consentArtifactId: null,
|
|
consentRecordedAt: new Date().toISOString(),
|
|
ageVerificationMethod: "none",
|
|
ageVerifiedAt: null,
|
|
},
|
|
),
|
|
],
|
|
[
|
|
"recordConsent (foreign recorder)",
|
|
() =>
|
|
media.recordConsent(
|
|
{ spaceId: owner.spaceId, recordedByUserId: intruder.userId },
|
|
{
|
|
personId: a.personId,
|
|
consentArtifactId: null,
|
|
consentRecordedAt: new Date().toISOString(),
|
|
ageVerificationMethod: "none",
|
|
ageVerifiedAt: null,
|
|
},
|
|
),
|
|
],
|
|
["removePersonCompletely", () => media.removePersonCompletely(b, a.personId)],
|
|
["replaceFaceTracks", () => media.replaceFaceTracks(b, a.assetId, "faces-2", [])],
|
|
[
|
|
"createMark",
|
|
() =>
|
|
media.createMark(b, {
|
|
kind: "scar",
|
|
label: null,
|
|
personId: null,
|
|
assetId: a.assetId,
|
|
prompt: { tMs: 0 },
|
|
treatment: "blur",
|
|
}),
|
|
],
|
|
["getMark", () => media.getMark(b, a.markId)],
|
|
["updateMark", () => media.updateMark(b, a.markId, { treatment: "cover" })],
|
|
[
|
|
"recordMarkResult",
|
|
() =>
|
|
media.recordMarkResult(b, a.markId, {
|
|
qualityScore: 0,
|
|
suggestedFallback: "cover",
|
|
maskDir: "x",
|
|
}),
|
|
],
|
|
["removeMark", () => media.removeMark(b, a.markId)],
|
|
["upsertBrandKit", () => media.upsertBrandKit(b, { id: a.brandKitId, kit: testBrandKit })],
|
|
["getBrandKit", () => media.getBrandKit(b, a.brandKitId)],
|
|
["removeBrandKit", () => media.removeBrandKit(b, a.brandKitId)],
|
|
[
|
|
"createProject (foreign brand kit)",
|
|
() =>
|
|
media.createProject(b, intruder.userId, {
|
|
title: "x",
|
|
goal: "teaser",
|
|
profileIds: ["example-short-vertical"],
|
|
brandKitId: a.brandKitId,
|
|
}),
|
|
],
|
|
[
|
|
"createProject (foreign collection)",
|
|
() =>
|
|
media.createProject(b, intruder.userId, {
|
|
title: "x",
|
|
goal: "teaser",
|
|
profileIds: ["example-short-vertical"],
|
|
collectionIds: [a.collectionId],
|
|
}),
|
|
],
|
|
["updateProject", () => media.updateProject(b, a.projectId, { title: "Mine" })],
|
|
["getProject", () => media.getProject(b, a.projectId)],
|
|
["getProjectRecord", () => media.getProjectRecord(b, a.projectId)],
|
|
["removeProject", () => media.removeProject(b, a.projectId)],
|
|
[
|
|
"createPlanVersion",
|
|
() =>
|
|
media.createPlanVersion(b, {
|
|
projectId: a.projectId,
|
|
profileId: "example-short-vertical",
|
|
edl: testEdl(a.assetId),
|
|
createdBy: "agent",
|
|
rationale: null,
|
|
}),
|
|
],
|
|
["getPlan", () => media.getPlan(b, a.planId)],
|
|
["latestPlan", () => media.latestPlan(b, a.projectId, "example-short-vertical")],
|
|
["listPlanVersions", () => media.listPlanVersions(b, a.projectId, "example-short-vertical")],
|
|
[
|
|
"createRender",
|
|
() => media.createRender(b, { planId: a.planId, quality: "draft", createdBy: "x" }),
|
|
],
|
|
["getRender", () => media.getRender(b, a.renderId)],
|
|
["getRenderRecord", () => media.getRenderRecord(b, a.renderId)],
|
|
["claimRender", () => media.claimRender(b, a.queuedRenderId)],
|
|
["updateRender", () => media.updateRender(b, a.queuedRenderId, { progress: 0.5 })],
|
|
["cancelRender", () => media.cancelRender(b, a.queuedRenderId)],
|
|
[
|
|
"createPublication",
|
|
() =>
|
|
media.createPublication(b, intruder.userId, {
|
|
renderId: a.renderId,
|
|
destination: "bundle",
|
|
targetUrl: null,
|
|
trackingUrl: null,
|
|
utm: null,
|
|
title: null,
|
|
caption: null,
|
|
tags: [],
|
|
thumbnailVariant: null,
|
|
scheduledAt: null,
|
|
}),
|
|
],
|
|
["getPublication", () => media.getPublication(b, a.publicationId)],
|
|
["getPublicationRecord", () => media.getPublicationRecord(b, a.publicationId)],
|
|
["schedulePublication", () => media.schedulePublication(b, a.publicationId, new Date())],
|
|
["cancelPublication", () => media.cancelPublication(b, a.publicationId)],
|
|
["markPublicationBundled", () => media.markPublicationBundled(b, a.publicationId, null)],
|
|
["markPublicationPublished", () => media.markPublicationPublished(b, a.publicationId, null)],
|
|
["markPublicationFailed", () => media.markPublicationFailed(b, a.publicationId)],
|
|
[
|
|
"recordMetric",
|
|
() =>
|
|
media.recordMetric(b, {
|
|
publicationId: a.publicationId,
|
|
observedAt: new Date().toISOString(),
|
|
views: 1,
|
|
clicks: 0,
|
|
conversions: 0,
|
|
revenueCents: 0,
|
|
currency: "EUR",
|
|
source: "manual",
|
|
}),
|
|
],
|
|
[
|
|
"recordMetrics",
|
|
() =>
|
|
media.recordMetrics(b, [
|
|
{
|
|
publicationId: a.publicationId,
|
|
observedAt: new Date().toISOString(),
|
|
views: 1,
|
|
clicks: 0,
|
|
conversions: 0,
|
|
revenueCents: 0,
|
|
currency: "EUR",
|
|
source: "csv_import",
|
|
},
|
|
]),
|
|
],
|
|
["listMetrics", () => media.listMetrics(b, a.publicationId)],
|
|
[
|
|
"saveMetricMapping (foreign id)",
|
|
() =>
|
|
media.saveMetricMapping(b, {
|
|
id: a.mappingId,
|
|
name: "Stolen",
|
|
delimiter: ",",
|
|
columns: { publication: "id", observedAt: "date" },
|
|
publicationKey: "id",
|
|
revenueUnit: "major",
|
|
defaultCurrency: "EUR",
|
|
}),
|
|
],
|
|
["getMetricMapping", () => media.getMetricMapping(b, a.mappingId)],
|
|
["funnel (foreign project)", () => media.funnel(b, a.projectId)],
|
|
];
|
|
for (const [name, call] of denied) {
|
|
await expect(call(), name).rejects.toBeInstanceOf(IsolationError);
|
|
}
|
|
|
|
// Reads without ids return nothing of the other space.
|
|
expect(await media.listSources(b)).toEqual([]);
|
|
expect((await media.searchAssets(b, {})).items).toEqual([]);
|
|
expect((await media.searchAssets(b, { text: "clip" })).items).toEqual([]);
|
|
expect((await media.searchAssets(b, { personId: a.personId })).items).toEqual([]);
|
|
expect((await media.searchAssets(b, { collectionId: a.collectionId })).items).toEqual([]);
|
|
expect((await media.searchAssets(b, { sourceId: a.sourceId })).items).toEqual([]);
|
|
expect(await media.listAssetRecords(b, [a.assetId])).toEqual([]);
|
|
expect(await media.listAssetsForClustering(b)).toEqual([]);
|
|
expect(await media.assetsNeedingWork(b, "indexed", 10)).toEqual([]);
|
|
expect((await media.latestAnalysesByKind(b, "embeddings")).size).toBe(0);
|
|
expect(await media.listSegments(b, [a.assetId])).toEqual([]);
|
|
expect((await media.listCollections(b, {})).items).toEqual([]);
|
|
expect(await media.listPeople(b)).toEqual([]);
|
|
expect(await media.listPersonRecords(b, [a.personId])).toEqual([]);
|
|
expect(await media.personCentroids(b)).toEqual([]);
|
|
expect(await media.listFaceTracks(b, { assetId: a.assetId })).toEqual([]);
|
|
expect(await media.faceTrackRecords(b, [a.assetId])).toEqual([]);
|
|
expect(await media.faceTrackEmbeddings(b)).toEqual([]);
|
|
expect(await media.listMarks(b, { assetId: a.assetId })).toEqual([]);
|
|
expect(await media.marksForAsset(b, a.assetId)).toEqual([]);
|
|
expect(await media.listBrandKits(b)).toEqual([]);
|
|
expect((await media.listProjects(b)).items).toEqual([]);
|
|
expect(await media.latestPlans(b, a.projectId)).toEqual([]);
|
|
expect(await media.listRenders(b, a.projectId)).toEqual([]);
|
|
expect((await media.listPublications(b, { projectId: a.projectId })).items).toEqual([]);
|
|
expect((await media.resolvePublicationKeys(b, "id", [a.publicationId])).size).toBe(0);
|
|
expect(await media.listMetricMappings(b)).toEqual([]);
|
|
expect(await media.funnel(b)).toEqual([]);
|
|
expect(await media.publicationsWithMetricsCount(b)).toBe(0);
|
|
expect(await media.getTeaserModel(b)).toBeNull();
|
|
await media.resetTeaserModel(b);
|
|
|
|
expect(await snapshot()).toEqual(before);
|
|
// The owner still sees everything.
|
|
expect((await media.searchAssets(owner.spaceId, {})).items).toHaveLength(2);
|
|
expect(await media.getTeaserModel(owner.spaceId)).not.toBeNull();
|
|
});
|
|
|
|
it("resolves the owning space for worker jobs", async () => {
|
|
expect(await media.mediaSpaceOf("source", a.sourceId)).toBe(owner.spaceId);
|
|
expect(await media.mediaSpaceOf("asset", a.assetId)).toBe(owner.spaceId);
|
|
expect(await media.mediaSpaceOf("render", a.renderId)).toBe(owner.spaceId);
|
|
expect(await media.mediaSpaceOf("publication", a.publicationId)).toBe(owner.spaceId);
|
|
expect(await media.mediaSpaceOf("asset", "missing")).toBeNull();
|
|
});
|
|
|
|
it("deleting a space removes every media row of it and nothing else", async () => {
|
|
const other = await db.newUser();
|
|
const seeded = await seedMediaSpace(media, other.spaceId, other.userId);
|
|
expect(seeded.assetId).toBeTruthy();
|
|
const tables = [
|
|
"media_sources",
|
|
"media_assets",
|
|
"media_uploads",
|
|
"media_segments",
|
|
"media_analyses",
|
|
"media_collections",
|
|
"media_people",
|
|
"media_face_tracks",
|
|
"media_marks",
|
|
"media_brand_kits",
|
|
"media_projects",
|
|
"media_edit_plans",
|
|
"media_renders",
|
|
"media_publications",
|
|
"media_metrics",
|
|
"media_metric_mappings",
|
|
"media_teaser_models",
|
|
];
|
|
async function counts(spaceId: string) {
|
|
const out: Record<string, number> = {};
|
|
for (const table of tables) {
|
|
const rows = await db.prisma.$queryRawUnsafe<Array<{ count: number }>>(
|
|
`SELECT count(*)::int AS count FROM ${table} WHERE "spaceId" = $1`,
|
|
spaceId,
|
|
);
|
|
out[table] = rows[0]?.count ?? -1;
|
|
}
|
|
const items = await db.prisma.$queryRawUnsafe<Array<{ count: number }>>(
|
|
`SELECT count(*)::int AS count FROM media_collection_items ci
|
|
JOIN media_assets a ON a.id = ci."assetId" WHERE a."spaceId" = $1`,
|
|
spaceId,
|
|
);
|
|
out.media_collection_items = items[0]?.count ?? -1;
|
|
return out;
|
|
}
|
|
const populated = await counts(other.spaceId);
|
|
for (const table of tables) expect(populated[table], table).toBeGreaterThan(0);
|
|
const ownerBefore = await counts(owner.spaceId);
|
|
|
|
const itemCountBefore = await db.prisma.mediaCollectionItem.count();
|
|
await db.prisma.space.delete({ where: { id: other.spaceId } });
|
|
|
|
const after = await counts(other.spaceId);
|
|
for (const [table, count] of Object.entries(after)) expect(count, table).toBe(0);
|
|
expect(await db.prisma.mediaCollectionItem.count()).toBe(
|
|
itemCountBefore - (populated.media_collection_items ?? 0),
|
|
);
|
|
expect(await counts(owner.spaceId)).toEqual(ownerBefore);
|
|
});
|
|
});
|