184 lines
6.1 KiB
TypeScript
184 lines
6.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { NextRequest } from "next/server";
|
|
import { GET, POST } from "@/app/api/session/route";
|
|
import { SESSION_COOKIE } from "@/lib/cookie";
|
|
import { getSession } from "@/lib/session-store";
|
|
|
|
// A fetch stub that walks the three Minnect calls. `overrides` lets a test
|
|
// force a non-200 or a weird body at any step.
|
|
function makeFetchStub(overrides: Partial<Record<"login" | "me" | "feed", Response>> = {}) {
|
|
return vi.fn(async (input: RequestInfo | URL): Promise<Response> => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
|
|
if (url.endsWith("/api/v4/auth/login")) {
|
|
return (
|
|
overrides.login ??
|
|
new Response(
|
|
JSON.stringify({
|
|
data: { accessToken: "profile-access", refreshToken: "profile-refresh", expiresIn: 3600 },
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } },
|
|
)
|
|
);
|
|
}
|
|
if (url.endsWith("/api/v4/auth/me")) {
|
|
return (
|
|
overrides.me ??
|
|
new Response(
|
|
JSON.stringify({
|
|
data: {
|
|
userId: "user-123",
|
|
email: "person@example.com",
|
|
firstName: "Pat",
|
|
lastName: "Rivera",
|
|
profilePictureUrl: "https://cdn.example.com/pat.jpg",
|
|
role: "user",
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } },
|
|
)
|
|
);
|
|
}
|
|
if (url.endsWith("/api/v4/auth/external-services/feed")) {
|
|
return (
|
|
overrides.feed ??
|
|
new Response(JSON.stringify({ accessToken: "feed-access" }), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
})
|
|
);
|
|
}
|
|
throw new Error(`unexpected fetch to ${url}`);
|
|
});
|
|
}
|
|
|
|
function postRequest(body: unknown): NextRequest {
|
|
return new NextRequest("http://localhost/api/session", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("POST /api/session", () => {
|
|
it("wrong password: 401, generic message, no cookie, no session", async () => {
|
|
const serverError = {
|
|
data: null,
|
|
errors: [{ message: "User not found in our records", code: "UnauthorizedException" }],
|
|
};
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
makeFetchStub({
|
|
login: new Response(JSON.stringify(serverError), {
|
|
status: 401,
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const res = await POST(postRequest({ email: "person@example.com", password: "wrong" }));
|
|
const json = await res.json();
|
|
|
|
expect(res.status).toBe(401);
|
|
expect(json).toEqual({ error: "Email or password is incorrect." });
|
|
// The Minnect message must not leak through.
|
|
expect(JSON.stringify(json)).not.toContain("records");
|
|
// No cookie is set.
|
|
expect(res.cookies.get(SESSION_COOKIE)).toBeUndefined();
|
|
});
|
|
|
|
it("missing fields: 400, no cookie", async () => {
|
|
vi.stubGlobal("fetch", makeFetchStub());
|
|
const res = await POST(postRequest({ email: "person@example.com" }));
|
|
expect(res.status).toBe(400);
|
|
expect(res.cookies.get(SESSION_COOKIE)).toBeUndefined();
|
|
});
|
|
|
|
it("correct login: 200, person body, opaque session cookie, server session created", async () => {
|
|
vi.stubGlobal("fetch", makeFetchStub());
|
|
|
|
const res = await POST(postRequest({ email: "person@example.com", password: "right" }));
|
|
const json = await res.json();
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(json).toEqual({
|
|
name: "Pat Rivera",
|
|
email: "person@example.com",
|
|
pictureUrl: "https://cdn.example.com/pat.jpg",
|
|
});
|
|
|
|
const cookie = res.cookies.get(SESSION_COOKIE);
|
|
expect(cookie).toBeDefined();
|
|
expect(cookie?.httpOnly).toBe(true);
|
|
expect(cookie?.sameSite).toBe("lax");
|
|
// The cookie is an opaque id, not a Minnect token.
|
|
const id = cookie!.value;
|
|
expect(id).not.toContain("profile-access");
|
|
expect(id).not.toContain("feed-access");
|
|
|
|
// The tokens live server-side only.
|
|
const session = getSession(id);
|
|
expect(session?.profileAccessToken).toBe("profile-access");
|
|
expect(session?.profileRefreshToken).toBe("profile-refresh");
|
|
expect(session?.feedAccessToken).toBe("feed-access");
|
|
});
|
|
|
|
it("feed-token exchange fails: 502, feed message, no cookie", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
makeFetchStub({
|
|
feed: new Response(JSON.stringify({ data: null }), {
|
|
status: 500,
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
}),
|
|
);
|
|
const res = await POST(postRequest({ email: "person@example.com", password: "right" }));
|
|
const json = await res.json();
|
|
expect(res.status).toBe(502);
|
|
expect(json).toEqual({ error: "Could not open the feed." });
|
|
expect(res.cookies.get(SESSION_COOKIE)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("GET /api/session", () => {
|
|
it("no cookie: 401 sign_in", async () => {
|
|
const res = await GET(new NextRequest("http://localhost/api/session"));
|
|
const json = await res.json();
|
|
expect(res.status).toBe(401);
|
|
expect(json).toEqual({ error: "sign_in" });
|
|
});
|
|
|
|
it("unknown session id: 401 sign_in", async () => {
|
|
const res = await GET(
|
|
new NextRequest("http://localhost/api/session", {
|
|
headers: { cookie: `${SESSION_COOKIE}=does-not-exist` },
|
|
}),
|
|
);
|
|
const json = await res.json();
|
|
expect(res.status).toBe(401);
|
|
expect(json).toEqual({ error: "sign_in" });
|
|
});
|
|
|
|
it("valid session: returns the person's name and email", async () => {
|
|
vi.stubGlobal("fetch", makeFetchStub());
|
|
const postRes = await POST(postRequest({ email: "person@example.com", password: "right" }));
|
|
const id = postRes.cookies.get(SESSION_COOKIE)!.value;
|
|
|
|
const res = await GET(
|
|
new NextRequest("http://localhost/api/session", {
|
|
headers: { cookie: `${SESSION_COOKIE}=${id}` },
|
|
}),
|
|
);
|
|
const json = await res.json();
|
|
expect(res.status).toBe(200);
|
|
expect(json.name).toBe("Pat Rivera");
|
|
expect(json.email).toBe("person@example.com");
|
|
});
|
|
});
|