The Appview for the kipclip.com atproto bookmarking service
at main 180 lines 5.9 kB view raw
1/** 2 * API tests for kipclip. 3 * Tests route handlers via the app handler function. 4 */ 5 6// Load test environment before importing application code 7import "./test-setup.ts"; 8 9import { assertEquals, assertStringIncludes } from "@std/assert"; 10 11// Import the app and initialize OAuth for tests 12import { app } from "../main.ts"; 13import { initOAuth } from "../lib/oauth-config.ts"; 14 15// Initialize OAuth with test URL before running tests 16initOAuth(new Request("https://kipclip.com")); 17 18// Create handler from app 19const handler = app.handler(); 20 21Deno.test("GET /api/bookmarks - returns 401 when not authenticated", async () => { 22 const req = new Request("https://kipclip.com/api/bookmarks"); 23 const res = await handler(req); 24 25 assertEquals(res.status, 401); 26 const body = await res.json(); 27 assertEquals(body.error, "Authentication required"); 28}); 29 30Deno.test("GET /api/bookmarks - requires authentication", async () => { 31 // Make request without session cookie 32 const req = new Request("https://kipclip.com/api/bookmarks", { 33 method: "GET", 34 }); 35 36 const res = await handler(req); 37 38 assertEquals(res.status, 401); 39 const data = await res.json(); 40 // Returns NO_COOKIE when no session cookie is present 41 assertEquals(data.code, "NO_COOKIE"); 42}); 43 44Deno.test("GET /api/tags - returns 401 when not authenticated", async () => { 45 const req = new Request("https://kipclip.com/api/tags"); 46 const res = await handler(req); 47 48 assertEquals(res.status, 401); 49 const body = await res.json(); 50 assertEquals(body.error, "Authentication required"); 51}); 52 53Deno.test("POST /api/bookmarks - returns 401 when not authenticated", async () => { 54 const req = new Request("https://kipclip.com/api/bookmarks", { 55 method: "POST", 56 headers: { "Content-Type": "application/json" }, 57 body: JSON.stringify({ url: "https://example.com" }), 58 }); 59 60 const res = await handler(req); 61 62 assertEquals(res.status, 401); 63 const body = await res.json(); 64 assertEquals(body.error, "Authentication required"); 65}); 66 67Deno.test("GET /robots.txt - returns robots.txt content", async () => { 68 const req = new Request("https://kipclip.com/robots.txt"); 69 const res = await handler(req); 70 71 assertEquals(res.status, 200); 72 assertEquals(res.headers.get("Content-Type"), "text/plain"); 73 74 const body = await res.text(); 75 assertStringIncludes(body, "User-agent: *"); 76 assertStringIncludes(body, "Disallow: /api/"); 77 assertStringIncludes(body, "Sitemap: https://kipclip.com/sitemap.xml"); 78}); 79 80Deno.test("GET /oauth-client-metadata.json - returns OAuth metadata", async () => { 81 const req = new Request("https://kipclip.com/oauth-client-metadata.json"); 82 const res = await handler(req); 83 84 assertEquals(res.status, 200); 85 assertEquals(res.headers.get("Content-Type"), "application/json"); 86 87 const body = await res.json(); 88 assertEquals(body.client_name, "kipclip"); 89 // Now dynamic - should match the BASE_URL used to init OAuth 90 assertEquals( 91 body.client_id, 92 "https://kipclip.com/oauth-client-metadata.json", 93 ); 94 assertEquals(body.dpop_bound_access_tokens, true); 95}); 96 97Deno.test("GET /opensearch.xml - returns OpenSearch description", async () => { 98 const req = new Request("https://kipclip.com/opensearch.xml"); 99 const res = await handler(req); 100 101 assertEquals(res.status, 200); 102 assertEquals( 103 res.headers.get("Content-Type"), 104 "application/opensearchdescription+xml", 105 ); 106 107 const body = await res.text(); 108 assertStringIncludes(body, "<ShortName>kipclip</ShortName>"); 109 assertStringIncludes(body, "https://kipclip.com/?q={searchTerms}"); 110}); 111 112Deno.test("RSS feed - RFC 822 date format", () => { 113 // Test date formatting 114 const testDate = "2025-11-01T12:00:00.000Z"; 115 const date = new Date(testDate); 116 const rfc822 = date.toUTCString(); 117 118 // Should match RFC 822 format: "Fri, 01 Nov 2025 12:00:00 GMT" 119 assertStringIncludes(rfc822, "Nov 2025"); 120 assertStringIncludes(rfc822, "GMT"); 121}); 122 123// ============================================================================ 124// Security Tests 125// ============================================================================ 126 127Deno.test("GET /api/auth/debug - returns 404 (removed for security)", async () => { 128 const req = new Request("https://kipclip.com/api/auth/debug"); 129 const res = await handler(req); 130 131 // Debug endpoint should be removed 132 assertEquals(res.status, 404); 133}); 134 135Deno.test("Security headers - X-Frame-Options is set", async () => { 136 const req = new Request("https://kipclip.com/api/bookmarks"); 137 const res = await handler(req); 138 139 assertEquals(res.headers.get("X-Frame-Options"), "DENY"); 140}); 141 142Deno.test("Security headers - X-Content-Type-Options is set", async () => { 143 const req = new Request("https://kipclip.com/api/bookmarks"); 144 const res = await handler(req); 145 146 assertEquals(res.headers.get("X-Content-Type-Options"), "nosniff"); 147}); 148 149Deno.test("Security headers - Referrer-Policy is set", async () => { 150 const req = new Request("https://kipclip.com/api/bookmarks"); 151 const res = await handler(req); 152 153 assertEquals( 154 res.headers.get("Referrer-Policy"), 155 "strict-origin-when-cross-origin", 156 ); 157}); 158 159Deno.test("Security headers - Strict-Transport-Security is set", async () => { 160 const req = new Request("https://kipclip.com/api/bookmarks"); 161 const res = await handler(req); 162 163 assertEquals( 164 res.headers.get("Strict-Transport-Security"), 165 "max-age=31536000; includeSubDomains", 166 ); 167}); 168 169Deno.test("Security headers - Permissions-Policy is set", async () => { 170 const req = new Request("https://kipclip.com/api/bookmarks"); 171 const res = await handler(req); 172 173 assertEquals( 174 res.headers.get("Permissions-Policy"), 175 "camera=(), microphone=(), geolocation=()", 176 ); 177}); 178 179// Note: Full integration tests with real PDS would go in a separate integration test file 180// These unit tests focus on the route handler logic and error handling