import { http, HttpResponse } from "msw"; import { fn } from "storybook/test"; import preview from "../../.storybook/preview.tsx"; import { sleep } from "../sleep.ts"; import type { CurrentUser, SessionInfo } from "../types.ts"; import { LoginCard } from "./LoginCard.tsx"; function createMocks(options?: { responseSpeed?: number }) { const simulateNetwork = () => sleep(options?.responseSpeed ?? 2000); const john: CurrentUser = { id: "john", email: "john@example.com", isVerified: true, }; const mary: CurrentUser = { id: "mary", email: "mary@example.com", isVerified: true, }; const sessionInfo: SessionInfo = { createdTs: 123, expiresTs: 123, }; return { data: { john, mary }, handlers: { refreshTokens: { failed: () => { return http.post( "http://mock.api/v1/sessions/access-tokens", async () => { await simulateNetwork(); return HttpResponse.text("Refresh token expired or missing", { status: 401, }); }, ); }, }, getCurrentUser: { success: (currentUser: CurrentUser) => { return http.get("http://mock.api/v1/users/me", async () => { await simulateNetwork(); return HttpResponse.json(currentUser); }); }, /** * Cookie is valid, but user doesn't exist any more. This can happen * after user deletion. */ notFound: () => { return http.get("http://mock.api/v1/users/me", async () => { await simulateNetwork(); return HttpResponse.text("User not found", { status: 404 }); }); }, noConnection: () => { return http.get("http://mock.api/v1/users/me", async () => { return HttpResponse.error(); }); }, unknownFailure: () => { return http.get("http://mock.api/v1/users/me", async () => { await simulateNetwork(); return HttpResponse.text("Internal server error", { status: 500 }); }); }, /** * Auth cookies no longer valid to make this request. */ notAuthenticated: () => { return http.get("http://mock.api/v1/users/me", async () => { await simulateNetwork(); return HttpResponse.text("Not authenticated", { status: 401 }); }); }, }, createNewSession: { success: (currentUser: CurrentUser) => { return http.post("http://mock.api/v1/sessions", async () => { await simulateNetwork(); return HttpResponse.json({ currentUser, sessionInfo }); }); }, invalidCredentials: () => { return http.post("http://mock.api/v1/sessions", async () => { await simulateNetwork(); return HttpResponse.text("Credentials do not match", { status: 401, }); }); }, userNotFound: () => { return http.post("http://mock.api/v1/sessions", async () => { await simulateNetwork(); return HttpResponse.text("User not found", { status: 404 }); }); }, unknownFailure: () => { return http.post("http://mock.api/v1/sessions", async () => { await simulateNetwork(); return HttpResponse.text("Internal server error", { status: 500 }); }); }, }, }, }; } const { data, handlers } = createMocks({ responseSpeed: 700 }); const meta = preview.meta({ title: "Account/Login Card", component: LoginCard, tags: ["autodocs"], args: { description: "Log in to Indie Tabletop Club to enable backup & sync.", defaultValues: {}, onLogin: fn(), }, parameters: { msw: { handlers: { refreshTokens: handlers.refreshTokens.failed(), }, }, }, }); /** * The majority case where no user is stored locally, proactive user session * check returns 401, and subsequently correct credentials are provided. */ export const Default = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.notAuthenticated(), createNewSession: handlers.createNewSession.success(data.john), }, }, }, }); /** * Similar to the default case, but invalid credentials are provided to when * attempting to create a new session. */ export const InvalidCredentialsOnSubmit = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.notAuthenticated(), createNewSession: handlers.createNewSession.invalidCredentials(), }, }, }, }); /** * Similar to the default case, but when credentials are provided, an account * email is used that is not currently in the database. */ export const UserNotFoundOnSubmit = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.notAuthenticated(), createNewSession: handlers.createNewSession.userNotFound(), }, }, }, }); /** * Similar to the default case, but the session creation call returns an error * that doesn't have a specific meaning in the context of the login page. */ export const UnknownFailureOnSubmit = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.notAuthenticated(), createNewSession: handlers.createNewSession.unknownFailure(), }, }, }, }); /** * A case when user is stored locally, but their server session has expired. */ export const ReauthenticateSession = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.notAuthenticated(), createNewSession: handlers.createNewSession.success(data.john), }, }, }, }); /** * The user is already stored in the app, and proactive user-session check * returns the same user (based on ID). * * The user is directed to app without any further steps. */ export const AlreadyLoggedIn = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.success(data.john), }, }, }, }); /** * A user is provided, and proactive user session check returns a different * user. This can happen when different users log into separate apps with * different credentials. */ export const UserMismatch = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.success(data.mary), }, }, }, }); /** * During the proactive user session check, the user is reportd as not found. * This means that the tokens are still valid, but the user DB entity is gone. * This can happen if a user closed their account. */ export const UserNotFound = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.notFound(), }, }, }, }); /** * The proactive user session check has failed due to connection issues. */ export const NoConnection = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.noConnection(), }, }, }, }); /** * The proactive user session check has failed due to an error that doesn't * carry any special meaning. */ export const UnknownFailure = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.unknownFailure(), }, }, }, });