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
This commit is contained in:
225
packages/core/tests/unit/auth/passkey-config.test.ts
Normal file
225
packages/core/tests/unit/auth/passkey-config.test.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user