import { describe, it, expect, vi } from 'vitest'
import {
asDID,
asHandle,
asInstanceURL,
asSealedToken,
isValidDID,
isValidHandle,
isValidInstanceURL,
tryAsDID,
tryAsHandle,
tryAsInstanceURL,
toClientAccount,
toClientSession,
parseApiMeResponse,
type AccountSession,
} from './session'
// ============================================================================
// Branded Types Tests
// ============================================================================
describe('DID validation', () => {
it('validates correct DID format', () => {
expect(isValidDID('did:plc:abc123')).toBe(true)
expect(isValidDID('did:web:example.com')).toBe(true)
expect(
isValidDID('did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK'),
).toBe(true)
})
it('rejects invalid DID format', () => {
expect(isValidDID('not-a-did')).toBe(false)
expect(isValidDID('did:')).toBe(false)
expect(isValidDID('did:plc:')).toBe(false)
expect(isValidDID('')).toBe(false)
})
it('asDID throws for invalid DID', () => {
expect(() => asDID('invalid')).toThrow('Invalid DID format')
})
it('asDID returns branded type for valid DID', () => {
const did = asDID('did:plc:test123')
expect(did).toBe('did:plc:test123')
})
it('tryAsDID returns null for invalid DID', () => {
expect(tryAsDID('invalid')).toBeNull()
})
it('tryAsDID returns branded type for valid DID', () => {
const did = tryAsDID('did:plc:test123')
expect(did).toBe('did:plc:test123')
})
})
describe('Handle validation', () => {
it('validates correct Handle format', () => {
expect(isValidHandle('alice.bsky.social')).toBe(true)
expect(isValidHandle('user.example.com')).toBe(true)
expect(isValidHandle('test-user.my-domain.org')).toBe(true)
})
it('rejects invalid Handle format', () => {
expect(isValidHandle('notsingle')).toBe(false)
expect(isValidHandle('')).toBe(false)
expect(isValidHandle('.starts.with.dot')).toBe(false)
})
it('asHandle throws for invalid Handle', () => {
expect(() => asHandle('invalid')).toThrow('Invalid Handle format')
})
it('asHandle returns branded type for valid Handle', () => {
const handle = asHandle('user.example.com')
expect(handle).toBe('user.example.com')
})
it('tryAsHandle returns null for invalid Handle', () => {
expect(tryAsHandle('invalid')).toBeNull()
})
it('tryAsHandle returns branded type for valid Handle', () => {
const handle = tryAsHandle('user.example.com')
expect(handle).toBe('user.example.com')
})
})
describe('InstanceURL validation', () => {
it('validates correct InstanceURL format', () => {
expect(isValidInstanceURL('https://example.com')).toBe(true)
expect(isValidInstanceURL('https://bsky.social')).toBe(true)
expect(isValidInstanceURL('http://localhost:3000')).toBe(true)
})
it('rejects invalid InstanceURL format', () => {
expect(isValidInstanceURL('not-a-url')).toBe(false)
expect(isValidInstanceURL('')).toBe(false)
expect(isValidInstanceURL('ftp://example.com')).toBe(false)
})
it('asInstanceURL throws for invalid URL', () => {
expect(() => asInstanceURL('invalid')).toThrow(
'Invalid Instance URL format',
)
})
it('asInstanceURL returns branded type for valid URL', () => {
const url = asInstanceURL('https://example.com')
expect(url).toBe('https://example.com')
})
it('tryAsInstanceURL returns null for invalid URL', () => {
expect(tryAsInstanceURL('invalid')).toBeNull()
})
it('tryAsInstanceURL returns branded type for valid URL', () => {
const url = tryAsInstanceURL('https://example.com')
expect(url).toBe('https://example.com')
})
})
// ============================================================================
// parseApiMeResponse Tests
// ============================================================================
describe('parseApiMeResponse', () => {
const testInstance = asInstanceURL('https://coves.example.com')
const testToken = asSealedToken('sealed-token-123')
it('returns AccountSession for valid response', () => {
const data = {
did: 'did:plc:user1',
handle: 'user1.example.com',
}
const result = parseApiMeResponse(data, testInstance, testToken)
expect(result).not.toBeNull()
expect(result!.did).toBe('did:plc:user1')
expect(result!.handle).toBe('user1.example.com')
expect(result!.instance).toBe('https://coves.example.com')
expect(result!.sealedToken).toBe('sealed-token-123')
expect(result!.avatar).toBeUndefined()
})
it('returns null when did is missing', () => {
const data = { handle: 'user1.example.com' }
const result = parseApiMeResponse(data, testInstance, testToken)
expect(result).toBeNull()
})
it('returns null when DID format is invalid', () => {
const data = { did: 'not-a-did', handle: 'user1.example.com' }
const result = parseApiMeResponse(data, testInstance, testToken)
expect(result).toBeNull()
})
it('returns null when handle is missing', () => {
const data = { did: 'did:plc:user1' }
const result = parseApiMeResponse(data, testInstance, testToken)
expect(result).toBeNull()
})
it('returns null when handle format is invalid', () => {
const data = { did: 'did:plc:user1', handle: 'invalid' }
const result = parseApiMeResponse(data, testInstance, testToken)
expect(result).toBeNull()
})
it('includes avatar when present', () => {
const data = {
did: 'did:plc:user1',
handle: 'user1.example.com',
avatar: 'https://example.com/avatar.png',
}
const result = parseApiMeResponse(data, testInstance, testToken)
expect(result).not.toBeNull()
expect(result!.avatar).toBe('https://example.com/avatar.png')
})
it('sets avatar to undefined when not present', () => {
const data = {
did: 'did:plc:user1',
handle: 'user1.example.com',
}
const result = parseApiMeResponse(data, testInstance, testToken)
expect(result).not.toBeNull()
expect(result!.avatar).toBeUndefined()
})
it('returns null for null input', () => {
const result = parseApiMeResponse(null, testInstance, testToken)
expect(result).toBeNull()
})
it('returns null for non-object input', () => {
expect(parseApiMeResponse('string', testInstance, testToken)).toBeNull()
expect(parseApiMeResponse(42, testInstance, testToken)).toBeNull()
expect(parseApiMeResponse(true, testInstance, testToken)).toBeNull()
expect(parseApiMeResponse(undefined, testInstance, testToken)).toBeNull()
})
it('logs warning when did is missing', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
parseApiMeResponse({ handle: 'user1.example.com' }, testInstance, testToken)
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Missing or non-string "did"'),
)
warnSpy.mockRestore()
})
it('logs warning when DID format is invalid', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
parseApiMeResponse(
{ did: 'not-a-did', handle: 'user1.example.com' },
testInstance,
testToken,
)
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Invalid DID format'),
'not-a-did',
)
warnSpy.mockRestore()
})
it('logs warning when handle is missing', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
parseApiMeResponse({ did: 'did:plc:user1' }, testInstance, testToken)
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Missing or non-string "handle"'),
)
warnSpy.mockRestore()
})
it('logs warning when handle format is invalid', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
parseApiMeResponse(
{ did: 'did:plc:user1', handle: 'invalid' },
testInstance,
testToken,
)
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Invalid handle format'),
'invalid',
)
warnSpy.mockRestore()
})
it('logs warning for non-object input', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
parseApiMeResponse('string', testInstance, testToken)
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Invalid input: expected object'),
'string',
)
warnSpy.mockRestore()
})
it('rejects javascript: avatar URLs and sets avatar to undefined', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const data = {
did: 'did:plc:user1',
handle: 'user1.example.com',
avatar: 'javascript:alert(1)',
}
const result = parseApiMeResponse(data, testInstance, testToken)
expect(result).not.toBeNull()
expect(result!.avatar).toBeUndefined()
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Avatar URL rejected'),
'javascript:alert(1)',
)
warnSpy.mockRestore()
})
it('rejects data: avatar URLs and sets avatar to undefined', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const data = {
did: 'did:plc:user1',
handle: 'user1.example.com',
avatar: 'data:text/html,',
}
const result = parseApiMeResponse(data, testInstance, testToken)
expect(result).not.toBeNull()
expect(result!.avatar).toBeUndefined()
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Avatar URL rejected'),
'data:text/html,',
)
warnSpy.mockRestore()
})
it('accepts https avatar URLs', () => {
const data = {
did: 'did:plc:user1',
handle: 'user1.example.com',
avatar: 'https://cdn.example.com/avatar.png',
}
const result = parseApiMeResponse(data, testInstance, testToken)
expect(result).not.toBeNull()
expect(result!.avatar).toBe('https://cdn.example.com/avatar.png')
})
it('accepts http avatar URLs', () => {
const data = {
did: 'did:plc:user1',
handle: 'user1.example.com',
avatar: 'http://localhost:3000/avatar.png',
}
const result = parseApiMeResponse(data, testInstance, testToken)
expect(result).not.toBeNull()
expect(result!.avatar).toBe('http://localhost:3000/avatar.png')
})
})
// ============================================================================
// toClientAccount / toClientSession Tests
// ============================================================================
describe('toClientAccount', () => {
it('removes sealedToken and uses DID as id', () => {
const account: AccountSession = {
did: asDID('did:plc:test'),
handle: asHandle('test.example.com'),
instance: asInstanceURL('https://example.com'),
sealedToken: asSealedToken('secret-token'),
avatar: 'https://example.com/avatar.png',
}
const clientAccount = toClientAccount(account)
expect(clientAccount.id).toBe('did:plc:test')
expect(clientAccount.did).toBe('did:plc:test')
expect(clientAccount.handle).toBe('test.example.com')
expect(clientAccount.instance).toBe('https://example.com')
expect(clientAccount.avatar).toBe('https://example.com/avatar.png')
expect('sealedToken' in clientAccount).toBe(false)
})
})
describe('toClientSession', () => {
it('returns unauthenticated session for null account', () => {
const clientSession = toClientSession(null)
expect(clientSession.authenticated).toBe(false)
expect(clientSession.activeAccountId).toBeNull()
expect(clientSession.account).toBeNull()
})
it('returns authenticated session with account for valid AccountSession', () => {
const account: AccountSession = {
did: asDID('did:plc:test'),
handle: asHandle('test.example.com'),
instance: asInstanceURL('https://example.com'),
sealedToken: asSealedToken('secret-token'),
}
const clientSession = toClientSession(account)
expect(clientSession.authenticated).toBe(true)
expect(clientSession.activeAccountId).toBe('did:plc:test')
expect(clientSession.account).not.toBeNull()
if (clientSession.authenticated) {
expect(clientSession.account.did).toBe('did:plc:test')
expect('sealedToken' in clientSession.account).toBe(false)
}
})
})