Coves frontend - a photon fork
at main 394 lines 12 kB view raw
1import { describe, it, expect, vi } from 'vitest' 2import { 3 asDID, 4 asHandle, 5 asInstanceURL, 6 asSealedToken, 7 isValidDID, 8 isValidHandle, 9 isValidInstanceURL, 10 tryAsDID, 11 tryAsHandle, 12 tryAsInstanceURL, 13 toClientAccount, 14 toClientSession, 15 parseApiMeResponse, 16 type AccountSession, 17} from './session' 18 19// ============================================================================ 20// Branded Types Tests 21// ============================================================================ 22 23describe('DID validation', () => { 24 it('validates correct DID format', () => { 25 expect(isValidDID('did:plc:abc123')).toBe(true) 26 expect(isValidDID('did:web:example.com')).toBe(true) 27 expect( 28 isValidDID('did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK'), 29 ).toBe(true) 30 }) 31 32 it('rejects invalid DID format', () => { 33 expect(isValidDID('not-a-did')).toBe(false) 34 expect(isValidDID('did:')).toBe(false) 35 expect(isValidDID('did:plc:')).toBe(false) 36 expect(isValidDID('')).toBe(false) 37 }) 38 39 it('asDID throws for invalid DID', () => { 40 expect(() => asDID('invalid')).toThrow('Invalid DID format') 41 }) 42 43 it('asDID returns branded type for valid DID', () => { 44 const did = asDID('did:plc:test123') 45 expect(did).toBe('did:plc:test123') 46 }) 47 48 it('tryAsDID returns null for invalid DID', () => { 49 expect(tryAsDID('invalid')).toBeNull() 50 }) 51 52 it('tryAsDID returns branded type for valid DID', () => { 53 const did = tryAsDID('did:plc:test123') 54 expect(did).toBe('did:plc:test123') 55 }) 56}) 57 58describe('Handle validation', () => { 59 it('validates correct Handle format', () => { 60 expect(isValidHandle('alice.bsky.social')).toBe(true) 61 expect(isValidHandle('user.example.com')).toBe(true) 62 expect(isValidHandle('test-user.my-domain.org')).toBe(true) 63 }) 64 65 it('rejects invalid Handle format', () => { 66 expect(isValidHandle('notsingle')).toBe(false) 67 expect(isValidHandle('')).toBe(false) 68 expect(isValidHandle('.starts.with.dot')).toBe(false) 69 }) 70 71 it('asHandle throws for invalid Handle', () => { 72 expect(() => asHandle('invalid')).toThrow('Invalid Handle format') 73 }) 74 75 it('asHandle returns branded type for valid Handle', () => { 76 const handle = asHandle('user.example.com') 77 expect(handle).toBe('user.example.com') 78 }) 79 80 it('tryAsHandle returns null for invalid Handle', () => { 81 expect(tryAsHandle('invalid')).toBeNull() 82 }) 83 84 it('tryAsHandle returns branded type for valid Handle', () => { 85 const handle = tryAsHandle('user.example.com') 86 expect(handle).toBe('user.example.com') 87 }) 88}) 89 90describe('InstanceURL validation', () => { 91 it('validates correct InstanceURL format', () => { 92 expect(isValidInstanceURL('https://example.com')).toBe(true) 93 expect(isValidInstanceURL('https://bsky.social')).toBe(true) 94 expect(isValidInstanceURL('http://localhost:3000')).toBe(true) 95 }) 96 97 it('rejects invalid InstanceURL format', () => { 98 expect(isValidInstanceURL('not-a-url')).toBe(false) 99 expect(isValidInstanceURL('')).toBe(false) 100 expect(isValidInstanceURL('ftp://example.com')).toBe(false) 101 }) 102 103 it('asInstanceURL throws for invalid URL', () => { 104 expect(() => asInstanceURL('invalid')).toThrow( 105 'Invalid Instance URL format', 106 ) 107 }) 108 109 it('asInstanceURL returns branded type for valid URL', () => { 110 const url = asInstanceURL('https://example.com') 111 expect(url).toBe('https://example.com') 112 }) 113 114 it('tryAsInstanceURL returns null for invalid URL', () => { 115 expect(tryAsInstanceURL('invalid')).toBeNull() 116 }) 117 118 it('tryAsInstanceURL returns branded type for valid URL', () => { 119 const url = tryAsInstanceURL('https://example.com') 120 expect(url).toBe('https://example.com') 121 }) 122}) 123 124// ============================================================================ 125// parseApiMeResponse Tests 126// ============================================================================ 127 128describe('parseApiMeResponse', () => { 129 const testInstance = asInstanceURL('https://coves.example.com') 130 const testToken = asSealedToken('sealed-token-123') 131 132 it('returns AccountSession for valid response', () => { 133 const data = { 134 did: 'did:plc:user1', 135 handle: 'user1.example.com', 136 } 137 138 const result = parseApiMeResponse(data, testInstance, testToken) 139 140 expect(result).not.toBeNull() 141 expect(result!.did).toBe('did:plc:user1') 142 expect(result!.handle).toBe('user1.example.com') 143 expect(result!.instance).toBe('https://coves.example.com') 144 expect(result!.sealedToken).toBe('sealed-token-123') 145 expect(result!.avatar).toBeUndefined() 146 }) 147 148 it('returns null when did is missing', () => { 149 const data = { handle: 'user1.example.com' } 150 const result = parseApiMeResponse(data, testInstance, testToken) 151 expect(result).toBeNull() 152 }) 153 154 it('returns null when DID format is invalid', () => { 155 const data = { did: 'not-a-did', handle: 'user1.example.com' } 156 const result = parseApiMeResponse(data, testInstance, testToken) 157 expect(result).toBeNull() 158 }) 159 160 it('returns null when handle is missing', () => { 161 const data = { did: 'did:plc:user1' } 162 const result = parseApiMeResponse(data, testInstance, testToken) 163 expect(result).toBeNull() 164 }) 165 166 it('returns null when handle format is invalid', () => { 167 const data = { did: 'did:plc:user1', handle: 'invalid' } 168 const result = parseApiMeResponse(data, testInstance, testToken) 169 expect(result).toBeNull() 170 }) 171 172 it('includes avatar when present', () => { 173 const data = { 174 did: 'did:plc:user1', 175 handle: 'user1.example.com', 176 avatar: 'https://example.com/avatar.png', 177 } 178 179 const result = parseApiMeResponse(data, testInstance, testToken) 180 181 expect(result).not.toBeNull() 182 expect(result!.avatar).toBe('https://example.com/avatar.png') 183 }) 184 185 it('sets avatar to undefined when not present', () => { 186 const data = { 187 did: 'did:plc:user1', 188 handle: 'user1.example.com', 189 } 190 191 const result = parseApiMeResponse(data, testInstance, testToken) 192 193 expect(result).not.toBeNull() 194 expect(result!.avatar).toBeUndefined() 195 }) 196 197 it('returns null for null input', () => { 198 const result = parseApiMeResponse(null, testInstance, testToken) 199 expect(result).toBeNull() 200 }) 201 202 it('returns null for non-object input', () => { 203 expect(parseApiMeResponse('string', testInstance, testToken)).toBeNull() 204 expect(parseApiMeResponse(42, testInstance, testToken)).toBeNull() 205 expect(parseApiMeResponse(true, testInstance, testToken)).toBeNull() 206 expect(parseApiMeResponse(undefined, testInstance, testToken)).toBeNull() 207 }) 208 209 it('logs warning when did is missing', () => { 210 const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) 211 212 parseApiMeResponse({ handle: 'user1.example.com' }, testInstance, testToken) 213 214 expect(warnSpy).toHaveBeenCalledWith( 215 expect.stringContaining('Missing or non-string "did"'), 216 ) 217 warnSpy.mockRestore() 218 }) 219 220 it('logs warning when DID format is invalid', () => { 221 const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) 222 223 parseApiMeResponse( 224 { did: 'not-a-did', handle: 'user1.example.com' }, 225 testInstance, 226 testToken, 227 ) 228 229 expect(warnSpy).toHaveBeenCalledWith( 230 expect.stringContaining('Invalid DID format'), 231 'not-a-did', 232 ) 233 warnSpy.mockRestore() 234 }) 235 236 it('logs warning when handle is missing', () => { 237 const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) 238 239 parseApiMeResponse({ did: 'did:plc:user1' }, testInstance, testToken) 240 241 expect(warnSpy).toHaveBeenCalledWith( 242 expect.stringContaining('Missing or non-string "handle"'), 243 ) 244 warnSpy.mockRestore() 245 }) 246 247 it('logs warning when handle format is invalid', () => { 248 const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) 249 250 parseApiMeResponse( 251 { did: 'did:plc:user1', handle: 'invalid' }, 252 testInstance, 253 testToken, 254 ) 255 256 expect(warnSpy).toHaveBeenCalledWith( 257 expect.stringContaining('Invalid handle format'), 258 'invalid', 259 ) 260 warnSpy.mockRestore() 261 }) 262 263 it('logs warning for non-object input', () => { 264 const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) 265 266 parseApiMeResponse('string', testInstance, testToken) 267 268 expect(warnSpy).toHaveBeenCalledWith( 269 expect.stringContaining('Invalid input: expected object'), 270 'string', 271 ) 272 warnSpy.mockRestore() 273 }) 274 275 it('rejects javascript: avatar URLs and sets avatar to undefined', () => { 276 const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) 277 278 const data = { 279 did: 'did:plc:user1', 280 handle: 'user1.example.com', 281 avatar: 'javascript:alert(1)', 282 } 283 284 const result = parseApiMeResponse(data, testInstance, testToken) 285 286 expect(result).not.toBeNull() 287 expect(result!.avatar).toBeUndefined() 288 expect(warnSpy).toHaveBeenCalledWith( 289 expect.stringContaining('Avatar URL rejected'), 290 'javascript:alert(1)', 291 ) 292 warnSpy.mockRestore() 293 }) 294 295 it('rejects data: avatar URLs and sets avatar to undefined', () => { 296 const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) 297 298 const data = { 299 did: 'did:plc:user1', 300 handle: 'user1.example.com', 301 avatar: 'data:text/html,<script>alert(1)</script>', 302 } 303 304 const result = parseApiMeResponse(data, testInstance, testToken) 305 306 expect(result).not.toBeNull() 307 expect(result!.avatar).toBeUndefined() 308 expect(warnSpy).toHaveBeenCalledWith( 309 expect.stringContaining('Avatar URL rejected'), 310 'data:text/html,<script>alert(1)</script>', 311 ) 312 warnSpy.mockRestore() 313 }) 314 315 it('accepts https avatar URLs', () => { 316 const data = { 317 did: 'did:plc:user1', 318 handle: 'user1.example.com', 319 avatar: 'https://cdn.example.com/avatar.png', 320 } 321 322 const result = parseApiMeResponse(data, testInstance, testToken) 323 324 expect(result).not.toBeNull() 325 expect(result!.avatar).toBe('https://cdn.example.com/avatar.png') 326 }) 327 328 it('accepts http avatar URLs', () => { 329 const data = { 330 did: 'did:plc:user1', 331 handle: 'user1.example.com', 332 avatar: 'http://localhost:3000/avatar.png', 333 } 334 335 const result = parseApiMeResponse(data, testInstance, testToken) 336 337 expect(result).not.toBeNull() 338 expect(result!.avatar).toBe('http://localhost:3000/avatar.png') 339 }) 340}) 341 342// ============================================================================ 343// toClientAccount / toClientSession Tests 344// ============================================================================ 345 346describe('toClientAccount', () => { 347 it('removes sealedToken and uses DID as id', () => { 348 const account: AccountSession = { 349 did: asDID('did:plc:test'), 350 handle: asHandle('test.example.com'), 351 instance: asInstanceURL('https://example.com'), 352 sealedToken: asSealedToken('secret-token'), 353 avatar: 'https://example.com/avatar.png', 354 } 355 356 const clientAccount = toClientAccount(account) 357 358 expect(clientAccount.id).toBe('did:plc:test') 359 expect(clientAccount.did).toBe('did:plc:test') 360 expect(clientAccount.handle).toBe('test.example.com') 361 expect(clientAccount.instance).toBe('https://example.com') 362 expect(clientAccount.avatar).toBe('https://example.com/avatar.png') 363 expect('sealedToken' in clientAccount).toBe(false) 364 }) 365}) 366 367describe('toClientSession', () => { 368 it('returns unauthenticated session for null account', () => { 369 const clientSession = toClientSession(null) 370 371 expect(clientSession.authenticated).toBe(false) 372 expect(clientSession.activeAccountId).toBeNull() 373 expect(clientSession.account).toBeNull() 374 }) 375 376 it('returns authenticated session with account for valid AccountSession', () => { 377 const account: AccountSession = { 378 did: asDID('did:plc:test'), 379 handle: asHandle('test.example.com'), 380 instance: asInstanceURL('https://example.com'), 381 sealedToken: asSealedToken('secret-token'), 382 } 383 384 const clientSession = toClientSession(account) 385 386 expect(clientSession.authenticated).toBe(true) 387 expect(clientSession.activeAccountId).toBe('did:plc:test') 388 expect(clientSession.account).not.toBeNull() 389 if (clientSession.authenticated) { 390 expect(clientSession.account.did).toBe('did:plc:test') 391 expect('sealedToken' in clientSession.account).toBe(false) 392 } 393 }) 394})