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:
129
packages/auth-atproto/tests/auth.test.ts
Normal file
129
packages/auth-atproto/tests/auth.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { atproto, type AtprotoAuthConfig } from "../src/auth.js";
|
||||
|
||||
const AUTH_ROUTES_RE = /^@emdash-cms\/auth-atproto\/routes\//;
|
||||
|
||||
describe("atproto auth config", () => {
|
||||
describe("AuthProviderDescriptor contract", () => {
|
||||
it("returns id 'atproto'", () => {
|
||||
const descriptor = atproto();
|
||||
expect(descriptor.id).toBe("atproto");
|
||||
});
|
||||
|
||||
it("has label 'Atmosphere'", () => {
|
||||
const descriptor = atproto();
|
||||
expect(descriptor.label).toBe("Atmosphere");
|
||||
});
|
||||
|
||||
it("points adminEntry to the admin module", () => {
|
||||
const descriptor = atproto();
|
||||
expect(descriptor.adminEntry).toBe("@emdash-cms/auth-atproto/admin");
|
||||
});
|
||||
|
||||
it("defaults config to empty object when no options provided", () => {
|
||||
const descriptor = atproto();
|
||||
expect(descriptor.config).toEqual({});
|
||||
});
|
||||
|
||||
it("defaults config to empty object when undefined is passed", () => {
|
||||
const descriptor = atproto(undefined);
|
||||
expect(descriptor.config).toEqual({});
|
||||
});
|
||||
|
||||
it("declares routes pointing to auth package", () => {
|
||||
const descriptor = atproto();
|
||||
expect(descriptor.routes).toBeDefined();
|
||||
expect(descriptor.routes!.length).toBe(4);
|
||||
for (const route of descriptor.routes!) {
|
||||
expect(route.entrypoint).toMatch(AUTH_ROUTES_RE);
|
||||
}
|
||||
});
|
||||
|
||||
it("declares storage collections for OAuth state", () => {
|
||||
const descriptor = atproto();
|
||||
expect(descriptor.storage).toBeDefined();
|
||||
expect(descriptor.storage).toHaveProperty("states");
|
||||
expect(descriptor.storage).toHaveProperty("sessions");
|
||||
});
|
||||
|
||||
it("declares publicRoutes with specific paths", () => {
|
||||
const descriptor = atproto();
|
||||
expect(descriptor.publicRoutes).toBeDefined();
|
||||
expect(descriptor.publicRoutes).toContain("/_emdash/api/auth/atproto/");
|
||||
// Should not have overly broad prefixes
|
||||
expect(descriptor.publicRoutes).not.toContain("/_emdash/.well-known/");
|
||||
});
|
||||
});
|
||||
|
||||
describe("config passthrough", () => {
|
||||
it("passes allowedDIDs through", () => {
|
||||
const config: AtprotoAuthConfig = {
|
||||
allowedDIDs: ["did:plc:abc123", "did:web:example.com"],
|
||||
};
|
||||
const descriptor = atproto(config);
|
||||
const result = descriptor.config as AtprotoAuthConfig;
|
||||
expect(result.allowedDIDs).toEqual(["did:plc:abc123", "did:web:example.com"]);
|
||||
});
|
||||
|
||||
it("passes defaultRole through", () => {
|
||||
const descriptor = atproto({ defaultRole: 20 });
|
||||
const result = descriptor.config as AtprotoAuthConfig;
|
||||
expect(result.defaultRole).toBe(20);
|
||||
});
|
||||
|
||||
it("passes allowedHandles through", () => {
|
||||
const config: AtprotoAuthConfig = {
|
||||
allowedHandles: ["*.example.com", "alice.bsky.social"],
|
||||
};
|
||||
const descriptor = atproto(config);
|
||||
const result = descriptor.config as AtprotoAuthConfig;
|
||||
expect(result.allowedHandles).toEqual(["*.example.com", "alice.bsky.social"]);
|
||||
});
|
||||
|
||||
it("passes full config through unchanged", () => {
|
||||
const config: AtprotoAuthConfig = {
|
||||
allowedDIDs: ["did:plc:me123"],
|
||||
allowedHandles: ["*.example.com"],
|
||||
defaultRole: 40,
|
||||
};
|
||||
const descriptor = atproto(config);
|
||||
expect(descriptor.config).toEqual(config);
|
||||
});
|
||||
|
||||
it("does not mutate the input config", () => {
|
||||
const config: AtprotoAuthConfig = {
|
||||
allowedDIDs: ["did:plc:alice123"],
|
||||
allowedHandles: ["*.example.com"],
|
||||
defaultRole: 30,
|
||||
};
|
||||
const original = {
|
||||
...config,
|
||||
allowedDIDs: [...config.allowedDIDs!],
|
||||
allowedHandles: [...config.allowedHandles!],
|
||||
};
|
||||
atproto(config);
|
||||
expect(config).toEqual(original);
|
||||
});
|
||||
});
|
||||
|
||||
describe("descriptor shape invariants", () => {
|
||||
it("id is always a non-empty string", () => {
|
||||
const descriptor = atproto();
|
||||
expect(typeof descriptor.id).toBe("string");
|
||||
expect(descriptor.id.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("label is always a non-empty string", () => {
|
||||
const descriptor = atproto();
|
||||
expect(typeof descriptor.label).toBe("string");
|
||||
expect(descriptor.label.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("adminEntry is always a non-empty string", () => {
|
||||
const descriptor = atproto();
|
||||
expect(typeof descriptor.adminEntry).toBe("string");
|
||||
expect(descriptor.adminEntry!.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
98
packages/auth-atproto/tests/oauth-client.test.ts
Normal file
98
packages/auth-atproto/tests/oauth-client.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
|
||||
const LOCALHOST_RE = /^http:\/\/localhost/;
|
||||
|
||||
// Reset the module singleton between tests by re-importing fresh copies
|
||||
async function freshImport() {
|
||||
// Clear the module cache so the singleton resets
|
||||
vi.resetModules();
|
||||
return import("../src/oauth-client.js");
|
||||
}
|
||||
|
||||
describe("getAtprotoOAuthClient (HTTPS - public client)", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("returns an OAuthClient instance", async () => {
|
||||
const { getAtprotoOAuthClient } = await freshImport();
|
||||
const client = await getAtprotoOAuthClient("https://example.com");
|
||||
expect(client).toBeDefined();
|
||||
expect(client.metadata).toBeDefined();
|
||||
});
|
||||
|
||||
it("sets client_id to the well-known metadata URL", async () => {
|
||||
const { getAtprotoOAuthClient } = await freshImport();
|
||||
const client = await getAtprotoOAuthClient("https://example.com");
|
||||
expect(client.metadata.client_id).toBe(
|
||||
"https://example.com/.well-known/atproto-client-metadata.json",
|
||||
);
|
||||
});
|
||||
|
||||
it("sets redirect_uri to the callback endpoint", async () => {
|
||||
const { getAtprotoOAuthClient } = await freshImport();
|
||||
const client = await getAtprotoOAuthClient("https://example.com");
|
||||
expect(client.metadata.redirect_uris).toEqual([
|
||||
"https://example.com/_emdash/api/auth/atproto/callback",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not set jwks_uri (public client)", async () => {
|
||||
const { getAtprotoOAuthClient } = await freshImport();
|
||||
const client = await getAtprotoOAuthClient("https://example.com");
|
||||
expect(client.metadata.jwks_uri).toBeUndefined();
|
||||
});
|
||||
|
||||
it("requests atproto scope", async () => {
|
||||
const { getAtprotoOAuthClient } = await freshImport();
|
||||
const client = await getAtprotoOAuthClient("https://example.com");
|
||||
expect(client.metadata.scope).toBe("atproto transition:generic");
|
||||
});
|
||||
|
||||
it("creates a fresh instance per call (no shared state between requests)", async () => {
|
||||
const { getAtprotoOAuthClient } = await freshImport();
|
||||
const client1 = await getAtprotoOAuthClient("https://example.com");
|
||||
const client2 = await getAtprotoOAuthClient("https://example.com");
|
||||
expect(client1).not.toBe(client2);
|
||||
});
|
||||
|
||||
it("creates distinct instances for different baseUrls", async () => {
|
||||
const { getAtprotoOAuthClient } = await freshImport();
|
||||
const client1 = await getAtprotoOAuthClient("https://example.com");
|
||||
const client2 = await getAtprotoOAuthClient("https://other.com");
|
||||
expect(client1).not.toBe(client2);
|
||||
expect(client2.metadata.client_id).toContain("other.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAtprotoOAuthClient (localhost - loopback public client)", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("creates a loopback client for http://localhost", async () => {
|
||||
const { getAtprotoOAuthClient } = await freshImport();
|
||||
const client = await getAtprotoOAuthClient("http://localhost:4321");
|
||||
expect(client).toBeDefined();
|
||||
expect(client.metadata).toBeDefined();
|
||||
});
|
||||
|
||||
it("uses http://localhost client_id (loopback format)", async () => {
|
||||
const { getAtprotoOAuthClient } = await freshImport();
|
||||
const client = await getAtprotoOAuthClient("http://localhost:4321");
|
||||
// Loopback clients have client_id starting with http://localhost
|
||||
expect(client.metadata.client_id).toMatch(LOCALHOST_RE);
|
||||
});
|
||||
|
||||
it("does not set jwks_uri for loopback clients", async () => {
|
||||
const { getAtprotoOAuthClient } = await freshImport();
|
||||
const client = await getAtprotoOAuthClient("http://localhost:4321");
|
||||
expect(client.metadata.jwks_uri).toBeUndefined();
|
||||
});
|
||||
|
||||
it("also treats 127.0.0.1 as loopback", async () => {
|
||||
const { getAtprotoOAuthClient } = await freshImport();
|
||||
const client = await getAtprotoOAuthClient("http://127.0.0.1:4321");
|
||||
expect(client.metadata.client_id).toMatch(LOCALHOST_RE);
|
||||
});
|
||||
});
|
||||
27
packages/auth-atproto/tests/resolve-handle.test.ts
Normal file
27
packages/auth-atproto/tests/resolve-handle.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
|
||||
import { verifyHandleDID } from "../src/resolve-handle.js";
|
||||
|
||||
describe("verifyHandleDID", () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("returns null for handles without a dot", async () => {
|
||||
expect(await verifyHandleDID("localhost")).toBeNull();
|
||||
expect(await verifyHandleDID("")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when resolution fails", async () => {
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error("network error"));
|
||||
expect(await verifyHandleDID("nobody.example.com")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when HTTP returns non-ok", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(new Response("", { status: 404 }));
|
||||
expect(await verifyHandleDID("nobody.example.com")).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user