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! :)
at main 246 lines 9.1 kB view raw
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});