Files
emdash-patch-imageupload/packages/core/tests/unit/auth/passkey-config.test.ts
kunthawat 2d1be52177 Emdash source with visual editor image upload fix
Fixes:
1. media.ts: wrap placeholder generation in try-catch
2. toolbar.ts: check r.ok, display error message in popover
2026-05-03 10:44:54 +07:00

226 lines
8.1 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { getPasskeyConfig } from "../../../src/auth/passkey-config.js";
/** URL shape from `new URL(request.url)` after trusted proxy + Astro `security.allowedDomains`. */
function urlAfterTrustedProxy(path: string, host: string, proto: "http" | "https"): URL {
return new URL(path, `${proto}://${host}`);
}
describe("passkey-config", () => {
describe("getPasskeyConfig() via emulated reverse proxy URL", () => {
const internalDevUrl = "http://127.0.0.1:4321/_emdash/api/auth/passkey/register/options";
it("loopback URL alone matches Node before rewrite — rpId is not the public host", () => {
const url = new URL(internalDevUrl);
expect(getPasskeyConfig(url).rpId).toBe("127.0.0.1");
});
it("emits a single-element origins array by default", () => {
const url = new URL(internalDevUrl);
expect(getPasskeyConfig(url).origins).toEqual(["http://127.0.0.1:4321"]);
});
it("forwarded Host/Proto yield the URL handlers see; rp matches HTTP reverse-proxy edge", () => {
const url = urlAfterTrustedProxy(
"/_emdash/api/auth/passkey/register/options",
"emdash.local:8080",
"http",
);
const config = getPasskeyConfig(url, "My Site");
expect(config.rpId).toBe("emdash.local");
expect(config.rpName).toBe("My Site");
expect(config.origins[0]).toBe("http://emdash.local:8080");
});
it("HTTPS listener on proxy with HTTP upstream: siteUrl aligns origin with browser", () => {
const urlAstroSeesFromForwardedHttp = urlAfterTrustedProxy(
"/_emdash/api/setup/admin",
"emdash.local:8080",
"http",
);
const browserOrigin = "https://emdash.local:8443";
const config = getPasskeyConfig(urlAstroSeesFromForwardedHttp, "My Site", browserOrigin);
expect(config.rpId).toBe("emdash.local");
expect(config.rpName).toBe("My Site");
expect(config.origins[0]).toBe(browserOrigin);
});
});
describe("getPasskeyConfig()", () => {
it("throws when siteUrl is not a valid URL", () => {
const url = new URL("http://localhost:4321/admin");
expect(() => getPasskeyConfig(url, "Site", "::not-a-url")).toThrow("Invalid siteUrl");
});
it("extracts rpId from localhost URL", () => {
const url = new URL("http://localhost:4321/admin");
const config = getPasskeyConfig(url);
expect(config.rpId).toBe("localhost");
});
it("extracts rpId from production URL", () => {
const url = new URL("https://example.com/admin");
const config = getPasskeyConfig(url);
expect(config.rpId).toBe("example.com");
});
it("extracts rpId from subdomain URL", () => {
const url = new URL("https://admin.example.com/dashboard");
const config = getPasskeyConfig(url);
expect(config.rpId).toBe("admin.example.com");
});
it("returns correct origin for http", () => {
const url = new URL("http://localhost:4321/admin");
const config = getPasskeyConfig(url);
expect(config.origins[0]).toBe("http://localhost:4321");
});
it("returns correct origin for https", () => {
const url = new URL("https://example.com/admin");
const config = getPasskeyConfig(url);
expect(config.origins[0]).toBe("https://example.com");
});
it("handles port numbers correctly", () => {
const url = new URL("http://localhost:3000/setup");
const config = getPasskeyConfig(url);
expect(config.rpId).toBe("localhost");
expect(config.origins[0]).toBe("http://localhost:3000");
});
it("handles https with non-standard port", () => {
const url = new URL("https://staging.example.com:8443/admin");
const config = getPasskeyConfig(url);
expect(config.rpId).toBe("staging.example.com");
expect(config.origins[0]).toBe("https://staging.example.com:8443");
});
it("uses hostname as rpName by default", () => {
const url = new URL("https://example.com/admin");
const config = getPasskeyConfig(url);
expect(config.rpName).toBe("example.com");
});
it("uses provided siteName for rpName", () => {
const url = new URL("https://example.com/admin");
const config = getPasskeyConfig(url, "My Cool Site");
expect(config.rpName).toBe("My Cool Site");
expect(config.rpId).toBe("example.com");
});
it("ignores path and query params for origin", () => {
const url = new URL("https://example.com:443/admin/setup?foo=bar#section");
const config = getPasskeyConfig(url);
// Standard https port 443 is omitted from origin
expect(config.origins[0]).toBe("https://example.com");
expect(config.rpId).toBe("example.com");
});
it("documents HTTPS reverse-proxy dev pitfall: server URL scheme must match the browser", () => {
const serverDevUrl = new URL("http://emdash.local:8443/_emdash/api/setup/admin");
const browserPageOrigin = new URL("https://emdash.local:8443/_emdash/admin/setup");
const fromServer = getPasskeyConfig(serverDevUrl);
const fromBrowser = getPasskeyConfig(browserPageOrigin);
expect(fromServer.rpId).toBe(fromBrowser.rpId);
expect(fromServer.origins[0]).toBe("http://emdash.local:8443");
expect(fromBrowser.origins[0]).toBe("https://emdash.local:8443");
// verifyRegistrationResponse requires clientData.origin === config.origins[0] (see @emdash-cms/auth/passkey)
expect(fromServer.origins[0]).not.toBe(fromBrowser.origins[0]);
});
it("siteUrl overrides origin and rpId (TLS termination and loopback request URL)", () => {
const fromForwardedHttp = getPasskeyConfig(
new URL("http://emdash.local:8443/_emdash/api/setup/admin"),
"My Site",
"https://emdash.local:8443",
);
expect(fromForwardedHttp.rpName).toBe("My Site");
expect(fromForwardedHttp.rpId).toBe("emdash.local");
expect(fromForwardedHttp.origins[0]).toBe("https://emdash.local:8443");
const fromLoopback = getPasskeyConfig(
new URL("http://127.0.0.1:4321/_emdash/api/setup/admin"),
"My CMS",
"https://public.example:8443",
);
expect(fromLoopback.rpId).toBe("public.example");
expect(fromLoopback.rpName).toBe("My CMS");
expect(fromLoopback.origins[0]).toBe("https://public.example:8443");
const hostnameOnly = getPasskeyConfig(
new URL("http://127.0.0.1:4321/x"),
undefined,
"https://public.example:8443",
);
expect(hostnameOnly.rpName).toBe("public.example");
expect(hostnameOnly.rpId).toBe("public.example");
});
});
describe("getPasskeyConfig() multi-origin", () => {
it("appends allowedOrigins after the canonical origin", () => {
const url = new URL("http://localhost:4321/admin");
const config = getPasskeyConfig(url, "Site", "https://example.com", [
"https://preview.example.com",
"https://staging.example.com",
]);
expect(config.rpId).toBe("example.com");
expect(config.origins).toEqual([
"https://example.com",
"https://preview.example.com",
"https://staging.example.com",
]);
});
it("places the canonical origin first when no siteUrl is set", () => {
const url = new URL("https://preview.example.com/admin");
const config = getPasskeyConfig(url, undefined, undefined, ["https://example.com"]);
// rpId is preview.example.com (no siteUrl); allow apex as second origin
expect(config.rpId).toBe("preview.example.com");
expect(config.origins).toEqual(["https://preview.example.com", "https://example.com"]);
});
it("dedupes if allowedOrigins repeats the canonical origin", () => {
const url = new URL("http://localhost:4321/admin");
const config = getPasskeyConfig(url, undefined, "https://example.com", [
"https://example.com",
"https://preview.example.com",
]);
expect(config.origins).toEqual(["https://example.com", "https://preview.example.com"]);
});
it("ignores empty/falsy entries in allowedOrigins", () => {
const url = new URL("http://localhost:4321/admin");
const config = getPasskeyConfig(url, undefined, "https://example.com", [
"",
"https://preview.example.com",
]);
expect(config.origins).toEqual(["https://example.com", "https://preview.example.com"]);
});
it("yields a single-element origins array when allowedOrigins is empty", () => {
const url = new URL("http://localhost:4321/admin");
const config = getPasskeyConfig(url, undefined, "https://example.com", []);
expect(config.origins).toEqual(["https://example.com"]);
});
});
});