atproto user agency toolkit for individuals and groups
1import { describe, it, expect, beforeAll } from "vitest";
2import {
3 loadP2pdsLexicons,
4 getLexicon,
5 validateP2pdsRecord,
6} from "./lexicons.js";
7
8describe("lexicons", () => {
9 beforeAll(() => {
10 loadP2pdsLexicons();
11 });
12
13 describe("loadP2pdsLexicons", () => {
14 it("loads peer and offer lexicons", () => {
15 const peer = getLexicon("org.p2pds.peer");
16 const offer = getLexicon("org.p2pds.replication.offer");
17
18 expect(peer).toBeDefined();
19 expect(peer!.id).toBe("org.p2pds.peer");
20 expect(peer!.lexicon).toBe(1);
21 expect(peer!.defs.main.type).toBe("record");
22
23 expect(offer).toBeDefined();
24 expect(offer!.id).toBe("org.p2pds.replication.offer");
25 expect(offer!.defs.main.type).toBe("record");
26 });
27
28 it("peer lexicon has correct schema", () => {
29 const peer = getLexicon("org.p2pds.peer")!;
30 const props = peer.defs.main.record.properties;
31 const required = peer.defs.main.record.required;
32
33 expect(required).toContain("peerId");
34 expect(required).toContain("multiaddrs");
35 expect(required).toContain("createdAt");
36
37 expect(props.peerId.type).toBe("string");
38 expect(props.multiaddrs.type).toBe("array");
39 expect(props.createdAt.type).toBe("string");
40 expect(props.createdAt.format).toBe("datetime");
41 });
42
43 it("offer lexicon has correct schema", () => {
44 const offer = getLexicon("org.p2pds.replication.offer")!;
45 const props = offer.defs.main.record.properties;
46 const required = offer.defs.main.record.required;
47
48 expect(required).toContain("subject");
49 expect(required).toContain("minCopies");
50 expect(required).toContain("intervalSec");
51 expect(required).toContain("priority");
52 expect(required).toContain("createdAt");
53
54 expect(props.subject.type).toBe("string");
55 expect(props.subject.format).toBe("did");
56 expect(props.minCopies.type).toBe("integer");
57 expect(props.priority.maximum).toBe(100);
58 });
59 });
60
61 describe("validateP2pdsRecord — org.p2pds.peer", () => {
62 const validPeer = {
63 $type: "org.p2pds.peer",
64 peerId: "12D3KooWNvSZnPi3RrhrTwEY4LuuBeB6K6facKUCJcyWG1aoDd2p",
65 multiaddrs: ["/ip4/127.0.0.1/tcp/4001"],
66 createdAt: "2026-01-15T10:30:00.000Z",
67 };
68
69 it("accepts a valid peer record", () => {
70 expect(() =>
71 validateP2pdsRecord("org.p2pds.peer", validPeer),
72 ).not.toThrow();
73 });
74
75 it("rejects missing peerId", () => {
76 const { peerId: _, ...bad } = validPeer;
77 expect(() =>
78 validateP2pdsRecord("org.p2pds.peer", bad),
79 ).toThrow("Missing required field: peerId");
80 });
81
82 it("rejects missing multiaddrs", () => {
83 const { multiaddrs: _, ...bad } = validPeer;
84 expect(() =>
85 validateP2pdsRecord("org.p2pds.peer", bad),
86 ).toThrow("Missing required field: multiaddrs");
87 });
88
89 it("rejects missing createdAt", () => {
90 const { createdAt: _, ...bad } = validPeer;
91 expect(() =>
92 validateP2pdsRecord("org.p2pds.peer", bad),
93 ).toThrow("Missing required field: createdAt");
94 });
95
96 it("rejects non-string peerId", () => {
97 expect(() =>
98 validateP2pdsRecord("org.p2pds.peer", {
99 ...validPeer,
100 peerId: 123,
101 }),
102 ).toThrow("must be a string");
103 });
104
105 it("rejects non-array multiaddrs", () => {
106 expect(() =>
107 validateP2pdsRecord("org.p2pds.peer", {
108 ...validPeer,
109 multiaddrs: "not-an-array",
110 }),
111 ).toThrow("must be an array");
112 });
113
114 it("rejects invalid datetime", () => {
115 expect(() =>
116 validateP2pdsRecord("org.p2pds.peer", {
117 ...validPeer,
118 createdAt: "not-a-date",
119 }),
120 ).toThrow("valid datetime");
121 });
122
123 it("accepts empty multiaddrs array", () => {
124 expect(() =>
125 validateP2pdsRecord("org.p2pds.peer", {
126 ...validPeer,
127 multiaddrs: [],
128 }),
129 ).not.toThrow();
130 });
131
132 it("rejects non-string items in multiaddrs", () => {
133 expect(() =>
134 validateP2pdsRecord("org.p2pds.peer", {
135 ...validPeer,
136 multiaddrs: [123],
137 }),
138 ).toThrow("must be a string");
139 });
140 });
141
142 describe("validateP2pdsRecord — org.p2pds.replication.offer", () => {
143 const validOffer = {
144 $type: "org.p2pds.replication.offer",
145 subject: "did:plc:abc123",
146 minCopies: 2,
147 intervalSec: 600,
148 priority: 50,
149 createdAt: "2026-01-15T10:30:00.000Z",
150 };
151
152 it("accepts a valid offer record", () => {
153 expect(() =>
154 validateP2pdsRecord("org.p2pds.replication.offer", validOffer),
155 ).not.toThrow();
156 });
157
158 it("rejects missing subject", () => {
159 const { subject: _, ...bad } = validOffer;
160 expect(() =>
161 validateP2pdsRecord("org.p2pds.replication.offer", bad),
162 ).toThrow("Missing required field: subject");
163 });
164
165 it("rejects non-DID subject", () => {
166 expect(() =>
167 validateP2pdsRecord("org.p2pds.replication.offer", {
168 ...validOffer,
169 subject: "not-a-did",
170 }),
171 ).toThrow("must be a valid DID");
172 });
173
174 it("rejects non-integer minCopies", () => {
175 expect(() =>
176 validateP2pdsRecord("org.p2pds.replication.offer", {
177 ...validOffer,
178 minCopies: 1.5,
179 }),
180 ).toThrow("must be an integer");
181 });
182
183 it("rejects minCopies below minimum", () => {
184 expect(() =>
185 validateP2pdsRecord("org.p2pds.replication.offer", {
186 ...validOffer,
187 minCopies: 0,
188 }),
189 ).toThrow(">= 1");
190 });
191
192 it("rejects priority above maximum", () => {
193 expect(() =>
194 validateP2pdsRecord("org.p2pds.replication.offer", {
195 ...validOffer,
196 priority: 101,
197 }),
198 ).toThrow("<= 100");
199 });
200
201 it("rejects priority below minimum", () => {
202 expect(() =>
203 validateP2pdsRecord("org.p2pds.replication.offer", {
204 ...validOffer,
205 priority: -1,
206 }),
207 ).toThrow(">= 0");
208 });
209
210 it("accepts boundary values", () => {
211 expect(() =>
212 validateP2pdsRecord("org.p2pds.replication.offer", {
213 ...validOffer,
214 minCopies: 1,
215 priority: 0,
216 intervalSec: 1,
217 }),
218 ).not.toThrow();
219
220 expect(() =>
221 validateP2pdsRecord("org.p2pds.replication.offer", {
222 ...validOffer,
223 priority: 100,
224 }),
225 ).not.toThrow();
226 });
227 });
228
229 describe("validateP2pdsRecord — unknown NSID", () => {
230 it("throws for unknown NSID", () => {
231 expect(() =>
232 validateP2pdsRecord("org.p2pds.unknown", {}),
233 ).toThrow("No lexicon loaded for NSID");
234 });
235 });
236
237 describe("validateP2pdsRecord — non-object", () => {
238 it("rejects null", () => {
239 expect(() =>
240 validateP2pdsRecord("org.p2pds.peer", null),
241 ).toThrow("must be an object");
242 });
243
244 it("rejects string", () => {
245 expect(() =>
246 validateP2pdsRecord("org.p2pds.peer", "string"),
247 ).toThrow("must be an object");
248 });
249 });
250});