WIP: A simple cli for daily tangled use cases and AI integration. This is for my personal use right now, but happy if others get mileage from it! :)
1import { describe, expect, it } from 'vitest';
2import {
3 isValidAtUri,
4 isValidHandle,
5 isValidTangledDid,
6 safeValidateDid,
7 safeValidateHandle,
8 safeValidateIdentifier,
9 validateAppPassword,
10 validateDid,
11 validateHandle,
12 validateIdentifier,
13 validateIssueBody,
14 validateIssueTitle,
15} from '../../src/utils/validation.js';
16
17describe('Handle Validation', () => {
18 describe('validateHandle', () => {
19 it('should accept standard Bluesky handles', () => {
20 expect(validateHandle('user.bsky.social')).toBe('user.bsky.social');
21 expect(validateHandle('test.bsky.social')).toBe('test.bsky.social');
22 });
23
24 it('should accept custom domain handles', () => {
25 expect(validateHandle('markbennett.ca')).toBe('markbennett.ca');
26 expect(validateHandle('example.com')).toBe('example.com');
27 expect(validateHandle('subdomain.example.com')).toBe('subdomain.example.com');
28 });
29
30 it('should reject invalid handles', () => {
31 expect(() => validateHandle('')).toThrow('Handle cannot be empty');
32 expect(() => validateHandle('invalid')).toThrow('Invalid handle format');
33 expect(() => validateHandle('invalid..com')).toThrow('Invalid handle format');
34 expect(() => validateHandle('.example.com')).toThrow('Invalid handle format');
35 expect(() => validateHandle('example.com.')).toThrow('Invalid handle format');
36 });
37 });
38
39 describe('safeValidateHandle', () => {
40 it('should return success for valid handles', () => {
41 const result = safeValidateHandle('user.bsky.social');
42 expect(result.success).toBe(true);
43 if (result.success) {
44 expect(result.data).toBe('user.bsky.social');
45 }
46 });
47
48 it('should return error for invalid handles', () => {
49 const result = safeValidateHandle('invalid');
50 expect(result.success).toBe(false);
51 if (!result.success) {
52 expect(result.error).toContain('Invalid handle format');
53 }
54 });
55 });
56});
57
58describe('DID Validation', () => {
59 describe('validateDid', () => {
60 it('should accept valid DIDs', () => {
61 expect(validateDid('did:plc:test123')).toBe('did:plc:test123');
62 expect(validateDid('did:web:example.com')).toBe('did:web:example.com');
63 expect(validateDid('did:key:z6MkhaXg')).toBe('did:key:z6MkhaXg');
64 });
65
66 it('should reject invalid DIDs', () => {
67 expect(() => validateDid('')).toThrow('DID cannot be empty');
68 expect(() => validateDid('not-a-did')).toThrow('Invalid DID format');
69 expect(() => validateDid('did:')).toThrow('Invalid DID format');
70 expect(() => validateDid('did:plc:')).toThrow('Invalid DID format');
71 });
72 });
73
74 describe('safeValidateDid', () => {
75 it('should return success for valid DIDs', () => {
76 const result = safeValidateDid('did:plc:test123');
77 expect(result.success).toBe(true);
78 if (result.success) {
79 expect(result.data).toBe('did:plc:test123');
80 }
81 });
82
83 it('should return error for invalid DIDs', () => {
84 const result = safeValidateDid('not-a-did');
85 expect(result.success).toBe(false);
86 if (!result.success) {
87 expect(result.error).toContain('Invalid DID format');
88 }
89 });
90 });
91});
92
93describe('Identifier Validation', () => {
94 describe('validateIdentifier', () => {
95 it('should accept valid handles', () => {
96 expect(validateIdentifier('user.bsky.social')).toBe('user.bsky.social');
97 expect(validateIdentifier('example.com')).toBe('example.com');
98 });
99
100 it('should accept valid DIDs', () => {
101 expect(validateIdentifier('did:plc:test123')).toBe('did:plc:test123');
102 expect(validateIdentifier('did:web:example.com')).toBe('did:web:example.com');
103 });
104
105 it('should reject invalid identifiers', () => {
106 expect(() => validateIdentifier('')).toThrow();
107 expect(() => validateIdentifier('invalid')).toThrow();
108 });
109 });
110
111 describe('safeValidateIdentifier', () => {
112 it('should return success for valid identifiers', () => {
113 expect(safeValidateIdentifier('user.bsky.social').success).toBe(true);
114 expect(safeValidateIdentifier('did:plc:test123').success).toBe(true);
115 });
116
117 it('should return error for invalid identifiers', () => {
118 const result = safeValidateIdentifier('invalid');
119 expect(result.success).toBe(false);
120 if (!result.success) {
121 expect(result.error).toBeTruthy();
122 }
123 });
124 });
125});
126
127describe('App Password Validation', () => {
128 describe('validateAppPassword', () => {
129 it('should accept valid passwords', () => {
130 expect(validateAppPassword('password123')).toBe('password123');
131 expect(validateAppPassword('xxxx-xxxx-xxxx-xxxx')).toBe('xxxx-xxxx-xxxx-xxxx');
132 expect(validateAppPassword('a'.repeat(100))).toBe('a'.repeat(100));
133 });
134
135 it('should reject empty passwords', () => {
136 expect(() => validateAppPassword('')).toThrow('Password cannot be empty');
137 });
138
139 it('should reject extremely long passwords', () => {
140 expect(() => validateAppPassword('a'.repeat(1001))).toThrow('Password is too long');
141 });
142 });
143});
144
145describe('Boolean Validation Helpers', () => {
146 describe('isValidHandle', () => {
147 it('should return true for valid handles', () => {
148 expect(isValidHandle('user.bsky.social')).toBe(true);
149 expect(isValidHandle('markbennett.ca')).toBe(true);
150 expect(isValidHandle('sub.domain.example.com')).toBe(true);
151 });
152
153 it('should return false for invalid handles', () => {
154 expect(isValidHandle('invalid')).toBe(false);
155 expect(isValidHandle('.starts-with-dot.com')).toBe(false);
156 expect(isValidHandle('ends-with-dot.com.')).toBe(false);
157 expect(isValidHandle('has space.com')).toBe(false);
158 expect(isValidHandle('')).toBe(false);
159 });
160 });
161
162 describe('isValidTangledDid', () => {
163 it('should return true for valid Tangled DIDs (did:plc: format)', () => {
164 expect(isValidTangledDid('did:plc:b2mcbcamkwyznc5fkplwlxbf')).toBe(true);
165 expect(isValidTangledDid('did:plc:abc123xyz')).toBe(true);
166 });
167
168 it('should return false for invalid Tangled DIDs', () => {
169 expect(isValidTangledDid('did:plc:')).toBe(false);
170 expect(isValidTangledDid('did:plc:ABC123')).toBe(false); // uppercase not allowed
171 expect(isValidTangledDid('did:web:example.com')).toBe(false); // wrong method
172 expect(isValidTangledDid('not-a-did')).toBe(false);
173 expect(isValidTangledDid('')).toBe(false);
174 });
175 });
176});
177
178describe('Issue Validation', () => {
179 describe('validateIssueTitle', () => {
180 it('should accept valid issue titles', () => {
181 expect(validateIssueTitle('Bug: Fix login error')).toBe('Bug: Fix login error');
182 expect(validateIssueTitle('Feature: Add dark mode')).toBe('Feature: Add dark mode');
183 expect(validateIssueTitle('A')).toBe('A'); // minimum length
184 });
185
186 it('should accept titles up to 256 characters', () => {
187 const longTitle = 'A'.repeat(256);
188 expect(validateIssueTitle(longTitle)).toBe(longTitle);
189 });
190
191 it('should reject empty titles', () => {
192 expect(() => validateIssueTitle('')).toThrow('Issue title cannot be empty');
193 });
194
195 it('should reject titles over 256 characters', () => {
196 const tooLong = 'A'.repeat(257);
197 expect(() => validateIssueTitle(tooLong)).toThrow(
198 'Issue title must be 256 characters or less'
199 );
200 });
201 });
202
203 describe('validateIssueBody', () => {
204 it('should accept valid issue bodies', () => {
205 expect(validateIssueBody('This is a description')).toBe('This is a description');
206 expect(validateIssueBody('Multi\nline\ndescription')).toBe('Multi\nline\ndescription');
207 });
208
209 it('should accept bodies up to 50,000 characters', () => {
210 const longBody = 'A'.repeat(50000);
211 expect(validateIssueBody(longBody)).toBe(longBody);
212 });
213
214 it('should accept empty string', () => {
215 expect(validateIssueBody('')).toBe('');
216 });
217
218 it('should reject bodies over 50,000 characters', () => {
219 const tooLong = 'A'.repeat(50001);
220 expect(() => validateIssueBody(tooLong)).toThrow(
221 'Issue body must be 50,000 characters or less'
222 );
223 });
224 });
225});
226
227describe('AT-URI Validation', () => {
228 describe('isValidAtUri', () => {
229 it('should return true for valid AT-URIs', () => {
230 expect(isValidAtUri('at://did:plc:abc123/sh.tangled.repo/my-repo')).toBe(true);
231 expect(isValidAtUri('at://did:plc:abc123/sh.tangled.repo.issue/xyz789')).toBe(true);
232 expect(isValidAtUri('at://did:web:example.com/collection')).toBe(true);
233 });
234
235 it('should return true for AT-URIs without rkey', () => {
236 expect(isValidAtUri('at://did:plc:abc123/collection')).toBe(true);
237 });
238
239 it('should return false for invalid AT-URIs', () => {
240 expect(isValidAtUri('http://example.com')).toBe(false);
241 expect(isValidAtUri('at://not-a-did/collection')).toBe(false);
242 expect(isValidAtUri('at://did:plc:abc/invalid collection')).toBe(false);
243 expect(isValidAtUri('')).toBe(false);
244 });
245 });
246});