package api import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "path/filepath" "sync" "testing" "github.com/sandrodach/mindsift/internal/auth" "github.com/sandrodach/mindsift/internal/config" "github.com/sandrodach/mindsift/internal/domain" "github.com/sandrodach/mindsift/internal/storage" "github.com/sandrodach/mindsift/internal/ws" ) func setupTestServer(t *testing.T) (*Server, *storage.Store, string) { t.Helper() dbPath := filepath.Join(t.TempDir(), "test_mindsift.db") store, err := storage.NewStore(dbPath) if err != nil { t.Fatalf("failed to create test store: %v", err) } t.Cleanup(func() { _ = store.Close() }) user, err := store.EnsureDefaultUser() if err != nil { t.Fatalf("failed to ensure default user: %v", err) } cfg := &config.Config{ Port: "8080", DBPath: dbPath, JWTSecret: "test-jwt-secret-key", AllowRegistration: true, } hub := ws.NewHub() srv := NewServer(cfg, store, hub, nil) token, err := auth.GenerateToken(cfg.JWTSecret, user.ID, user.Email, user.DisplayName) if err != nil { t.Fatalf("failed to generate token: %v", err) } return srv, store, token } func TestConcurrentInitialLoadAndListsNoDeadlock(t *testing.T) { srv, _, token := setupTestServer(t) router := srv.Router() endpoints := []string{ "/api/v1/auth/me", "/api/v1/triage/deck", "/api/v1/lists", "/api/v1/spaces", "/api/v1/tasks", "/api/v1/documents", } var wg sync.WaitGroup for i := 0; i < 3; i++ { for _, ep := range endpoints { wg.Add(1) go func(path string) { defer wg.Done() req := httptest.NewRequest(http.MethodGet, path, nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("GET %s returned %d: %s", path, rec.Code, rec.Body.String()) } }(ep) } } wg.Wait() } func TestStaleTokenFallbackAndQuickDumpToTriageAndDirectTargets(t *testing.T) { srv, store, _ := setupTestServer(t) router := srv.Router() // Generate a stale token from a previous container run (non-existent user ID) staleToken, err := auth.GenerateToken("test-jwt-secret-key", "usr_stale_from_old_container", "admin@mindsift.local", "Sandro") if err != nil { t.Fatalf("failed to generate stale token: %v", err) } // 1. Capture Quick Dump (default target: inbox) using stale token dumpPayload := map[string]string{ "content": "Schneller Quick Dump Testgedanke #test", "priority": "high", "target": "inbox", } body, _ := json.Marshal(dumpPayload) req := httptest.NewRequest(http.MethodPost, "/api/v1/inbox", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+staleToken) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("expected 201 Created for Quick Dump, got %d: %s", rec.Code, rec.Body.String()) } var createdDump domain.Thought if err := json.Unmarshal(rec.Body.Bytes(), &createdDump); err != nil { t.Fatalf("failed to decode created dump: %v", err) } if createdDump.Status != domain.StatusInbox { t.Fatalf("expected status 'inbox', got %q", createdDump.Status) } // 2. Capture Direct Task (target: task) taskPayload := map[string]string{ "content": "Direkte Aufgabe ohne Triage", "priority": "urgent", "target": "task", "dueDate": "2026-11-01", } body, _ = json.Marshal(taskPayload) req = httptest.NewRequest(http.MethodPost, "/api/v1/inbox", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+staleToken) req.Header.Set("Content-Type", "application/json") rec = httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("expected 201 Created for Direct Task, got %d: %s", rec.Code, rec.Body.String()) } var createdTask domain.Thought _ = json.Unmarshal(rec.Body.Bytes(), &createdTask) if createdTask.Status != domain.StatusConverted || createdTask.ConvertedType != "task" { t.Fatalf("expected converted task, got status=%q type=%q", createdTask.Status, createdTask.ConvertedType) } // 3. Capture Direct List Item (target: list_item) defUser, _ := store.EnsureDefaultUser() lists, err := store.GetAllListsForUser(defUser.ID) if err != nil || len(lists) == 0 { t.Fatalf("expected starter lists, got len=%d err=%v", len(lists), err) } targetListID := lists[0].ID listPayload := map[string]string{ "content": "Frische Haferflocken", "target": "list_item", "targetListId": targetListID, } body, _ = json.Marshal(listPayload) req = httptest.NewRequest(http.MethodPost, "/api/v1/inbox", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+staleToken) req.Header.Set("Content-Type", "application/json") rec = httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("expected 201 Created for Direct List Item, got %d: %s", rec.Code, rec.Body.String()) } var createdListItemThought domain.Thought _ = json.Unmarshal(rec.Body.Bytes(), &createdListItemThought) if createdListItemThought.ConvertedType != "list_item" || createdListItemThought.ConvertedRefID == "" { t.Fatalf("expected converted list_item, got %+v", createdListItemThought) } // 4. Swipe Triage card to Task triagePayload := map[string]string{ "action": "keep", "convertTo": "task", } body, _ = json.Marshal(triagePayload) req = httptest.NewRequest(http.MethodPatch, "/api/v1/triage/"+createdDump.ID, bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+staleToken) req.Header.Set("Content-Type", "application/json") rec = httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200 OK for triage action, got %d: %s", rec.Code, rec.Body.String()) } } func TestNewFullFeatures(t *testing.T) { srv, _, token := setupTestServer(t) router := srv.Router() // 1. Test TaskLists: Create & Get taskListPayload := map[string]string{ "title": "Homelab & Docker", "icon": "🐳", "color": "#3b82f6", } body, _ := json.Marshal(taskListPayload) req := httptest.NewRequest(http.MethodPost, "/api/v1/task-lists", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("expected 201 Created for TaskList, got %d: %s", rec.Code, rec.Body.String()) } var createdTaskList domain.TaskList _ = json.Unmarshal(rec.Body.Bytes(), &createdTaskList) if createdTaskList.Title != "Homelab & Docker" { t.Fatalf("expected task list title 'Homelab & Docker', got %q", createdTaskList.Title) } req = httptest.NewRequest(http.MethodGet, "/api/v1/task-lists", nil) req.Header.Set("Authorization", "Bearer "+token) rec = httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200 OK for GET /api/v1/task-lists, got %d", rec.Code) } // 2. Test User Settings: Update Discord Webhook & Profile settingsPayload := map[string]interface{}{ "displayName": "Sandro D.", "discordWebhookUrl": "https://discord.com/api/webhooks/test/123", "notifyBeforeMinutes": 15, } body, _ = json.Marshal(settingsPayload) req = httptest.NewRequest(http.MethodPatch, "/api/v1/auth/settings", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") rec = httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200 OK for PATCH /api/v1/auth/settings, got %d: %s", rec.Code, rec.Body.String()) } // 3. Test Admin routes (default user is admin) req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/users", nil) req.Header.Set("Authorization", "Bearer "+token) rec = httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200 OK for GET /api/v1/admin/users, got %d: %s", rec.Code, rec.Body.String()) } req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/settings", nil) req.Header.Set("Authorization", "Bearer "+token) rec = httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200 OK for GET /api/v1/admin/settings, got %d: %s", rec.Code, rec.Body.String()) } // 4. Test Kanban & Task details update taskUpdatePayload := map[string]interface{}{ "kanbanStatus": "in_progress", "notes": "Detailnotizen für die Aufgabe", "taskListId": createdTaskList.ID, } body, _ = json.Marshal(taskUpdatePayload) req = httptest.NewRequest(http.MethodPost, "/api/v1/inbox", bytes.NewReader([]byte(`{"content":"Kanban Task Test","target":"task"}`))) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") rec = httptest.NewRecorder() router.ServeHTTP(rec, req) var taskThought domain.Thought _ = json.Unmarshal(rec.Body.Bytes(), &taskThought) body, _ = json.Marshal(taskUpdatePayload) req = httptest.NewRequest(http.MethodPatch, "/api/v1/thoughts/"+taskThought.ID, bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") rec = httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200 OK for PATCH /api/v1/thoughts/{id}, got %d: %s", rec.Code, rec.Body.String()) } var updatedThought domain.Thought _ = json.Unmarshal(rec.Body.Bytes(), &updatedThought) if updatedThought.KanbanStatus != "in_progress" || updatedThought.Notes != "Detailnotizen für die Aufgabe" { t.Fatalf("expected updated kanbanStatus and notes, got %+v", updatedThought) } }