Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun
at main 197 lines 5.6 kB view raw
1import { beforeEach, describe, expect, it } from "vitest"; 2import { fireEvent, render, screen, waitFor } from "@testing-library/svelte"; 3import Login from "../routes/Login.svelte"; 4import { 5 clearMocks, 6 jsonResponse, 7 mockData, 8 mockEndpoint, 9 setupFetchMock, 10 setupIndexedDBMock, 11} from "./mocks.ts"; 12import { _testSetState, type SavedAccount } from "../lib/auth.svelte.ts"; 13import { 14 unsafeAsAccessToken, 15 unsafeAsDid, 16 unsafeAsHandle, 17 unsafeAsRefreshToken, 18} from "../lib/types/branded.ts"; 19import { getToasts } from "../lib/toast.svelte.ts"; 20 21describe("Login", () => { 22 beforeEach(() => { 23 clearMocks(); 24 setupFetchMock(); 25 setupIndexedDBMock(); 26 mockEndpoint( 27 "/oauth/par", 28 () => jsonResponse({ request_uri: "urn:mock:request" }), 29 ); 30 }); 31 32 describe("initial render with no saved accounts", () => { 33 beforeEach(() => { 34 _testSetState({ 35 session: null, 36 loading: false, 37 error: null, 38 savedAccounts: [], 39 }); 40 }); 41 42 it("renders login page with title and OAuth button", async () => { 43 render(Login); 44 await waitFor(() => { 45 expect(screen.getByRole("heading", { name: /sign in/i })) 46 .toBeInTheDocument(); 47 expect(screen.getByRole("button", { name: /sign in/i })) 48 .toBeInTheDocument(); 49 }); 50 }); 51 52 it("shows create account link", async () => { 53 render(Login); 54 await waitFor(() => { 55 expect(screen.getByText(/no account\?/i)).toBeInTheDocument(); 56 expect(screen.getByRole("link", { name: /create/i })).toHaveAttribute( 57 "href", 58 "/app/register", 59 ); 60 }); 61 }); 62 63 it("shows forgot password and lost passkey links", async () => { 64 render(Login); 65 await waitFor(() => { 66 expect(screen.getByRole("link", { name: /forgot password/i })) 67 .toHaveAttribute("href", "/app/reset-password"); 68 expect(screen.getByRole("link", { name: /lost passkey/i })) 69 .toHaveAttribute("href", "/app/request-passkey-recovery"); 70 }); 71 }); 72 }); 73 74 describe("with saved accounts", () => { 75 const savedAccounts: SavedAccount[] = [ 76 { 77 did: unsafeAsDid("did:web:test.tranquil.dev:u:alice"), 78 handle: unsafeAsHandle("alice.test.tranquil.dev"), 79 accessJwt: unsafeAsAccessToken("mock-jwt-alice"), 80 refreshJwt: unsafeAsRefreshToken("mock-refresh-alice"), 81 }, 82 { 83 did: unsafeAsDid("did:web:test.tranquil.dev:u:bob"), 84 handle: unsafeAsHandle("bob.test.tranquil.dev"), 85 accessJwt: unsafeAsAccessToken("mock-jwt-bob"), 86 refreshJwt: unsafeAsRefreshToken("mock-refresh-bob"), 87 }, 88 ]; 89 90 beforeEach(() => { 91 _testSetState({ 92 session: null, 93 loading: false, 94 error: null, 95 savedAccounts, 96 }); 97 mockEndpoint( 98 "com.atproto.server.getSession", 99 () => 100 jsonResponse( 101 mockData.session({ 102 handle: unsafeAsHandle("alice.test.tranquil.dev"), 103 }), 104 ), 105 ); 106 }); 107 108 it("displays saved accounts list", async () => { 109 render(Login); 110 await waitFor(() => { 111 expect(screen.getByText(/@alice\.test\.tranquil\.dev/)) 112 .toBeInTheDocument(); 113 expect(screen.getByText(/@bob\.test\.tranquil\.dev/)) 114 .toBeInTheDocument(); 115 }); 116 }); 117 118 it("shows sign in to another account option", async () => { 119 render(Login); 120 await waitFor(() => { 121 expect(screen.getByText(/sign in to another/i)).toBeInTheDocument(); 122 }); 123 }); 124 125 it("can click on saved account to switch", async () => { 126 render(Login); 127 await waitFor(() => { 128 expect(screen.getByText(/@alice\.test\.tranquil\.dev/)) 129 .toBeInTheDocument(); 130 }); 131 const aliceAccount = screen.getByText(/@alice\.test\.tranquil\.dev/) 132 .closest("[role='button']"); 133 if (aliceAccount) { 134 await fireEvent.click(aliceAccount); 135 } 136 await waitFor(() => { 137 expect(globalThis.location.pathname).toBe("/app/dashboard"); 138 }); 139 }); 140 141 it("can remove saved account with forget button", async () => { 142 render(Login); 143 await waitFor(() => { 144 expect(screen.getByText(/@alice\.test\.tranquil\.dev/)) 145 .toBeInTheDocument(); 146 const forgetButtons = screen.getAllByTitle(/remove/i); 147 expect(forgetButtons.length).toBe(2); 148 }); 149 }); 150 }); 151 152 describe("error handling", () => { 153 it("displays error message as toast when auth state has error", async () => { 154 _testSetState({ 155 session: null, 156 loading: false, 157 error: "OAuth login failed", 158 savedAccounts: [], 159 }); 160 render(Login); 161 await waitFor(() => { 162 const toasts = getToasts(); 163 const errorToast = toasts.find( 164 (t) => t.type === "error" && t.message.includes("OAuth login failed"), 165 ); 166 expect(errorToast).toBeDefined(); 167 }); 168 }); 169 }); 170 171 describe("verification flow", () => { 172 beforeEach(() => { 173 _testSetState({ 174 session: null, 175 loading: false, 176 error: null, 177 savedAccounts: [], 178 }); 179 }); 180 181 it("shows verification form when pending verification exists", () => { 182 render(Login); 183 }); 184 }); 185 186 describe("loading state", () => { 187 it("shows loading state while auth is initializing", () => { 188 _testSetState({ 189 session: null, 190 loading: true, 191 error: null, 192 savedAccounts: [], 193 }); 194 render(Login); 195 }); 196 }); 197});