/** * API tests for kipclip. * Tests route handlers via the app handler function. */ // Load test environment before importing application code import "./test-setup.ts"; import { assertEquals, assertStringIncludes } from "@std/assert"; // Import the app and initialize OAuth for tests import { app } from "../main.ts"; import { initOAuth } from "../lib/oauth-config.ts"; // Initialize OAuth with test URL before running tests initOAuth(new Request("https://kipclip.com")); // Create handler from app const handler = app.handler(); Deno.test("GET /api/bookmarks - returns 401 when not authenticated", async () => { const req = new Request("https://kipclip.com/api/bookmarks"); const res = await handler(req); assertEquals(res.status, 401); const body = await res.json(); assertEquals(body.error, "Authentication required"); }); Deno.test("GET /api/bookmarks - requires authentication", async () => { // Make request without session cookie const req = new Request("https://kipclip.com/api/bookmarks", { method: "GET", }); const res = await handler(req); assertEquals(res.status, 401); const data = await res.json(); // Returns NO_COOKIE when no session cookie is present assertEquals(data.code, "NO_COOKIE"); }); Deno.test("GET /api/tags - returns 401 when not authenticated", async () => { const req = new Request("https://kipclip.com/api/tags"); const res = await handler(req); assertEquals(res.status, 401); const body = await res.json(); assertEquals(body.error, "Authentication required"); }); Deno.test("POST /api/bookmarks - returns 401 when not authenticated", async () => { const req = new Request("https://kipclip.com/api/bookmarks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://example.com" }), }); const res = await handler(req); assertEquals(res.status, 401); const body = await res.json(); assertEquals(body.error, "Authentication required"); }); Deno.test("GET /robots.txt - returns robots.txt content", async () => { const req = new Request("https://kipclip.com/robots.txt"); const res = await handler(req); assertEquals(res.status, 200); assertEquals(res.headers.get("Content-Type"), "text/plain"); const body = await res.text(); assertStringIncludes(body, "User-agent: *"); assertStringIncludes(body, "Disallow: /api/"); assertStringIncludes(body, "Sitemap: https://kipclip.com/sitemap.xml"); }); Deno.test("GET /oauth-client-metadata.json - returns OAuth metadata", async () => { const req = new Request("https://kipclip.com/oauth-client-metadata.json"); const res = await handler(req); assertEquals(res.status, 200); assertEquals(res.headers.get("Content-Type"), "application/json"); const body = await res.json(); assertEquals(body.client_name, "kipclip"); // Now dynamic - should match the BASE_URL used to init OAuth assertEquals( body.client_id, "https://kipclip.com/oauth-client-metadata.json", ); assertEquals(body.dpop_bound_access_tokens, true); }); Deno.test("GET /opensearch.xml - returns OpenSearch description", async () => { const req = new Request("https://kipclip.com/opensearch.xml"); const res = await handler(req); assertEquals(res.status, 200); assertEquals( res.headers.get("Content-Type"), "application/opensearchdescription+xml", ); const body = await res.text(); assertStringIncludes(body, "kipclip"); assertStringIncludes(body, "https://kipclip.com/?q={searchTerms}"); }); Deno.test("RSS feed - RFC 822 date format", () => { // Test date formatting const testDate = "2025-11-01T12:00:00.000Z"; const date = new Date(testDate); const rfc822 = date.toUTCString(); // Should match RFC 822 format: "Fri, 01 Nov 2025 12:00:00 GMT" assertStringIncludes(rfc822, "Nov 2025"); assertStringIncludes(rfc822, "GMT"); }); // ============================================================================ // Security Tests // ============================================================================ Deno.test("GET /api/auth/debug - returns 404 (removed for security)", async () => { const req = new Request("https://kipclip.com/api/auth/debug"); const res = await handler(req); // Debug endpoint should be removed assertEquals(res.status, 404); }); Deno.test("Security headers - X-Frame-Options is set", async () => { const req = new Request("https://kipclip.com/api/bookmarks"); const res = await handler(req); assertEquals(res.headers.get("X-Frame-Options"), "DENY"); }); Deno.test("Security headers - X-Content-Type-Options is set", async () => { const req = new Request("https://kipclip.com/api/bookmarks"); const res = await handler(req); assertEquals(res.headers.get("X-Content-Type-Options"), "nosniff"); }); Deno.test("Security headers - Referrer-Policy is set", async () => { const req = new Request("https://kipclip.com/api/bookmarks"); const res = await handler(req); assertEquals( res.headers.get("Referrer-Policy"), "strict-origin-when-cross-origin", ); }); Deno.test("Security headers - Strict-Transport-Security is set", async () => { const req = new Request("https://kipclip.com/api/bookmarks"); const res = await handler(req); assertEquals( res.headers.get("Strict-Transport-Security"), "max-age=31536000; includeSubDomains", ); }); Deno.test("Security headers - Permissions-Policy is set", async () => { const req = new Request("https://kipclip.com/api/bookmarks"); const res = await handler(req); assertEquals( res.headers.get("Permissions-Policy"), "camera=(), microphone=(), geolocation=()", ); }); // Note: Full integration tests with real PDS would go in a separate integration test file // These unit tests focus on the route handler logic and error handling