import { http, HttpResponse } from "msw"; import preview from "../../.storybook/preview.tsx"; import { sleep } from "../sleep.ts"; import type { CurrentUser, SessionInfo } from "../types.ts"; import { CurrentUserFetcher } from "./CurrentUserFetcher.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 vernon: CurrentUser = { id: "vernon", email: "vernon@example.com", isVerified: false, }; const sessionInfo: SessionInfo = { createdTs: 123, expiresTs: 123, }; return { data: { john, mary, vernon }, handlers: { refreshTokens: { success: () => { return http.post( "http://mock.api/v1/sessions/access-tokens", async () => { await simulateNetwork(); return HttpResponse.json({ sessionInfo }); }, ); }, 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 }); }); }, }, requestVerify: { success: () => { return http.post( "http://mock.api/v1/user-verification-tokens", async () => { await simulateNetwork(); return HttpResponse.json({ message: "OK", tokenId: "1" }); }, ); }, }, verify: { success: () => { return http.put( "http://mock.api/v1/user-verification-tokens/:id", async () => { await simulateNetwork(); return HttpResponse.json({ message: "OK" }); }, ); }, }, }, }; } const { data, handlers } = createMocks({ responseSpeed: 700 }); const meta = preview.meta({ title: "Account/Current User Fetcher", component: CurrentUserFetcher, tags: ["autodocs"], args: { children: (
App content
), }, parameters: { msw: { handlers: { refreshTokens: handlers.refreshTokens.failed(), }, }, }, }); /** * The default case, in which a local user is provided (so they have previously * logged in), and the current user request returns the same user as is * the local user (determined by the user id). */ export const Default = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.success(data.john), }, }, }, }); /** * In this case, no local user is provided and the component simply renders * its children. There should be no network request in this case. */ export const NoLocalUser = meta.story({}); /** * In this case, the local user is provided and the current user request returns * error 401. The user should supply their credentials again. */ export const SessionExpired = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.notAuthenticated(), }, }, }, }); /** * In this case, the local user is provided and the current user request returns * a different user (determined by user ID). * * This case can happen when user A logs into app X, then user B logs into * app Y. Returning to app X, user B will be logged into ITC with their account * but local data belong to user A. * * In practice this should be a rare case, but with unpleasant circumstances * if not handled correctly. */ export const UserMismatch = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.success(data.mary), }, }, }, }); /** */ export const UserUnverified = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.success(data.vernon), requestVerify: handlers.requestVerify.success(), verify: handlers.verify.success(), refreshTokens: handlers.refreshTokens.success(), }, }, }, }); /** * In this case, the local user is provided and the current user request returns * 404, indicating that the current user no longer exists. * * This can happen if users delete their accounts but */ export const UserNotFound = meta.story({ parameters: { msw: { handlers: { getCurrentUser: handlers.getCurrentUser.notFound(), }, }, }, }); /** * 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(), }, }, }, });