refactor(runtime): replace Bun dependency with Node fetch
Run Perplexity search and auth requests through pi's Node runtime instead of shelling out to Bun. Move development and CI commands to npm, add a Node test build path, and keep OTP auth fail-fast when Set-Cookie headers are not available.
This commit is contained in:
+12
-3
@@ -1,4 +1,4 @@
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test";
|
||||
import { afterEach, describe, expect, mock, test } from "../test-helpers.js";
|
||||
|
||||
import { AuthError, type StoredToken } from "../../src/search/types.js";
|
||||
|
||||
@@ -17,8 +17,15 @@ function createOpaqueToken(): string {
|
||||
return "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0.part2.part3.part4.part5";
|
||||
}
|
||||
|
||||
function csrfHeaders(): [string, string][] {
|
||||
return [
|
||||
["content-type", "application/json"],
|
||||
["set-cookie", "next-auth.csrf-token=csrf-cookie; Path=/; HttpOnly"],
|
||||
];
|
||||
}
|
||||
|
||||
async function importLoginModule() {
|
||||
return import(`../../src/auth/login.ts?test=${crypto.randomUUID()}`);
|
||||
return import(`../../src/auth/login.js?test=${crypto.randomUUID()}`);
|
||||
}
|
||||
|
||||
function restoreEnv(): void {
|
||||
@@ -178,7 +185,7 @@ describe("auth/login", () => {
|
||||
if (url.endsWith("/csrf")) {
|
||||
return new Response(JSON.stringify({ csrfToken: "csrf-token" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
headers: csrfHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -221,6 +228,8 @@ describe("auth/login", () => {
|
||||
|
||||
const signinEmailRequest = fetchMock.mock.calls[1]?.[1] as RequestInit;
|
||||
const signinOtpRequest = fetchMock.mock.calls[2]?.[1] as RequestInit;
|
||||
expect(new Headers(signinEmailRequest.headers).get("Cookie")).toBe("next-auth.csrf-token=csrf-cookie");
|
||||
expect(new Headers(signinOtpRequest.headers).get("Cookie")).toBe("next-auth.csrf-token=csrf-cookie");
|
||||
expect(JSON.parse(String(signinEmailRequest.body))).toEqual({
|
||||
email: "user@example.com",
|
||||
csrfToken: "csrf-token",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* OTP login flow tests derived from real captured request/response data.
|
||||
* See scripts/debug-login-dump.json for the raw fixture.
|
||||
*/
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test";
|
||||
import { afterEach, describe, expect, mock, test } from "../test-helpers.js";
|
||||
|
||||
import { AuthError, type StoredToken } from "../../src/search/types.js";
|
||||
|
||||
@@ -21,11 +21,23 @@ const CSRF_TOKEN = "0e4f8cc491e3197788492604ad32577f2022747fe30e0f51ba3ba235f07c
|
||||
|
||||
const TEST_EMAIL = "user@test.com";
|
||||
const TEST_OTP = "9f3e2-knzol";
|
||||
const CSRF_COOKIES = [
|
||||
"next-auth.csrf-token=csrf-cookie; Path=/; HttpOnly",
|
||||
"cf_clearance=clearance-cookie; Path=/; Secure",
|
||||
];
|
||||
const CSRF_COOKIE_HEADER = "next-auth.csrf-token=csrf-cookie; cf_clearance=clearance-cookie";
|
||||
|
||||
// ---
|
||||
|
||||
function jsonHeaders(cookies: string[] = []): [string, string][] {
|
||||
return [
|
||||
["content-type", "application/json; charset=utf-8"],
|
||||
...cookies.map((cookie): [string, string] => ["set-cookie", cookie]),
|
||||
];
|
||||
}
|
||||
|
||||
async function importLoginModule() {
|
||||
return import(`../../src/auth/login.ts?test=${crypto.randomUUID()}`);
|
||||
return import(`../../src/auth/login.js?test=${crypto.randomUUID()}`);
|
||||
}
|
||||
|
||||
function restoreEnv(): void {
|
||||
@@ -74,21 +86,21 @@ function buildReplayFetchMock(options?: {
|
||||
if (url.endsWith("/csrf")) {
|
||||
return new Response(JSON.stringify({ csrfToken: CSRF_TOKEN }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json; charset=utf-8" },
|
||||
headers: jsonHeaders(CSRF_COOKIES),
|
||||
});
|
||||
}
|
||||
|
||||
if (url.endsWith("/signin-email")) {
|
||||
return new Response(JSON.stringify({ success: "Email sign in triggered" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json; charset=utf-8" },
|
||||
headers: jsonHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
if (url.endsWith("/signin-otp")) {
|
||||
return new Response(JSON.stringify(otpBody), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json; charset=utf-8" },
|
||||
headers: jsonHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -165,15 +177,17 @@ describe("OTP login flow (from real captured responses)", () => {
|
||||
expect(calls[0].init?.method ?? "GET").toBe("GET");
|
||||
expect(calls[0].init?.body).toBeFalsy();
|
||||
|
||||
// signin-email: POST with email + csrfToken
|
||||
// signin-email: POST with email + csrfToken + CSRF cookies
|
||||
expect(calls[1].init?.method).toBe("POST");
|
||||
expect(new Headers(calls[1].init?.headers).get("Cookie")).toBe(CSRF_COOKIE_HEADER);
|
||||
expect(JSON.parse(String(calls[1].init?.body))).toEqual({
|
||||
email: TEST_EMAIL,
|
||||
csrfToken: CSRF_TOKEN,
|
||||
});
|
||||
|
||||
// signin-otp: POST with email + otp + csrfToken
|
||||
// signin-otp: POST with email + otp + csrfToken + CSRF cookies
|
||||
expect(calls[2].init?.method).toBe("POST");
|
||||
expect(new Headers(calls[2].init?.headers).get("Cookie")).toBe(CSRF_COOKIE_HEADER);
|
||||
expect(JSON.parse(String(calls[2].init?.body))).toEqual({
|
||||
email: TEST_EMAIL,
|
||||
otp: TEST_OTP,
|
||||
@@ -246,6 +260,34 @@ describe("OTP login flow (from real captured responses)", () => {
|
||||
expect((thrown as AuthError).code).toBe("NO_TOKEN");
|
||||
});
|
||||
|
||||
test("throws when CSRF response does not include auth cookies", async () => {
|
||||
process.env.PI_AUTH_NO_BORROW = "1";
|
||||
|
||||
mockStorage();
|
||||
|
||||
const fetchMock = mock(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("/csrf")) {
|
||||
return new Response(JSON.stringify({ csrfToken: CSRF_TOKEN }), {
|
||||
status: 200,
|
||||
headers: jsonHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
return new Response("should not continue", { status: 500 });
|
||||
});
|
||||
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||
|
||||
const { authenticate } = await importLoginModule();
|
||||
|
||||
await expect(authenticate({ promptForEmail: async () => TEST_EMAIL })).rejects.toMatchObject({
|
||||
name: "AuthError",
|
||||
code: "EXTRACTION_FAILED",
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("throws when OTP verification returns non-200", async () => {
|
||||
process.env.PI_AUTH_NO_BORROW = "1";
|
||||
|
||||
@@ -257,14 +299,14 @@ describe("OTP login flow (from real captured responses)", () => {
|
||||
if (url.endsWith("/csrf")) {
|
||||
return new Response(JSON.stringify({ csrfToken: CSRF_TOKEN }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
headers: jsonHeaders(CSRF_COOKIES),
|
||||
});
|
||||
}
|
||||
|
||||
if (url.endsWith("/signin-email")) {
|
||||
return new Response(JSON.stringify({ success: "Email sign in triggered" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
headers: jsonHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test";
|
||||
import { afterEach, describe, expect, mock, test } from "../test-helpers.js";
|
||||
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
async function importStorageModule() {
|
||||
return import(`../../src/auth/storage.ts?test=${crypto.randomUUID()}`);
|
||||
return import(`../../src/auth/storage.js?test=${crypto.randomUUID()}`);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
Reference in New Issue
Block a user