atproto user agency toolkit for individuals and groups
1import { describe, it, expect, beforeEach, afterEach } from "vitest";
2import { mkdtempSync, rmSync } from "node:fs";
3import { tmpdir } from "node:os";
4import { join } from "node:path";
5import Database from "better-sqlite3";
6
7import { PolicyStorage } from "./storage.js";
8import type { StoredPolicy } from "./types.js";
9import { toStoredPolicy } from "./types.js";
10import {
11 DEFAULT_REPLICATION,
12 DEFAULT_SYNC,
13 DEFAULT_RETENTION,
14} from "./types.js";
15import type { Policy } from "./types.js";
16
17function makePolicy(overrides: Partial<Policy> & { id: string }): Policy {
18 return {
19 name: overrides.id,
20 target: { type: "list", dids: ["did:plc:test"] },
21 replication: { ...DEFAULT_REPLICATION },
22 sync: { ...DEFAULT_SYNC },
23 retention: { ...DEFAULT_RETENTION },
24 priority: 50,
25 enabled: true,
26 ...overrides,
27 };
28}
29
30function makeStoredPolicy(id: string, did: string = "did:plc:test"): StoredPolicy {
31 return toStoredPolicy(
32 makePolicy({ id, target: { type: "list", dids: [did] } }),
33 "archive",
34 { source: "manual", createdBy: "test" },
35 );
36}
37
38describe("PolicyStorage", () => {
39 let tmpDir: string;
40 let db: InstanceType<typeof Database>;
41 let storage: PolicyStorage;
42
43 beforeEach(() => {
44 tmpDir = mkdtempSync(join(tmpdir(), "policy-storage-test-"));
45 db = new Database(join(tmpDir, "test.db"));
46 storage = new PolicyStorage(db);
47 storage.initSchema();
48 });
49
50 afterEach(() => {
51 db.close();
52 rmSync(tmpDir, { recursive: true, force: true });
53 });
54
55 describe("round-trip", () => {
56 it("upsert and load a policy", () => {
57 const policy = makeStoredPolicy("test-1", "did:plc:alice");
58 storage.upsertPolicy(policy);
59
60 const loaded = storage.getPolicy("test-1");
61 expect(loaded).not.toBeNull();
62 expect(loaded!.id).toBe("test-1");
63 expect(loaded!.type).toBe("archive");
64 expect(loaded!.state).toBe("active");
65 expect(loaded!.source).toBe("manual");
66 expect(loaded!.consent).toBe("unconsented");
67 expect(loaded!.createdBy).toBe("test");
68 expect(loaded!.target).toEqual({ type: "list", dids: ["did:plc:alice"] });
69 expect(loaded!.replication).toEqual(DEFAULT_REPLICATION);
70 expect(loaded!.sync).toEqual(DEFAULT_SYNC);
71 expect(loaded!.retention).toEqual(DEFAULT_RETENTION);
72 expect(loaded!.priority).toBe(50);
73 expect(loaded!.enabled).toBe(true);
74 });
75
76 it("upsert updates an existing policy", () => {
77 const policy = makeStoredPolicy("test-1");
78 storage.upsertPolicy(policy);
79
80 const updated = { ...policy, name: "Updated Name", priority: 99 };
81 storage.upsertPolicy(updated);
82
83 const loaded = storage.getPolicy("test-1");
84 expect(loaded!.name).toBe("Updated Name");
85 expect(loaded!.priority).toBe(99);
86 });
87
88 it("preserves JSON fields correctly", () => {
89 const policy = toStoredPolicy(
90 makePolicy({
91 id: "json-test",
92 replication: { minCopies: 3, preferredPeers: ["did:plc:peer1"] },
93 retention: { maxAgeSec: 86400, keepHistory: true },
94 }),
95 "saas",
96 );
97 storage.upsertPolicy(policy);
98
99 const loaded = storage.getPolicy("json-test")!;
100 expect(loaded.replication.minCopies).toBe(3);
101 expect(loaded.replication.preferredPeers).toEqual(["did:plc:peer1"]);
102 expect(loaded.retention.maxAgeSec).toBe(86400);
103 expect(loaded.retention.keepHistory).toBe(true);
104 });
105 });
106
107 describe("loadPolicies", () => {
108 it("loads all policies when no state filter", () => {
109 storage.upsertPolicy(makeStoredPolicy("p1"));
110 storage.upsertPolicy(makeStoredPolicy("p2"));
111 const all = storage.loadPolicies();
112 expect(all).toHaveLength(2);
113 });
114
115 it("filters by state", () => {
116 const active = makeStoredPolicy("active");
117 const suspended = { ...makeStoredPolicy("suspended"), state: "suspended" as const };
118 storage.upsertPolicy(active);
119 storage.upsertPolicy(suspended);
120
121 const result = storage.loadPolicies(["active"]);
122 expect(result).toHaveLength(1);
123 expect(result[0]!.id).toBe("active");
124 });
125
126 it("loadActivePolicies returns only active", () => {
127 storage.upsertPolicy(makeStoredPolicy("a1"));
128 storage.upsertPolicy({ ...makeStoredPolicy("t1"), state: "terminated" });
129 const active = storage.loadActivePolicies();
130 expect(active).toHaveLength(1);
131 expect(active[0]!.id).toBe("a1");
132 });
133 });
134
135 describe("state transitions", () => {
136 it("transitions active → suspended", () => {
137 storage.upsertPolicy(makeStoredPolicy("p1"));
138 const result = storage.transitionState("p1", "suspended");
139 expect(result).toBe(true);
140
141 const loaded = storage.getPolicy("p1")!;
142 expect(loaded.state).toBe("suspended");
143 expect(loaded.suspendedAt).not.toBeNull();
144 });
145
146 it("transitions to terminated sets terminatedAt", () => {
147 storage.upsertPolicy(makeStoredPolicy("p1"));
148 storage.transitionState("p1", "terminated");
149 const loaded = storage.getPolicy("p1")!;
150 expect(loaded.state).toBe("terminated");
151 expect(loaded.terminatedAt).not.toBeNull();
152 });
153
154 it("transitions to active sets activatedAt", () => {
155 const policy = { ...makeStoredPolicy("p1"), state: "suspended" as const };
156 storage.upsertPolicy(policy);
157 storage.transitionState("p1", "active");
158 const loaded = storage.getPolicy("p1")!;
159 expect(loaded.state).toBe("active");
160 expect(loaded.activatedAt).not.toBeNull();
161 });
162
163 it("returns false for non-existent policy", () => {
164 expect(storage.transitionState("nope", "suspended")).toBe(false);
165 });
166 });
167
168 describe("deletePolicy", () => {
169 it("deletes and returns true", () => {
170 storage.upsertPolicy(makeStoredPolicy("p1"));
171 expect(storage.deletePolicy("p1")).toBe(true);
172 expect(storage.getPolicy("p1")).toBeNull();
173 });
174
175 it("returns false for non-existent", () => {
176 expect(storage.deletePolicy("nope")).toBe(false);
177 });
178 });
179
180 describe("hasPolicy", () => {
181 it("returns true for existing", () => {
182 storage.upsertPolicy(makeStoredPolicy("p1"));
183 expect(storage.hasPolicy("p1")).toBe(true);
184 });
185
186 it("returns false for non-existent", () => {
187 expect(storage.hasPolicy("nope")).toBe(false);
188 });
189 });
190
191 describe("count", () => {
192 it("counts all policies", () => {
193 storage.upsertPolicy(makeStoredPolicy("p1"));
194 storage.upsertPolicy(makeStoredPolicy("p2"));
195 expect(storage.count()).toBe(2);
196 });
197
198 it("counts by state", () => {
199 storage.upsertPolicy(makeStoredPolicy("p1"));
200 storage.upsertPolicy({ ...makeStoredPolicy("p2"), state: "terminated" });
201 expect(storage.count("active")).toBe(1);
202 expect(storage.count("terminated")).toBe(1);
203 });
204 });
205});