import { randomUUID } from "node:crypto"; import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { coveringGrant, type PlanBinding, planBinding } from "../../src/authorization/grants.ts"; import { createTestSchema, pgAvailable, type TestSchema, uniqueSchema } from "../support/pg.ts"; const hasPg = await pgAvailable(); const now = new Date("2026-01-15T12:00:00Z"); const target: PlanBinding = { payloadHash: "plan-hash", revision: 1, mediaEntityId: randomUUID(), mediaKind: "tv", bytes: 100, }; describe.skipIf(!hasPg)("共用计划绑定与持续授权(真实 PG)", () => { let schema: TestSchema; beforeAll(async () => { schema = await createTestSchema(uniqueSchema("server_test_grants")); }); afterAll(async () => { await schema?.drop(); }); beforeEach(async () => { await schema.db.deleteFrom("capability_invocation").execute(); await schema.db.deleteFrom("automation_grant").execute(); await schema.db.deleteFrom("policy_revision").execute(); await schema.db.deleteFrom("artifact").execute(); }); const grant = (over: Record = {}) => schema.db .insertInto("automation_grant") .values({ action: "download.submit", granted_by: "user:test", ...over }) .returning("id") .executeTakeFirstOrThrow(); const check = (plan = target) => coveringGrant(schema.db, "download.submit", plan, now); it("整理计划绑定读取当前内容与 hash,计划不存在时返回 null", async () => { const p = await schema.db .insertInto("artifact") .values({ kind: "plan", title: "整理计划", content_hash: target.payloadHash, content: JSON.stringify({ mediaEntityId: target.mediaEntityId, mediaKind: target.mediaKind }), }) .returning("id") .executeTakeFirstOrThrow(); expect(await planBinding(schema.db, "organize_plan", p.id)).toEqual({ ...target, bytes: null }); await schema.db.updateTable("artifact").set({ content_hash: "new-hash" }).where("id", "=", p.id).execute(); expect((await planBinding(schema.db, "organize_plan", p.id))?.payloadHash).toBe("new-hash"); expect(await planBinding(schema.db, "organize_plan", randomUUID())).toBeNull(); }); it("授权范围同时检查能力、作品和媒体类型,series 范围覆盖 tv", async () => { const g = await grant({ scope: JSON.stringify({ capabilities: ["download.submit"], mediaEntityIds: [target.mediaEntityId], mediaKinds: ["series"], }), }); expect(await check()).toEqual({ grantId: g.id }); expect(await check({ ...target, mediaEntityId: randomUUID() })).toMatchObject({ reasons: [expect.any(String)] }); expect(await check({ ...target, mediaKind: "movie" })).toMatchObject({ reasons: [expect.any(String)] }); await schema.db .updateTable("automation_grant") .set({ scope: JSON.stringify({ capabilities: ["organize.execute"] }) }) .where("id", "=", g.id) .execute(); expect(await check()).toMatchObject({ reasons: [expect.any(String)] }); }); it.each(["expired", "revoked"] as const)("%s 授权不放行", async (state) => { await grant( state === "expired" ? { expires_at: new Date(now.getTime() - 1) } : { status: "revoked", revoked_at: now }, ); expect(await check()).toEqual({ reasons: [] }); }); it("策略被替代后,绑定该策略的持续授权不再放行", async () => { const p = await schema.db .insertInto("policy_revision") .values({ policy_key: "test-acquisition", revision: 1, kind: "acquisition", title: "测试策略", status: "published", }) .returning("id") .executeTakeFirstOrThrow(); const g = await grant({ action: "policy.acquisition", policy_revision_id: p.id }); expect(await check()).toEqual({ grantId: g.id }); await schema.db.updateTable("policy_revision").set({ status: "superseded" }).where("id", "=", p.id).execute(); expect(await check()).toMatchObject({ reasons: [expect.stringContaining("已不是发布状态")] }); }); it("每日次数只统计成功调用,达到预算后拒绝", async () => { const g = await grant({ budget: JSON.stringify({ maxInvocationsPerDay: 2 }) }); const invoke = (status: "failed" | "succeeded") => schema.db .insertInto("capability_invocation") .values({ idempotency_key: randomUUID(), capability_id: "download.submit", capability_version: 1, actor_id: "user:test", source: "web", status, grant_id: g.id, created_at: now, }) .execute(); await invoke("succeeded"); await invoke("failed"); expect(await check()).toEqual({ grantId: g.id }); await invoke("succeeded"); expect(await check()).toMatchObject({ reasons: [expect.stringContaining("次数预算已用尽")] }); }); it("提交下载时把本次预留字节计入预算", async () => { const g = await grant({ budget: JSON.stringify({ maxBytesPerDay: 100 }) }); expect(await check()).toEqual({ grantId: g.id }); expect(await check({ ...target, bytes: 101 })).toMatchObject({ reasons: [expect.stringContaining("字节预算不足")], }); }); });