Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
226 lines
8.1 KiB
TypeScript
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"]);
|
|
});
|
|
});
|
|
});
|