kdlb9zs9n: Scaffold Next.js app and create the server session from a Minnect login
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user