1package server
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "net/http"
9 "net/http/httptest"
10 "testing"
11
12 "shelley.exe.dev/db/generated"
13)
14
15func TestHandleVersion(t *testing.T) {
16 h := NewTestHarness(t)
17 defer h.cleanup()
18
19 // Test successful GET request
20 req := httptest.NewRequest(http.MethodGet, "/api/version", nil)
21 w := httptest.NewRecorder()
22 h.server.handleVersion(w, req)
23
24 if w.Code != http.StatusOK {
25 t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code)
26 }
27
28 if w.Header().Get("Content-Type") != "application/json" {
29 t.Errorf("Expected Content-Type application/json, got %s", w.Header().Get("Content-Type"))
30 }
31
32 // Test method not allowed
33 req = httptest.NewRequest(http.MethodPost, "/api/version", nil)
34 w = httptest.NewRecorder()
35 h.server.handleVersion(w, req)
36
37 if w.Code != http.StatusMethodNotAllowed {
38 t.Errorf("Expected status code %d, got %d", http.StatusMethodNotAllowed, w.Code)
39 }
40}
41
42func TestHandleArchivedConversations(t *testing.T) {
43 h := NewTestHarness(t)
44 defer h.cleanup()
45
46 // Create a test conversation and archive it
47 ctx := context.Background()
48 slug := "test-conversation"
49 conv, err := h.db.CreateConversation(ctx, &slug, true, nil, nil)
50 if err != nil {
51 t.Fatalf("Failed to create conversation: %v", err)
52 }
53
54 _, err = h.db.ArchiveConversation(ctx, conv.ConversationID)
55 if err != nil {
56 t.Fatalf("Failed to archive conversation: %v", err)
57 }
58
59 // Test successful GET request
60 req := httptest.NewRequest(http.MethodGet, "/api/conversations/archived", nil)
61 w := httptest.NewRecorder()
62 h.server.handleArchivedConversations(w, req)
63
64 if w.Code != http.StatusOK {
65 t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code)
66 }
67
68 if w.Header().Get("Content-Type") != "application/json" {
69 t.Errorf("Expected Content-Type application/json, got %s", w.Header().Get("Content-Type"))
70 }
71
72 var conversations []generated.Conversation
73 if err := json.Unmarshal(w.Body.Bytes(), &conversations); err != nil {
74 t.Fatalf("Failed to unmarshal response: %v", err)
75 }
76
77 if len(conversations) != 1 {
78 t.Errorf("Expected 1 archived conversation, got %d", len(conversations))
79 }
80
81 // Test method not allowed
82 req = httptest.NewRequest(http.MethodPost, "/api/conversations/archived", nil)
83 w = httptest.NewRecorder()
84 h.server.handleArchivedConversations(w, req)
85
86 if w.Code != http.StatusMethodNotAllowed {
87 t.Errorf("Expected status code %d, got %d", http.StatusMethodNotAllowed, w.Code)
88 }
89
90 // Test with query parameters
91 req = httptest.NewRequest(http.MethodGet, "/api/conversations/archived?limit=10&offset=0", nil)
92 w = httptest.NewRecorder()
93 h.server.handleArchivedConversations(w, req)
94
95 if w.Code != http.StatusOK {
96 t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code)
97 }
98}
99
100func TestHandleArchiveConversation(t *testing.T) {
101 h := NewTestHarness(t)
102 defer h.cleanup()
103
104 // Create a test conversation
105 ctx := context.Background()
106 slug := "test-conversation"
107 conv, err := h.db.CreateConversation(ctx, &slug, true, nil, nil)
108 if err != nil {
109 t.Fatalf("Failed to create conversation: %v", err)
110 }
111
112 // Test successful POST request
113 req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/conversation/%s/archive", conv.ConversationID), nil)
114 w := httptest.NewRecorder()
115 h.server.handleArchiveConversation(w, req, conv.ConversationID)
116
117 if w.Code != http.StatusOK {
118 t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code)
119 }
120
121 if w.Header().Get("Content-Type") != "application/json" {
122 t.Errorf("Expected Content-Type application/json, got %s", w.Header().Get("Content-Type"))
123 }
124
125 var archivedConv generated.Conversation
126 if err := json.Unmarshal(w.Body.Bytes(), &archivedConv); err != nil {
127 t.Fatalf("Failed to unmarshal response: %v", err)
128 }
129
130 if !archivedConv.Archived {
131 t.Error("Expected conversation to be archived")
132 }
133
134 // Test method not allowed
135 req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/conversation/%s/archive", conv.ConversationID), nil)
136 w = httptest.NewRecorder()
137 h.server.handleArchiveConversation(w, req, conv.ConversationID)
138
139 if w.Code != http.StatusMethodNotAllowed {
140 t.Errorf("Expected status code %d, got %d", http.StatusMethodNotAllowed, w.Code)
141 }
142
143 // Test with invalid conversation ID
144 req = httptest.NewRequest(http.MethodPost, "/conversation/invalid-id/archive", nil)
145 w = httptest.NewRecorder()
146 h.server.handleArchiveConversation(w, req, "invalid-id")
147
148 if w.Code != http.StatusInternalServerError {
149 t.Errorf("Expected status code %d, got %d", http.StatusInternalServerError, w.Code)
150 }
151}
152
153func TestHandleUnarchiveConversation(t *testing.T) {
154 h := NewTestHarness(t)
155 defer h.cleanup()
156
157 // Create a test conversation and archive it
158 ctx := context.Background()
159 slug := "test-conversation"
160 conv, err := h.db.CreateConversation(ctx, &slug, true, nil, nil)
161 if err != nil {
162 t.Fatalf("Failed to create conversation: %v", err)
163 }
164
165 _, err = h.db.ArchiveConversation(ctx, conv.ConversationID)
166 if err != nil {
167 t.Fatalf("Failed to archive conversation: %v", err)
168 }
169
170 // Test successful POST request
171 req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/conversation/%s/unarchive", conv.ConversationID), nil)
172 w := httptest.NewRecorder()
173 h.server.handleUnarchiveConversation(w, req, conv.ConversationID)
174
175 if w.Code != http.StatusOK {
176 t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code)
177 }
178
179 if w.Header().Get("Content-Type") != "application/json" {
180 t.Errorf("Expected Content-Type application/json, got %s", w.Header().Get("Content-Type"))
181 }
182
183 var unarchivedConv generated.Conversation
184 if err := json.Unmarshal(w.Body.Bytes(), &unarchivedConv); err != nil {
185 t.Fatalf("Failed to unmarshal response: %v", err)
186 }
187
188 if unarchivedConv.Archived {
189 t.Error("Expected conversation to be unarchived")
190 }
191
192 // Test method not allowed
193 req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/conversation/%s/unarchive", conv.ConversationID), nil)
194 w = httptest.NewRecorder()
195 h.server.handleUnarchiveConversation(w, req, conv.ConversationID)
196
197 if w.Code != http.StatusMethodNotAllowed {
198 t.Errorf("Expected status code %d, got %d", http.StatusMethodNotAllowed, w.Code)
199 }
200
201 // Test with invalid conversation ID
202 req = httptest.NewRequest(http.MethodPost, "/conversation/invalid-id/unarchive", nil)
203 w = httptest.NewRecorder()
204 h.server.handleUnarchiveConversation(w, req, "invalid-id")
205
206 if w.Code != http.StatusInternalServerError {
207 t.Errorf("Expected status code %d, got %d", http.StatusInternalServerError, w.Code)
208 }
209}
210
211func TestHandleDeleteConversation(t *testing.T) {
212 h := NewTestHarness(t)
213 defer h.cleanup()
214
215 // Create a test conversation
216 ctx := context.Background()
217 slug := "test-conversation"
218 conv, err := h.db.CreateConversation(ctx, &slug, true, nil, nil)
219 if err != nil {
220 t.Fatalf("Failed to create conversation: %v", err)
221 }
222
223 // Test successful POST request
224 req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/conversation/%s/delete", conv.ConversationID), nil)
225 w := httptest.NewRecorder()
226 h.server.handleDeleteConversation(w, req, conv.ConversationID)
227
228 if w.Code != http.StatusOK {
229 t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code)
230 }
231
232 if w.Header().Get("Content-Type") != "application/json" {
233 t.Errorf("Expected Content-Type application/json, got %s", w.Header().Get("Content-Type"))
234 }
235
236 var response map[string]string
237 if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
238 t.Fatalf("Failed to unmarshal response: %v", err)
239 }
240
241 if response["status"] != "deleted" {
242 t.Errorf("Expected status 'deleted', got '%s'", response["status"])
243 }
244
245 // Verify conversation is deleted
246 _, err = h.db.GetConversationByID(ctx, conv.ConversationID)
247 if err == nil {
248 t.Error("Expected conversation to be deleted, but it still exists")
249 }
250
251 // Test method not allowed
252 req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/conversation/%s/delete", conv.ConversationID), nil)
253 w = httptest.NewRecorder()
254 h.server.handleDeleteConversation(w, req, conv.ConversationID)
255
256 if w.Code != http.StatusMethodNotAllowed {
257 t.Errorf("Expected status code %d, got %d", http.StatusMethodNotAllowed, w.Code)
258 }
259
260 // Test with invalid conversation ID (should still return success as DELETE is idempotent)
261 req = httptest.NewRequest(http.MethodPost, "/conversation/invalid-id/delete", nil)
262 w = httptest.NewRecorder()
263 h.server.handleDeleteConversation(w, req, "invalid-id")
264
265 if w.Code != http.StatusOK {
266 t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code)
267 }
268}
269
270func TestHandleRenameConversation(t *testing.T) {
271 h := NewTestHarness(t)
272 defer h.cleanup()
273
274 // Create a test conversation
275 ctx := context.Background()
276 slug := "test-conversation"
277 conv, err := h.db.CreateConversation(ctx, &slug, true, nil, nil)
278 if err != nil {
279 t.Fatalf("Failed to create conversation: %v", err)
280 }
281
282 // Test successful POST request
283 newSlug := "new-test-conversation"
284 body := `{"slug": "` + newSlug + `"}`
285 req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/conversation/%s/rename", conv.ConversationID), bytes.NewBufferString(body))
286 req.Header.Set("Content-Type", "application/json")
287 w := httptest.NewRecorder()
288 h.server.handleRenameConversation(w, req, conv.ConversationID)
289
290 if w.Code != http.StatusOK {
291 t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code)
292 }
293
294 if w.Header().Get("Content-Type") != "application/json" {
295 t.Errorf("Expected Content-Type application/json, got %s", w.Header().Get("Content-Type"))
296 }
297
298 var renamedConv generated.Conversation
299 if err := json.Unmarshal(w.Body.Bytes(), &renamedConv); err != nil {
300 t.Fatalf("Failed to unmarshal response: %v", err)
301 }
302
303 if *renamedConv.Slug != newSlug {
304 t.Errorf("Expected slug '%s', got '%s'", newSlug, *renamedConv.Slug)
305 }
306
307 // Test method not allowed
308 req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/conversation/%s/rename", conv.ConversationID), nil)
309 w = httptest.NewRecorder()
310 h.server.handleRenameConversation(w, req, conv.ConversationID)
311
312 if w.Code != http.StatusMethodNotAllowed {
313 t.Errorf("Expected status code %d, got %d", http.StatusMethodNotAllowed, w.Code)
314 }
315
316 // Test with invalid JSON
317 req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/conversation/%s/rename", conv.ConversationID), bytes.NewBufferString(`invalid json`))
318 req.Header.Set("Content-Type", "application/json")
319 w = httptest.NewRecorder()
320 h.server.handleRenameConversation(w, req, conv.ConversationID)
321
322 if w.Code != http.StatusBadRequest {
323 t.Errorf("Expected status code %d, got %d", http.StatusBadRequest, w.Code)
324 }
325
326 // Test with missing slug
327 req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/conversation/%s/rename", conv.ConversationID), bytes.NewBufferString(`{}`))
328 req.Header.Set("Content-Type", "application/json")
329 w = httptest.NewRecorder()
330 h.server.handleRenameConversation(w, req, conv.ConversationID)
331
332 if w.Code != http.StatusBadRequest {
333 t.Errorf("Expected status code %d, got %d", http.StatusBadRequest, w.Code)
334 }
335
336 // Test with empty slug
337 req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/conversation/%s/rename", conv.ConversationID), bytes.NewBufferString(`{"slug": ""}`))
338 req.Header.Set("Content-Type", "application/json")
339 w = httptest.NewRecorder()
340 h.server.handleRenameConversation(w, req, conv.ConversationID)
341
342 if w.Code != http.StatusBadRequest {
343 t.Errorf("Expected status code %d, got %d", http.StatusBadRequest, w.Code)
344 }
345
346 // Test with invalid conversation ID
347 req = httptest.NewRequest(http.MethodPost, "/conversation/invalid-id/rename", bytes.NewBufferString(`{"slug": "test"}`))
348 req.Header.Set("Content-Type", "application/json")
349 w = httptest.NewRecorder()
350 h.server.handleRenameConversation(w, req, "invalid-id")
351
352 if w.Code != http.StatusInternalServerError {
353 t.Errorf("Expected status code %d, got %d", http.StatusInternalServerError, w.Code)
354 }
355}
356
357func TestHandleWriteFile(t *testing.T) {
358 h := NewTestHarness(t)
359 defer h.cleanup()
360
361 // Test successful POST request
362 filePath := "/tmp/test-file.txt"
363 fileContent := "test content"
364 body := fmt.Sprintf(`{"path": "%s", "content": "%s"}`, filePath, fileContent)
365 req := httptest.NewRequest(http.MethodPost, "/api/write-file", bytes.NewBufferString(body))
366 req.Header.Set("Content-Type", "application/json")
367 w := httptest.NewRecorder()
368 h.server.handleWriteFile(w, req)
369
370 if w.Code != http.StatusOK {
371 t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code)
372 }
373
374 // Verify file was written
375 // content, err := os.ReadFile(filePath)
376 // if err != nil {
377 // t.Fatalf("Failed to read written file: %v", err)
378 // }
379 // if string(content) != fileContent {
380 // t.Errorf("Expected file content '%s', got '%s'", fileContent, string(content))
381 // }
382
383 // Test method not allowed
384 req = httptest.NewRequest(http.MethodGet, "/api/write-file", nil)
385 w = httptest.NewRecorder()
386 h.server.handleWriteFile(w, req)
387
388 if w.Code != http.StatusMethodNotAllowed {
389 t.Errorf("Expected status code %d, got %d", http.StatusMethodNotAllowed, w.Code)
390 }
391
392 // Test with invalid JSON
393 req = httptest.NewRequest(http.MethodPost, "/api/write-file", bytes.NewBufferString(`invalid json`))
394 req.Header.Set("Content-Type", "application/json")
395 w = httptest.NewRecorder()
396 h.server.handleWriteFile(w, req)
397
398 if w.Code != http.StatusBadRequest {
399 t.Errorf("Expected status code %d, got %d", http.StatusBadRequest, w.Code)
400 }
401
402 // Test with missing path
403 req = httptest.NewRequest(http.MethodPost, "/api/write-file", bytes.NewBufferString(`{"content": "test"}`))
404 req.Header.Set("Content-Type", "application/json")
405 w = httptest.NewRecorder()
406 h.server.handleWriteFile(w, req)
407
408 if w.Code != http.StatusBadRequest {
409 t.Errorf("Expected status code %d, got %d", http.StatusBadRequest, w.Code)
410 }
411
412 // Test with relative path (should fail)
413 req = httptest.NewRequest(http.MethodPost, "/api/write-file", bytes.NewBufferString(`{"path": "relative-path.txt", "content": "test"}`))
414 req.Header.Set("Content-Type", "application/json")
415 w = httptest.NewRecorder()
416 h.server.handleWriteFile(w, req)
417
418 if w.Code != http.StatusBadRequest {
419 t.Errorf("Expected status code %d, got %d", http.StatusBadRequest, w.Code)
420 }
421}