handlers_test.go

  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}