Suite of AT Protocol TypeScript libraries built on web standards
1import { assertEquals, assertThrows } from "@std/assert";
2import {
3 ensureValidHandle,
4 ensureValidHandleRegex,
5 InvalidHandleError,
6 normalizeAndEnsureValidHandle,
7} from "../mod.ts";
8
9Deno.test("validation allows valid handles", () => {
10 const expectValid = (h: string) => {
11 ensureValidHandle(h);
12 ensureValidHandleRegex(h);
13 };
14
15 expectValid("A.ISI.EDU");
16 expectValid("XX.LCS.MIT.EDU");
17 expectValid("SRI-NIC.ARPA");
18 expectValid("john.test");
19 expectValid("jan.test");
20 expectValid("a234567890123456789.test");
21 expectValid("john2.test");
22 expectValid("john-john.test");
23 expectValid("john.bsky.app");
24 expectValid("jo.hn");
25 expectValid("a.co");
26 expectValid("a.org");
27 expectValid("joh.n");
28 expectValid("j0.h0");
29 const longHandle = "shoooort" + ".loooooooooooooooooooooooooong".repeat(8) +
30 ".test";
31 assertEquals(longHandle.length, 253);
32 expectValid(longHandle);
33 expectValid("short." + "o".repeat(63) + ".test");
34 expectValid("jaymome-johnber123456.test");
35 expectValid("jay.mome-johnber123456.test");
36 expectValid("john.test.bsky.app");
37
38 // NOTE: this probably isn't ever going to be a real domain, but my read of
39 // the RFC is that it would be possible
40 expectValid("john.t");
41});
42
43// NOTE: we may change this at the proto level; currently only disallowed at
44// the registration level
45Deno.test("validation allows .local and .arpa handles (proto-level)", () => {
46 const expectValid = (h: string) => {
47 ensureValidHandle(h);
48 ensureValidHandleRegex(h);
49 };
50
51 expectValid("laptop.local");
52 expectValid("laptop.arpa");
53});
54
55Deno.test("validation allows punycode handles", () => {
56 const expectValid = (h: string) => {
57 ensureValidHandle(h);
58 ensureValidHandleRegex(h);
59 };
60
61 expectValid("xn--ls8h.test"); // 💩.test
62 expectValid("xn--bcher-kva.tld"); // bücher.tld
63 expectValid("xn--3jk.com");
64 expectValid("xn--w3d.com");
65 expectValid("xn--vqb.com");
66 expectValid("xn--ppd.com");
67 expectValid("xn--cs9a.com");
68 expectValid("xn--8r9a.com");
69 expectValid("xn--cfd.com");
70 expectValid("xn--5jk.com");
71 expectValid("xn--2lb.com");
72});
73
74Deno.test("validation allows onion (Tor) handles", () => {
75 const expectValid = (h: string) => {
76 ensureValidHandle(h);
77 ensureValidHandleRegex(h);
78 };
79
80 expectValid("expyuzz4wqqyqhjn.onion");
81 expectValid("friend.expyuzz4wqqyqhjn.onion");
82 expectValid(
83 "g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion",
84 );
85 expectValid(
86 "friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion",
87 );
88 expectValid(
89 "friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion",
90 );
91 expectValid(
92 "2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion",
93 );
94 expectValid(
95 "friend.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion",
96 );
97});
98
99Deno.test("validation throws on invalid handles", () => {
100 const expectInvalid = (h: string) => {
101 assertThrows(() => ensureValidHandle(h), InvalidHandleError);
102 assertThrows(() => ensureValidHandleRegex(h), InvalidHandleError);
103 };
104
105 expectInvalid("did:thing.test");
106 expectInvalid("did:thing");
107 expectInvalid("john-.test");
108 expectInvalid("john.0");
109 expectInvalid("john.-");
110 expectInvalid("short." + "o".repeat(64) + ".test");
111 expectInvalid("short" + ".loooooooooooooooooooooooong".repeat(10) + ".test");
112 const longHandle = "shooooort" + ".loooooooooooooooooooooooooong".repeat(8) +
113 ".test";
114 assertEquals(longHandle.length, 254);
115 expectInvalid(longHandle);
116 expectInvalid("xn--bcher-.tld");
117 expectInvalid("john..test");
118 expectInvalid("jo_hn.test");
119 expectInvalid("-john.test");
120 expectInvalid(".john.test");
121 expectInvalid("jo!hn.test");
122 expectInvalid("jo%hn.test");
123 expectInvalid("jo&hn.test");
124 expectInvalid("jo@hn.test");
125 expectInvalid("jo*hn.test");
126 expectInvalid("jo|hn.test");
127 expectInvalid("jo:hn.test");
128 expectInvalid("jo/hn.test");
129 expectInvalid("john💩.test");
130 expectInvalid("bücher.test");
131 expectInvalid("john .test");
132 expectInvalid("john.test.");
133 expectInvalid("john");
134 expectInvalid("john.");
135 expectInvalid(".john");
136 expectInvalid("john.test.");
137 expectInvalid(".john.test");
138 expectInvalid(" john.test");
139 expectInvalid("john.test ");
140 expectInvalid("joh-.test");
141 expectInvalid("john.-est");
142 expectInvalid("john.tes-");
143});
144
145Deno.test("validation throws on 'dotless' TLD handles", () => {
146 const expectInvalid = (h: string) => {
147 assertThrows(() => ensureValidHandle(h), InvalidHandleError);
148 assertThrows(() => ensureValidHandleRegex(h), InvalidHandleError);
149 };
150
151 expectInvalid("org");
152 expectInvalid("ai");
153 expectInvalid("gg");
154 expectInvalid("io");
155});
156
157Deno.test("validation correctly validates corner cases (modern vs. old RFCs)", () => {
158 const expectValid = (h: string) => {
159 ensureValidHandle(h);
160 ensureValidHandleRegex(h);
161 };
162 const expectInvalid = (h: string) => {
163 assertThrows(() => ensureValidHandle(h), InvalidHandleError);
164 assertThrows(() => ensureValidHandleRegex(h), InvalidHandleError);
165 };
166
167 expectValid("12345.test");
168 expectValid("8.cn");
169 expectValid("4chan.org");
170 expectValid("4chan.o-g");
171 expectValid("blah.4chan.org");
172 expectValid("thing.a01");
173 expectValid("120.0.0.1.com");
174 expectValid("0john.test");
175 expectValid("9sta--ck.com");
176 expectValid("99stack.com");
177 expectValid("0ohn.test");
178 expectValid("john.t--t");
179 expectValid("thing.0aa.thing");
180
181 expectInvalid("cn.8");
182 expectInvalid("thing.0aa");
183 expectInvalid("thing.0aa");
184});
185
186Deno.test("validation does not allow IP addresses as handles", () => {
187 const expectInvalid = (h: string) => {
188 assertThrows(() => ensureValidHandle(h), InvalidHandleError);
189 assertThrows(() => ensureValidHandleRegex(h), InvalidHandleError);
190 };
191
192 expectInvalid("127.0.0.1");
193 expectInvalid("192.168.0.142");
194 expectInvalid("fe80::7325:8a97:c100:94b");
195 expectInvalid("2600:3c03::f03c:9100:feb0:af1f");
196});
197
198Deno.test("validation is consistent with examples from stackoverflow", () => {
199 const expectValid = (h: string) => {
200 ensureValidHandle(h);
201 ensureValidHandleRegex(h);
202 };
203 const expectInvalid = (h: string) => {
204 assertThrows(() => ensureValidHandle(h), InvalidHandleError);
205 assertThrows(() => ensureValidHandleRegex(h), InvalidHandleError);
206 };
207
208 const okStackoverflow = [
209 "stack.com",
210 "sta-ck.com",
211 "sta---ck.com",
212 "sta--ck9.com",
213 "stack99.com",
214 "sta99ck.com",
215 "google.com.uk",
216 "google.co.in",
217 "google.com",
218 "maselkowski.pl",
219 "m.maselkowski.pl",
220 "xn--masekowski-d0b.pl",
221 "xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s",
222 "xn--stackoverflow.com",
223 "stackoverflow.xn--com",
224 "stackoverflow.co.uk",
225 "xn--masekowski-d0b.pl",
226 "xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s",
227 ];
228 okStackoverflow.forEach(expectValid);
229
230 const badStackoverflow = [
231 "-notvalid.at-all",
232 "-thing.com",
233 "www.masełkowski.pl.com",
234 ];
235 badStackoverflow.forEach(expectInvalid);
236});
237
238Deno.test("validation conforms to interop valid handles", async () => {
239 const expectValid = (h: string) => {
240 ensureValidHandle(h);
241 ensureValidHandleRegex(h);
242 };
243
244 const filePath =
245 new URL("./interop/handle_syntax_valid.txt", import.meta.url).pathname;
246 const fileContent = await Deno.readTextFile(filePath);
247 const lines = fileContent.split("\n");
248
249 for (const line of lines) {
250 if (line.startsWith("#") || line.length === 0) {
251 continue;
252 }
253 expectValid(line);
254 }
255});
256
257Deno.test("validation conforms to interop invalid handles", async () => {
258 const expectInvalid = (h: string) => {
259 assertThrows(() => ensureValidHandle(h), InvalidHandleError);
260 assertThrows(() => ensureValidHandleRegex(h), InvalidHandleError);
261 };
262
263 const filePath =
264 new URL("./interop/handle_syntax_invalid.txt", import.meta.url).pathname;
265 const fileContent = await Deno.readTextFile(filePath);
266 const lines = fileContent.split("\n");
267
268 for (const line of lines) {
269 if (line.startsWith("#") || line.length === 0) {
270 continue;
271 }
272 expectInvalid(line);
273 }
274});
275
276Deno.test("validation conforms to interop invalid handles", async () => {
277 const expectInvalid = (h: string) => {
278 assertThrows(() => ensureValidHandle(h), InvalidHandleError);
279 assertThrows(() => ensureValidHandleRegex(h), InvalidHandleError);
280 };
281
282 const filePath =
283 new URL("./interop/handle_syntax_invalid.txt", import.meta.url).pathname;
284 const fileContent = await Deno.readTextFile(filePath);
285 const lines = fileContent.split("\n");
286
287 for (const line of lines) {
288 if (line.startsWith("#") || line.length === 0) {
289 continue;
290 }
291 expectInvalid(line);
292 }
293});
294
295Deno.test("normalization - normalizes handles", () => {
296 const normalized = normalizeAndEnsureValidHandle("JoHn.TeST");
297 assertEquals(normalized, "john.test");
298});
299
300Deno.test("normalization throws on invalid normalized handles", () => {
301 assertThrows(
302 () => normalizeAndEnsureValidHandle("JoH!n.TeST"),
303 InvalidHandleError,
304 );
305});