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