this repo has no description
1import { describe, it, expect, beforeEach, vi } from 'vitest' 2import { render, screen, fireEvent, waitFor } from '@testing-library/svelte' 3import AppPasswords from '../routes/AppPasswords.svelte' 4import { 5 setupFetchMock, 6 mockEndpoint, 7 jsonResponse, 8 errorResponse, 9 mockData, 10 clearMocks, 11 setupAuthenticatedUser, 12 setupUnauthenticatedUser, 13} from './mocks' 14describe('AppPasswords', () => { 15 beforeEach(() => { 16 clearMocks() 17 setupFetchMock() 18 window.confirm = vi.fn(() => true) 19 }) 20 describe('authentication guard', () => { 21 it('redirects to login when not authenticated', async () => { 22 setupUnauthenticatedUser() 23 render(AppPasswords) 24 await waitFor(() => { 25 expect(window.location.hash).toBe('#/login') 26 }) 27 }) 28 }) 29 describe('page structure', () => { 30 beforeEach(() => { 31 setupAuthenticatedUser() 32 mockEndpoint('com.atproto.server.listAppPasswords', () => 33 jsonResponse({ passwords: [] }) 34 ) 35 }) 36 it('displays all page elements', async () => { 37 render(AppPasswords) 38 await waitFor(() => { 39 expect(screen.getByRole('heading', { name: /app passwords/i, level: 1 })).toBeInTheDocument() 40 expect(screen.getByRole('link', { name: /dashboard/i })).toHaveAttribute('href', '#/dashboard') 41 expect(screen.getByText(/third-party apps/i)).toBeInTheDocument() 42 }) 43 }) 44 }) 45 describe('loading state', () => { 46 beforeEach(() => { 47 setupAuthenticatedUser() 48 }) 49 it('shows loading text while fetching passwords', async () => { 50 mockEndpoint('com.atproto.server.listAppPasswords', async () => { 51 await new Promise(resolve => setTimeout(resolve, 100)) 52 return jsonResponse({ passwords: [] }) 53 }) 54 render(AppPasswords) 55 expect(screen.getByText(/loading/i)).toBeInTheDocument() 56 }) 57 }) 58 describe('empty state', () => { 59 beforeEach(() => { 60 setupAuthenticatedUser() 61 mockEndpoint('com.atproto.server.listAppPasswords', () => 62 jsonResponse({ passwords: [] }) 63 ) 64 }) 65 it('shows empty message when no passwords exist', async () => { 66 render(AppPasswords) 67 await waitFor(() => { 68 expect(screen.getByText(/no app passwords yet/i)).toBeInTheDocument() 69 }) 70 }) 71 }) 72 describe('password list', () => { 73 const testPasswords = [ 74 mockData.appPassword({ name: 'Graysky', createdAt: '2024-01-15T10:00:00Z' }), 75 mockData.appPassword({ name: 'Skeets', createdAt: '2024-02-20T15:30:00Z' }), 76 ] 77 beforeEach(() => { 78 setupAuthenticatedUser() 79 mockEndpoint('com.atproto.server.listAppPasswords', () => 80 jsonResponse({ passwords: testPasswords }) 81 ) 82 }) 83 it('displays all app passwords with dates and revoke buttons', async () => { 84 render(AppPasswords) 85 await waitFor(() => { 86 expect(screen.getByText('Graysky')).toBeInTheDocument() 87 expect(screen.getByText('Skeets')).toBeInTheDocument() 88 expect(screen.getByText(/created.*1\/15\/2024/i)).toBeInTheDocument() 89 expect(screen.getByText(/created.*2\/20\/2024/i)).toBeInTheDocument() 90 expect(screen.getAllByRole('button', { name: /revoke/i })).toHaveLength(2) 91 }) 92 }) 93 }) 94 describe('create app password', () => { 95 beforeEach(() => { 96 setupAuthenticatedUser() 97 mockEndpoint('com.atproto.server.listAppPasswords', () => 98 jsonResponse({ passwords: [] }) 99 ) 100 }) 101 it('displays create form with input and button', async () => { 102 render(AppPasswords) 103 await waitFor(() => { 104 expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument() 105 expect(screen.getByRole('button', { name: /create/i })).toBeInTheDocument() 106 }) 107 }) 108 it('disables create button when input is empty', async () => { 109 render(AppPasswords) 110 await waitFor(() => { 111 expect(screen.getByRole('button', { name: /create/i })).toBeDisabled() 112 }) 113 }) 114 it('enables create button when input has value', async () => { 115 render(AppPasswords) 116 await waitFor(() => { 117 expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument() 118 }) 119 await fireEvent.input(screen.getByPlaceholderText(/app name/i), { target: { value: 'My New App' } }) 120 expect(screen.getByRole('button', { name: /create/i })).not.toBeDisabled() 121 }) 122 it('calls createAppPassword with correct name', async () => { 123 let capturedName: string | null = null 124 mockEndpoint('com.atproto.server.createAppPassword', (_url, options) => { 125 const body = JSON.parse((options?.body as string) || '{}') 126 capturedName = body.name 127 return jsonResponse({ 128 name: body.name, 129 password: 'xxxx-xxxx-xxxx-xxxx', 130 createdAt: new Date().toISOString(), 131 }) 132 }) 133 render(AppPasswords) 134 await waitFor(() => { 135 expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument() 136 }) 137 await fireEvent.input(screen.getByPlaceholderText(/app name/i), { target: { value: 'Graysky' } }) 138 await fireEvent.click(screen.getByRole('button', { name: /create/i })) 139 await waitFor(() => { 140 expect(capturedName).toBe('Graysky') 141 }) 142 }) 143 it('shows loading state while creating', async () => { 144 mockEndpoint('com.atproto.server.createAppPassword', async () => { 145 await new Promise(resolve => setTimeout(resolve, 100)) 146 return jsonResponse({ 147 name: 'Test', 148 password: 'xxxx-xxxx-xxxx-xxxx', 149 createdAt: new Date().toISOString(), 150 }) 151 }) 152 render(AppPasswords) 153 await waitFor(() => { 154 expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument() 155 }) 156 await fireEvent.input(screen.getByPlaceholderText(/app name/i), { target: { value: 'Test' } }) 157 await fireEvent.click(screen.getByRole('button', { name: /create/i })) 158 expect(screen.getByRole('button', { name: /creating/i })).toBeInTheDocument() 159 expect(screen.getByRole('button', { name: /creating/i })).toBeDisabled() 160 }) 161 it('displays created password in success box and clears input', async () => { 162 mockEndpoint('com.atproto.server.createAppPassword', () => 163 jsonResponse({ 164 name: 'MyApp', 165 password: 'abcd-efgh-ijkl-mnop', 166 createdAt: new Date().toISOString(), 167 }) 168 ) 169 render(AppPasswords) 170 await waitFor(() => { 171 expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument() 172 }) 173 const input = screen.getByPlaceholderText(/app name/i) as HTMLInputElement 174 await fireEvent.input(input, { target: { value: 'MyApp' } }) 175 await fireEvent.click(screen.getByRole('button', { name: /create/i })) 176 await waitFor(() => { 177 expect(screen.getByText(/app password created/i)).toBeInTheDocument() 178 expect(screen.getByText('abcd-efgh-ijkl-mnop')).toBeInTheDocument() 179 expect(screen.getByText(/name: myapp/i)).toBeInTheDocument() 180 expect(input.value).toBe('') 181 }) 182 }) 183 it('dismisses created password box when clicking Done', async () => { 184 mockEndpoint('com.atproto.server.createAppPassword', () => 185 jsonResponse({ 186 name: 'Test', 187 password: 'xxxx-xxxx-xxxx-xxxx', 188 createdAt: new Date().toISOString(), 189 }) 190 ) 191 render(AppPasswords) 192 await waitFor(() => { 193 expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument() 194 }) 195 await fireEvent.input(screen.getByPlaceholderText(/app name/i), { target: { value: 'Test' } }) 196 await fireEvent.click(screen.getByRole('button', { name: /create/i })) 197 await waitFor(() => { 198 expect(screen.getByText(/app password created/i)).toBeInTheDocument() 199 }) 200 await fireEvent.click(screen.getByRole('button', { name: /done/i })) 201 await waitFor(() => { 202 expect(screen.queryByText(/app password created/i)).not.toBeInTheDocument() 203 }) 204 }) 205 it('shows error when creation fails', async () => { 206 mockEndpoint('com.atproto.server.createAppPassword', () => 207 errorResponse('InvalidRequest', 'Name already exists', 400) 208 ) 209 render(AppPasswords) 210 await waitFor(() => { 211 expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument() 212 }) 213 await fireEvent.input(screen.getByPlaceholderText(/app name/i), { target: { value: 'Duplicate' } }) 214 await fireEvent.click(screen.getByRole('button', { name: /create/i })) 215 await waitFor(() => { 216 expect(screen.getByText(/name already exists/i)).toBeInTheDocument() 217 expect(screen.getByText(/name already exists/i)).toHaveClass('error') 218 }) 219 }) 220 }) 221 describe('revoke app password', () => { 222 const testPassword = mockData.appPassword({ name: 'TestApp' }) 223 beforeEach(() => { 224 setupAuthenticatedUser() 225 }) 226 it('shows confirmation dialog before revoking', async () => { 227 const confirmSpy = vi.fn(() => false) 228 window.confirm = confirmSpy 229 mockEndpoint('com.atproto.server.listAppPasswords', () => 230 jsonResponse({ passwords: [testPassword] }) 231 ) 232 render(AppPasswords) 233 await waitFor(() => { 234 expect(screen.getByText('TestApp')).toBeInTheDocument() 235 }) 236 await fireEvent.click(screen.getByRole('button', { name: /revoke/i })) 237 expect(confirmSpy).toHaveBeenCalledWith( 238 expect.stringContaining('TestApp') 239 ) 240 }) 241 it('does not revoke when confirmation is cancelled', async () => { 242 window.confirm = vi.fn(() => false) 243 let revokeCalled = false 244 mockEndpoint('com.atproto.server.listAppPasswords', () => 245 jsonResponse({ passwords: [testPassword] }) 246 ) 247 mockEndpoint('com.atproto.server.revokeAppPassword', () => { 248 revokeCalled = true 249 return jsonResponse({}) 250 }) 251 render(AppPasswords) 252 await waitFor(() => { 253 expect(screen.getByText('TestApp')).toBeInTheDocument() 254 }) 255 await fireEvent.click(screen.getByRole('button', { name: /revoke/i })) 256 expect(revokeCalled).toBe(false) 257 }) 258 it('calls revokeAppPassword with correct name', async () => { 259 window.confirm = vi.fn(() => true) 260 let capturedName: string | null = null 261 mockEndpoint('com.atproto.server.listAppPasswords', () => 262 jsonResponse({ passwords: [testPassword] }) 263 ) 264 mockEndpoint('com.atproto.server.revokeAppPassword', (_url, options) => { 265 const body = JSON.parse((options?.body as string) || '{}') 266 capturedName = body.name 267 return jsonResponse({}) 268 }) 269 render(AppPasswords) 270 await waitFor(() => { 271 expect(screen.getByText('TestApp')).toBeInTheDocument() 272 }) 273 await fireEvent.click(screen.getByRole('button', { name: /revoke/i })) 274 await waitFor(() => { 275 expect(capturedName).toBe('TestApp') 276 }) 277 }) 278 it('shows loading state while revoking', async () => { 279 window.confirm = vi.fn(() => true) 280 mockEndpoint('com.atproto.server.listAppPasswords', () => 281 jsonResponse({ passwords: [testPassword] }) 282 ) 283 mockEndpoint('com.atproto.server.revokeAppPassword', async () => { 284 await new Promise(resolve => setTimeout(resolve, 100)) 285 return jsonResponse({}) 286 }) 287 render(AppPasswords) 288 await waitFor(() => { 289 expect(screen.getByText('TestApp')).toBeInTheDocument() 290 }) 291 await fireEvent.click(screen.getByRole('button', { name: /revoke/i })) 292 expect(screen.getByRole('button', { name: /revoking/i })).toBeInTheDocument() 293 expect(screen.getByRole('button', { name: /revoking/i })).toBeDisabled() 294 }) 295 it('reloads password list after successful revocation', async () => { 296 window.confirm = vi.fn(() => true) 297 let listCallCount = 0 298 mockEndpoint('com.atproto.server.listAppPasswords', () => { 299 listCallCount++ 300 if (listCallCount === 1) { 301 return jsonResponse({ passwords: [testPassword] }) 302 } 303 return jsonResponse({ passwords: [] }) 304 }) 305 mockEndpoint('com.atproto.server.revokeAppPassword', () => 306 jsonResponse({}) 307 ) 308 render(AppPasswords) 309 await waitFor(() => { 310 expect(screen.getByText('TestApp')).toBeInTheDocument() 311 }) 312 await fireEvent.click(screen.getByRole('button', { name: /revoke/i })) 313 await waitFor(() => { 314 expect(screen.queryByText('TestApp')).not.toBeInTheDocument() 315 expect(screen.getByText(/no app passwords yet/i)).toBeInTheDocument() 316 }) 317 }) 318 it('shows error when revocation fails', async () => { 319 window.confirm = vi.fn(() => true) 320 mockEndpoint('com.atproto.server.listAppPasswords', () => 321 jsonResponse({ passwords: [testPassword] }) 322 ) 323 mockEndpoint('com.atproto.server.revokeAppPassword', () => 324 errorResponse('InternalError', 'Server error', 500) 325 ) 326 render(AppPasswords) 327 await waitFor(() => { 328 expect(screen.getByText('TestApp')).toBeInTheDocument() 329 }) 330 await fireEvent.click(screen.getByRole('button', { name: /revoke/i })) 331 await waitFor(() => { 332 expect(screen.getByText(/server error/i)).toBeInTheDocument() 333 expect(screen.getByText(/server error/i)).toHaveClass('error') 334 }) 335 }) 336 }) 337 describe('error handling', () => { 338 beforeEach(() => { 339 setupAuthenticatedUser() 340 }) 341 it('shows error when loading passwords fails', async () => { 342 mockEndpoint('com.atproto.server.listAppPasswords', () => 343 errorResponse('InternalError', 'Database connection failed', 500) 344 ) 345 render(AppPasswords) 346 await waitFor(() => { 347 expect(screen.getByText(/database connection failed/i)).toBeInTheDocument() 348 expect(screen.getByText(/database connection failed/i)).toHaveClass('error') 349 }) 350 }) 351 }) 352})