Barazo default frontend barazo.forum
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

test(auth): add login flow contract tests (#89)

Add tests that verify the API client returns `{url}` (not `{redirectUrl}`)
and that AuthProvider correctly redirects to the OAuth URL. This prevents
regression of the /login/undefined/ bug caused by field name mismatch.

- New client-api.test.ts: 7 tests for initiateLogin, refreshSession,
initiateCrossPostAuth response shapes and error handling
- Updated auth-context.test.tsx: login flow integration test verifying
window.location.href receives the OAuth URL from the API

authored by

Guido X Jansen and committed by
GitHub
bd82b13c a0be5880

+168
+67
src/__tests__/auth/auth-context.test.tsx
··· 104 104 }) 105 105 }) 106 106 107 + describe('AuthProvider login flow', () => { 108 + it('redirects to OAuth URL from API response', async () => { 109 + const oauthUrl = 'https://bsky.social/oauth/authorize?client_id=test&request_uri=urn:test' 110 + 111 + server.use( 112 + http.get(`${API_URL}/api/auth/login`, ({ request }) => { 113 + const url = new URL(request.url) 114 + expect(url.searchParams.get('handle')).toBe('test.bsky.social') 115 + return HttpResponse.json({ url: oauthUrl }) 116 + }), 117 + // Silent refresh fails (user not logged in) 118 + http.post(`${API_URL}/api/auth/refresh`, () => { 119 + return HttpResponse.json({ error: 'No session' }, { status: 401 }) 120 + }) 121 + ) 122 + 123 + // Spy on window.location.href assignment 124 + const locationSpy = vi.spyOn(window, 'location', 'get').mockReturnValue({ 125 + ...window.location, 126 + href: 'http://localhost:3000/login', 127 + }) 128 + const hrefSetter = vi.fn() 129 + Object.defineProperty(window, 'location', { 130 + value: { ...window.location, href: 'http://localhost:3000/login' }, 131 + writable: true, 132 + configurable: true, 133 + }) 134 + Object.defineProperty(window.location, 'href', { 135 + set: hrefSetter, 136 + get: () => 'http://localhost:3000/login', 137 + configurable: true, 138 + }) 139 + 140 + function LoginTrigger() { 141 + const { login, isLoading } = useAuth() 142 + return ( 143 + <button disabled={isLoading} onClick={() => void login('test.bsky.social')}> 144 + Login 145 + </button> 146 + ) 147 + } 148 + 149 + render( 150 + <AuthProvider> 151 + <LoginTrigger /> 152 + </AuthProvider> 153 + ) 154 + 155 + // Wait for loading to finish 156 + await waitFor(() => { 157 + expect(screen.getByRole('button')).not.toBeDisabled() 158 + }) 159 + 160 + // Click login 161 + await act(async () => { 162 + screen.getByRole('button').click() 163 + }) 164 + 165 + // Verify redirect to the OAuth URL from the API (not undefined, not redirectUrl) 166 + await waitFor(() => { 167 + expect(hrefSetter).toHaveBeenCalledWith(oauthUrl) 168 + }) 169 + 170 + locationSpy.mockRestore() 171 + }) 172 + }) 173 + 107 174 describe('useAuth', () => { 108 175 it('throws when used outside AuthProvider', () => { 109 176 const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+101
src/__tests__/auth/client-api.test.ts
··· 1 + /** 2 + * Tests for API client auth functions. 3 + * Verifies the contract between client functions and the API response shape. 4 + */ 5 + 6 + import { describe, it, expect } from 'vitest' 7 + import { http, HttpResponse } from 'msw' 8 + import { server } from '@/mocks/server' 9 + import { initiateLogin, refreshSession, initiateCrossPostAuth } from '@/lib/api/client' 10 + 11 + const API_URL = '' 12 + 13 + describe('initiateLogin', () => { 14 + it('returns the redirect URL from the API', async () => { 15 + const expectedUrl = 'https://bsky.social/oauth/authorize?client_id=test&request_uri=urn:test' 16 + 17 + server.use( 18 + http.get(`${API_URL}/api/auth/login`, ({ request }) => { 19 + const url = new URL(request.url) 20 + expect(url.searchParams.get('handle')).toBe('alice.bsky.social') 21 + return HttpResponse.json({ url: expectedUrl }) 22 + }) 23 + ) 24 + 25 + const result = await initiateLogin('alice.bsky.social') 26 + expect(result).toEqual({ url: expectedUrl }) 27 + expect(result.url).toBe(expectedUrl) 28 + }) 29 + 30 + it('passes handle as query parameter', async () => { 31 + let receivedHandle: string | null = null 32 + 33 + server.use( 34 + http.get(`${API_URL}/api/auth/login`, ({ request }) => { 35 + const url = new URL(request.url) 36 + receivedHandle = url.searchParams.get('handle') 37 + return HttpResponse.json({ url: 'https://example.com/oauth' }) 38 + }) 39 + ) 40 + 41 + await initiateLogin('gui.do') 42 + expect(receivedHandle).toBe('gui.do') 43 + }) 44 + 45 + it('throws on API error', async () => { 46 + server.use( 47 + http.get(`${API_URL}/api/auth/login`, () => { 48 + return HttpResponse.json({ error: 'Failed to initiate login' }, { status: 502 }) 49 + }) 50 + ) 51 + 52 + await expect(initiateLogin('bad.handle')).rejects.toThrow('API 502') 53 + }) 54 + 55 + it('throws on invalid handle', async () => { 56 + server.use( 57 + http.get(`${API_URL}/api/auth/login`, () => { 58 + return HttpResponse.json({ error: 'Invalid handle' }, { status: 400 }) 59 + }) 60 + ) 61 + 62 + await expect(initiateLogin('')).rejects.toThrow('API 400') 63 + }) 64 + }) 65 + 66 + describe('refreshSession', () => { 67 + it('returns session data on success', async () => { 68 + const result = await refreshSession() 69 + expect(result).toHaveProperty('accessToken') 70 + expect(result).toHaveProperty('did') 71 + expect(result).toHaveProperty('handle') 72 + }) 73 + 74 + it('throws on expired session', async () => { 75 + server.use( 76 + http.post(`${API_URL}/api/auth/refresh`, () => { 77 + return HttpResponse.json({ error: 'No refresh token' }, { status: 401 }) 78 + }) 79 + ) 80 + 81 + await expect(refreshSession()).rejects.toThrow('API 401') 82 + }) 83 + }) 84 + 85 + describe('initiateCrossPostAuth', () => { 86 + it('returns the redirect URL with auth header', async () => { 87 + const expectedUrl = 'https://bsky.social/oauth/authorize?scope=crosspost' 88 + 89 + server.use( 90 + http.get(`${API_URL}/api/auth/crosspost-authorize`, ({ request }) => { 91 + const auth = request.headers.get('Authorization') 92 + expect(auth).toBe('Bearer test-token-123') 93 + return HttpResponse.json({ url: expectedUrl }) 94 + }) 95 + ) 96 + 97 + const result = await initiateCrossPostAuth('test-token-123') 98 + expect(result).toEqual({ url: expectedUrl }) 99 + expect(result.url).toBe(expectedUrl) 100 + }) 101 + })