131 lines
5.2 KiB
TypeScript
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("字节预算不足")],
|
|
});
|
|
});
|
|
});
|