The Appview for the kipclip.com atproto bookmarking service
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