Files
UHD_Media_Tool/apps/server/test/authorization/grants.test.ts
T

131 lines
5.2 KiB
TypeScript

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<string, unknown> = {}) =>
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("字节预算不足")],
});
});
});