MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 16s
290 lines
9.6 KiB
Go
290 lines
9.6 KiB
Go
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)
|
|
}
|
|
}
|