Files
clean-architecture-frontend…/tests/integration/auth-recovery.test.js
T

125 lines
3.8 KiB
JavaScript

import { HttpResponse, http } from "msw";
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { createExternalAuthSessionAdapter } from "../../src/adapters/auth/external-session-adapter.js";
import { createHttpClient } from "../../src/adapters/http/client.js";
import { TEST_HTTP_CONTRACT } from "../helpers/http-contract-fixture.js";
/** @type {number[]} */
let responseStatuses = [];
const server = setupServer(
http.get("https://api.test/api/entities", () => {
const status = responseStatuses.shift() ?? 200;
if (status === 401) {
return HttpResponse.json(
{
success: false,
error: { code: "UNAUTHENTICATED" },
meta: { requestId: "request-1", traceId: "trace-1" },
},
{ status },
);
}
return HttpResponse.json({
success: true,
data: [{ id: "resource-1", name: "Example" }],
meta: { requestId: "request-2", traceId: "trace-1" },
});
}),
);
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => {
responseStatuses = [];
server.resetHandlers();
});
afterAll(() => server.close());
const clock = { now: () => 0, sleep: async () => {} };
/** @param {Parameters<typeof createHttpClient>[0]} options */
function testClient(options) {
return createHttpClient({ ...TEST_HTTP_CONTRACT, ...options });
}
/**
* @param {Partial<Parameters<typeof createExternalAuthSessionAdapter>[0]>} overrides
* @returns {Parameters<typeof createExternalAuthSessionAdapter>[0]}
*/
function createOwner(overrides = {}) {
return {
readState: () => "authenticated",
subscribe: () => () => {},
beginSignIn: async () => {},
signOut: async () => {},
attachCredential: async (request) => request,
recoverSession: async () => "restored",
notifyUnauthenticated: () => {},
...overrides,
};
}
describe("bounded 401 session recovery", () => {
it("calls recovery once and replays a safe request once", async () => {
responseStatuses = [401, 200];
const recoverSession = vi.fn(async () => /** @type {const} */ ("restored"));
const authSession = createExternalAuthSessionAdapter(createOwner({
attachCredential: async (request) => request,
recoverSession,
notifyUnauthenticated: vi.fn(),
}));
const client = testClient({
baseUrl: "https://api.test",
authSession,
clock,
});
await expect(client.execute("LIST_ENTITIES")).resolves.toMatchObject({
ok: true,
});
expect(recoverSession).toHaveBeenCalledTimes(1);
});
it("stops after a second 401 and notifies unauthenticated once", async () => {
responseStatuses = [401, 401];
const notifyUnauthenticated = vi.fn();
const authSession = createExternalAuthSessionAdapter(createOwner({
attachCredential: async (request) => request,
recoverSession: async () => "restored",
notifyUnauthenticated,
}));
const client = testClient({
baseUrl: "https://api.test",
authSession,
clock,
});
await expect(client.execute("LIST_ENTITIES")).resolves.toMatchObject({
ok: false,
error: { kind: "AUTH_REQUIRED" },
});
expect(notifyUnauthenticated).toHaveBeenCalledTimes(1);
});
it("normalizes attach and invalid recovery failures", async () => {
const attachFailure = createExternalAuthSessionAdapter(createOwner({
attachCredential: async () => {
throw new Error("credential detail");
},
recoverSession: async () => "restored",
notifyUnauthenticated: vi.fn(),
}));
const client = testClient({
baseUrl: "https://api.test",
authSession: attachFailure,
clock,
});
await expect(client.execute("LIST_ENTITIES")).resolves.toMatchObject({
ok: false,
error: { kind: "AUTH_INTEGRATION_FAILURE" },
});
});
});