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