import * as React from "react"; import { setupServer } from "msw/node"; import { http, HttpResponse } from "msw"; import { QueryClient } from "@tanstack/react-query"; import { renderWithProviders } from "../testUtils/withProviders"; import DebugAuthentication from "../../../src/components/DebugAuthentication"; import { AuthMethodsResponse, PatronDebugResponse, } from "../../../src/api/patronDebug"; import { waitFor, fireEvent } from "@testing-library/react"; const LIBRARY = "test-library"; const SECOND_LIBRARY = "another-library"; const AUTH_METHODS_PATH = `/${LIBRARY}/admin/manage_patrons/auth_methods`; const DEBUG_AUTH_PATH = `/${LIBRARY}/admin/manage_patrons/debug_auth`; const SECOND_AUTH_METHODS_PATH = `/${SECOND_LIBRARY}/admin/manage_patrons/auth_methods`; const MOCK_AUTH_METHODS: AuthMethodsResponse = { authMethods: [ { id: 1, name: "Test SIP2", protocol: "api.sip", supportsDebug: true, supportsPassword: true, identifierLabel: "Barcode", passwordLabel: "PIN", }, { id: 2, name: "SAML Provider", protocol: "api.saml.provider", supportsDebug: false, supportsPassword: false, identifierLabel: "Username", passwordLabel: "Password", }, ], }; const MOCK_DEBUG_RESULTS: PatronDebugResponse = { results: [ { label: "Server-Side Validation", success: true, details: "ok" }, { label: "SIP2 Connection", success: true, details: "sip.example.com:6001", }, { label: "Password Validation", success: false, details: "valid_patron_password=N", }, ], }; describe("DebugAuthentication", () => { /* eslint-disable @typescript-eslint/no-empty-function */ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, }, logger: { log: console.log, warn: console.warn, error: process.env.NODE_ENV === "test" ? () => {} : console.error, }, }); /* eslint-enable @typescript-eslint/no-empty-function */ const server = setupServer( http.get(AUTH_METHODS_PATH, () => HttpResponse.json(MOCK_AUTH_METHODS, { status: 200 }) ), http.post(DEBUG_AUTH_PATH, () => HttpResponse.json(MOCK_DEBUG_RESULTS, { status: 200 }) ) ); beforeAll(() => server.listen()); afterEach(() => { server.resetHandlers(); queryClient.clear(); }); afterAll(() => server.close()); const renderComponent = () => { return renderWithProviders( , { queryClient } ); }; it("displays loading state initially", () => { const { getByText } = renderComponent(); expect(getByText("Loading authentication methods...")).toBeInTheDocument(); }); it("renders auth method dropdown after loading", async () => { const { getByLabelText, getByText } = renderComponent(); await waitFor(() => { expect(getByLabelText("Authentication Method")).toBeInTheDocument(); }); expect(getByText("Test SIP2 (api.sip)")).toBeInTheDocument(); expect(getByText("SAML Provider (api.saml.provider)")).toBeInTheDocument(); }); it("shows 'not supported' message for non-debug methods", async () => { const { getByLabelText, getByText } = renderComponent(); await waitFor(() => { expect(getByLabelText("Authentication Method")).toBeInTheDocument(); }); fireEvent.change(getByLabelText("Authentication Method"), { target: { value: "2" }, }); expect( getByText( "Debug authentication is not supported for this authentication method." ) ).toBeInTheDocument(); }); it("shows form fields for debug-capable method", async () => { const { getByLabelText, getByText } = renderComponent(); await waitFor(() => { expect(getByLabelText("Authentication Method")).toBeInTheDocument(); }); fireEvent.change(getByLabelText("Authentication Method"), { target: { value: "1" }, }); // Should show fields with the method's labels expect(getByLabelText("Barcode")).toBeInTheDocument(); expect(getByLabelText("PIN")).toBeInTheDocument(); expect(getByText("Run Debug")).toBeInTheDocument(); }); it("runs debug and displays results", async () => { const { getByLabelText, getByText } = renderComponent(); await waitFor(() => { expect(getByLabelText("Authentication Method")).toBeInTheDocument(); }); // Select the SIP2 method fireEvent.change(getByLabelText("Authentication Method"), { target: { value: "1" }, }); // Fill in the form fireEvent.change(getByLabelText("Barcode"), { target: { value: "12345" }, }); fireEvent.change(getByLabelText("PIN"), { target: { value: "1111" }, }); // Submit fireEvent.click(getByText("Run Debug")); // Wait for results await waitFor(() => { expect(getByText("Server-Side Validation")).toBeInTheDocument(); }); expect(getByText("SIP2 Connection")).toBeInTheDocument(); expect(getByText("Password Validation")).toBeInTheDocument(); }); it("auto-selects and hides dropdown when there is only one method", async () => { const singleMethod: AuthMethodsResponse = { authMethods: [MOCK_AUTH_METHODS.authMethods[0]], }; server.use( http.get(AUTH_METHODS_PATH, () => HttpResponse.json(singleMethod, { status: 200 }) ) ); const { getByLabelText, queryByLabelText } = renderComponent(); // The form fields should appear automatically without selecting a method. await waitFor(() => { expect(getByLabelText("Barcode")).toBeInTheDocument(); }); expect(getByLabelText("PIN")).toBeInTheDocument(); // The dropdown should not be rendered. expect(queryByLabelText("Authentication Method")).not.toBeInTheDocument(); }); it("reconciles selected method when switching to a different library", async () => { const singleMethodForSecondLibrary: AuthMethodsResponse = { authMethods: [ { id: 99, name: "Second Library API", protocol: "api.second", supportsDebug: true, supportsPassword: false, identifierLabel: "Email", passwordLabel: "Password", }, ], }; server.use( http.get(SECOND_AUTH_METHODS_PATH, () => HttpResponse.json(singleMethodForSecondLibrary, { status: 200 }) ) ); const { getByLabelText, queryByLabelText, rerender, } = renderWithProviders( , { queryClient } ); await waitFor(() => { expect(getByLabelText("Authentication Method")).toBeInTheDocument(); }); fireEvent.change(getByLabelText("Authentication Method"), { target: { value: "1" }, }); expect(getByLabelText("Barcode")).toBeInTheDocument(); rerender( ); await waitFor(() => { expect(getByLabelText("Email")).toBeInTheDocument(); }); expect(queryByLabelText("Authentication Method")).not.toBeInTheDocument(); }); it("shows warning when library has no auth methods", async () => { server.use( http.get(AUTH_METHODS_PATH, () => HttpResponse.json({ authMethods: [] }, { status: 200 }) ) ); const { getByText, queryByLabelText } = renderComponent(); await waitFor(() => { expect( getByText( "This library has no patron authentication integrations configured." ) ).toBeInTheDocument(); }); expect(queryByLabelText("Authentication Method")).not.toBeInTheDocument(); }); it("handles API error on fetching auth methods", async () => { server.use( http.get(AUTH_METHODS_PATH, () => new HttpResponse(null, { status: 500 })) ); const { getByText } = renderComponent(); await waitFor( () => { expect( getByText(/Error loading authentication methods/) ).toBeInTheDocument(); }, { timeout: 5000 } ); }); });