6db3bffb2f
b5 of the multi-provider email roadmap (frontend). Adds EmailProviderConnections -- one card per provider (Gmail, Outlook/Microsoft 365, generic IMAP) showing connect status and connect/disconnect actions, mounted in SettingsView's Account tab alongside the existing app-login GoogleAuthCard (a separate concern: that card is sign-in identity, this is mailbox linking). Gmail and Microsoft reuse the OAuth-popup + postMessage handshake already built server-side (mirrors Correspondence.tsx's existing Gmail-connect flow). IMAP has no OAuth step, so it's a plain host/port/ssl/username/password form posting to /api/imap/connect, which verifies the connection server-side before storing it. Deliberately NOT touched: the Gmail-specific job-candidate-matching/review UI in Correspondence.tsx and GmailReviewPage.tsx. That backend pipeline (ListJobCandidateMessagesAsync, GmailReviewDecisions) is still Gmail-only by design -- generalising it now would mean building fake UI for capabilities Microsoft/IMAP don't have yet. This is scoped to the piece that's actually provider-neutral: connect/disconnect status. Verified live (backend + frontend dev servers): logged in, confirmed all three /status calls return 200, Gmail connect-url fetch succeeds, IMAP form submit hits /api/imap/connect and surfaces the expected 400 on a bad host. Frontend suite: 25 suites / 57 tests green (2 new). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
73 lines
2.8 KiB
TypeScript
73 lines
2.8 KiB
TypeScript
import React from "react";
|
|
import "@testing-library/jest-dom";
|
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { ToastProvider } from "./toast";
|
|
import { api } from "./api";
|
|
import EmailProviderConnections from "./components/EmailProviderConnections";
|
|
|
|
jest.mock("./api", () => ({
|
|
api: {
|
|
get: jest.fn(),
|
|
post: jest.fn(),
|
|
delete: jest.fn(),
|
|
interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } },
|
|
},
|
|
getApiErrorMessage: (_err: unknown, fallback: string) => fallback,
|
|
}));
|
|
|
|
const mockedApi = api as jest.Mocked<typeof api>;
|
|
|
|
function renderComponent() {
|
|
return render(
|
|
<ToastProvider>
|
|
<EmailProviderConnections />
|
|
</ToastProvider>,
|
|
);
|
|
}
|
|
|
|
describe("EmailProviderConnections", () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it("renders connected state for Gmail and Outlook, disconnected form for IMAP", async () => {
|
|
mockedApi.get.mockImplementation((path: string) => {
|
|
if (path === "/gmail/status") return Promise.resolve({ data: { connected: true, gmailAddress: "me@gmail.test" } });
|
|
if (path === "/microsoft-graph/status") return Promise.resolve({ data: { connected: false } });
|
|
if (path === "/imap/status") return Promise.resolve({ data: { connected: false } });
|
|
return Promise.reject(new Error("unexpected path"));
|
|
});
|
|
|
|
renderComponent();
|
|
|
|
expect(await screen.findByText("me@gmail.test")).toBeInTheDocument();
|
|
expect(screen.getByLabelText("IMAP host")).toBeInTheDocument();
|
|
expect(screen.getAllByText("Not connected").length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("submits IMAP connect form and reloads status on success", async () => {
|
|
mockedApi.get.mockImplementation((path: string) => {
|
|
if (path === "/gmail/status") return Promise.resolve({ data: { connected: false } });
|
|
if (path === "/microsoft-graph/status") return Promise.resolve({ data: { connected: false } });
|
|
if (path === "/imap/status") return Promise.resolve({ data: { connected: false } });
|
|
return Promise.reject(new Error("unexpected path"));
|
|
});
|
|
mockedApi.post.mockResolvedValueOnce({ data: { username: "user@example.test" } });
|
|
|
|
renderComponent();
|
|
await screen.findByLabelText("IMAP host");
|
|
|
|
await userEvent.type(screen.getByLabelText("IMAP host"), "imap.example.test");
|
|
await userEvent.type(screen.getByLabelText("Username"), "user@example.test");
|
|
await userEvent.type(screen.getByLabelText("Password"), "secret");
|
|
await userEvent.click(screen.getByRole("button", { name: /connect imap account/i }));
|
|
|
|
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith("/imap/connect", expect.objectContaining({
|
|
host: "imap.example.test",
|
|
username: "user@example.test",
|
|
password: "secret",
|
|
})));
|
|
});
|
|
});
|