feat(auth,settings,tasks): add first-boot setup, real login/invites, full-page settings & admin views, multi-channel alarms, and fix null slice crashes
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s
This commit is contained in:
1 parent
50ac0c8f64
commit
7802eb2cf0
17 files changed
+4391
-1368
No files matched your search
+162
-18
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
@@ -29,14 +30,15 @@ func setupTestServer(t *testing.T) (*Server, *storage.Store, string) {
|
||||
|
||||
user, err := store.EnsureDefaultUser()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to ensure default user: %v", err)
|
||||
t.Fatalf("failed to ensure test user: %v", err)
|
||||
}
|
||||
_ = store.SeedStarterContent(user)
|
||||
|
||||
cfg := &config.Config{
|
||||
Port: "8080",
|
||||
DatabasePath: dbPath,
|
||||
JWTSecret: "test-jwt-secret-key",
|
||||
AllowRegistration: true,
|
||||
AllowRegistration: false,
|
||||
}
|
||||
hub := ws.NewHub()
|
||||
srv := NewServer(cfg, store, hub, nil)
|
||||
@@ -49,6 +51,137 @@ func setupTestServer(t *testing.T) (*Server, *storage.Store, string) {
|
||||
return srv, store, token
|
||||
}
|
||||
|
||||
func TestFirstBootSetupAndInviteRegistrationAndNonNullSlices(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "fresh_mindsift.db")
|
||||
store, err := storage.NewStore(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create fresh store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
cfg := &config.Config{
|
||||
Port: "8080",
|
||||
DatabasePath: dbPath,
|
||||
JWTSecret: "test-jwt-secret-key",
|
||||
AllowRegistration: false,
|
||||
}
|
||||
srv := NewServer(cfg, store, ws.NewHub(), nil)
|
||||
router := srv.Router()
|
||||
|
||||
// 1. Fresh DB must report needsSetup = true
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/status", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK from /api/v1/auth/status, got %d", rec.Code)
|
||||
}
|
||||
var statusResp struct {
|
||||
NeedsSetup bool `json:"needsSetup"`
|
||||
AllowRegistration bool `json:"allowRegistration"`
|
||||
InstanceName string `json:"instanceName"`
|
||||
}
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &statusResp)
|
||||
if !statusResp.NeedsSetup {
|
||||
t.Fatalf("expected needsSetup=true on fresh database, got false")
|
||||
}
|
||||
|
||||
// 2. Complete First-Boot Setup (without sample data)
|
||||
setupBody := []byte(`{
|
||||
"email": "owner@example.com",
|
||||
"password": "secretpassword",
|
||||
"displayName": "Server Owner",
|
||||
"instanceName": "My MindSift",
|
||||
"allowRegistration": false,
|
||||
"sharingMode": "teams_only",
|
||||
"seedSampleData": false
|
||||
}`)
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/setup", bytes.NewReader(setupBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201 Created from /api/v1/auth/setup, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var setupRes struct {
|
||||
Token string `json:"token"`
|
||||
User domain.User `json:"user"`
|
||||
}
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &setupRes)
|
||||
if setupRes.User.Role != "admin" || setupRes.Token == "" {
|
||||
t.Fatalf("expected admin user and token, got %+v", setupRes)
|
||||
}
|
||||
|
||||
// 3. Verify empty endpoints return [] and never null (preventing React crashes)
|
||||
arrayEndpoints := []string{
|
||||
"/api/v1/task-lists",
|
||||
"/api/v1/tasks",
|
||||
"/api/v1/organizations",
|
||||
"/api/v1/teams",
|
||||
"/api/v1/spaces",
|
||||
"/api/v1/lists",
|
||||
}
|
||||
for _, ep := range arrayEndpoints {
|
||||
req = httptest.NewRequest(http.MethodGet, ep, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setupRes.Token)
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("GET %s returned %d", ep, rec.Code)
|
||||
}
|
||||
trimmed := strings.TrimSpace(rec.Body.String())
|
||||
if trimmed == "null" || !strings.HasPrefix(trimmed, "[") {
|
||||
t.Fatalf("GET %s returned %q instead of JSON array []", ep, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Public registration without invite must be rejected (403)
|
||||
regBody := []byte(`{"email":"member@example.com","password":"password123","displayName":"Member"}`)
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(regBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected 403 Forbidden when registration is disabled, got %d", rec.Code)
|
||||
}
|
||||
|
||||
// 5. Create an Invite as Admin and register with that Invite Code
|
||||
invBody := []byte(`{"role":"team_admin","validDays":7}`)
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/invites", bytes.NewReader(invBody))
|
||||
req.Header.Set("Authorization", "Bearer "+setupRes.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 invite, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var createdInvite domain.UserInvite
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &createdInvite)
|
||||
if createdInvite.Code == "" {
|
||||
t.Fatalf("expected invite code, got empty")
|
||||
}
|
||||
|
||||
regWithInviteBody, _ := json.Marshal(map[string]string{
|
||||
"email": "invited@example.com",
|
||||
"password": "password123",
|
||||
"displayName": "Invited Team Admin",
|
||||
"inviteCode": createdInvite.Code,
|
||||
})
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(regWithInviteBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201 Created when registering with valid invite code, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var invitedRes struct {
|
||||
User domain.User `json:"user"`
|
||||
}
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &invitedRes)
|
||||
if invitedRes.User.Role != "team_admin" {
|
||||
t.Fatalf("expected invited user to receive role 'team_admin', got %q", invitedRes.User.Role)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentInitialLoadAndListsNoDeadlock(t *testing.T) {
|
||||
srv, _, token := setupTestServer(t)
|
||||
router := srv.Router()
|
||||
@@ -81,27 +214,31 @@ func TestConcurrentInitialLoadAndListsNoDeadlock(t *testing.T) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestStaleTokenFallbackAndQuickDumpToTriageAndDirectTargets(t *testing.T) {
|
||||
srv, store, _ := setupTestServer(t)
|
||||
func TestQuickDumpToTriageAndDirectTargets(t *testing.T) {
|
||||
srv, store, token := 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)
|
||||
// Verify stale token from non-existent user is rejected (401)
|
||||
staleToken, _ := auth.GenerateToken("test-jwt-secret-key", "usr_non_existent", "old@mindsift.local", "Old")
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/me", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+staleToken)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401 Unauthorized for stale token, got %d", rec.Code)
|
||||
}
|
||||
|
||||
// 1. Capture Quick Dump (default target: inbox) using stale token
|
||||
// 1. Capture Quick Dump (default target: inbox)
|
||||
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 = httptest.NewRequest(http.MethodPost, "/api/v1/inbox", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusCreated {
|
||||
@@ -125,7 +262,7 @@ func TestStaleTokenFallbackAndQuickDumpToTriageAndDirectTargets(t *testing.T) {
|
||||
}
|
||||
body, _ = json.Marshal(taskPayload)
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/inbox", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", "Bearer "+staleToken)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
@@ -154,7 +291,7 @@ func TestStaleTokenFallbackAndQuickDumpToTriageAndDirectTargets(t *testing.T) {
|
||||
}
|
||||
body, _ = json.Marshal(listPayload)
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/inbox", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", "Bearer "+staleToken)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
@@ -175,7 +312,7 @@ func TestStaleTokenFallbackAndQuickDumpToTriageAndDirectTargets(t *testing.T) {
|
||||
}
|
||||
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("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
@@ -219,10 +356,13 @@ func TestNewFullFeatures(t *testing.T) {
|
||||
t.Fatalf("expected 200 OK for GET /api/v1/task-lists, got %d", rec.Code)
|
||||
}
|
||||
|
||||
// 2. Test User Settings: Update Discord Webhook & Profile
|
||||
// 2. Test User Settings: Update Multi-Channel Alarms & Profile
|
||||
settingsPayload := map[string]interface{}{
|
||||
"displayName": "Sandro D.",
|
||||
"displayName": "Admin User",
|
||||
"alarmChannels": []string{"email", "discord", "webhook"},
|
||||
"alarmEmail": "alarms@example.com",
|
||||
"discordWebhookUrl": "https://discord.com/api/webhooks/test/123",
|
||||
"customWebhookUrl": "https://ntfy.sh/my-topic",
|
||||
"notifyBeforeMinutes": 15,
|
||||
}
|
||||
body, _ = json.Marshal(settingsPayload)
|
||||
@@ -235,6 +375,11 @@ func TestNewFullFeatures(t *testing.T) {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK for PATCH /api/v1/auth/settings, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var updatedUser domain.User
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &updatedUser)
|
||||
if len(updatedUser.AlarmChannels) != 3 || updatedUser.AlarmEmail != "alarms@example.com" {
|
||||
t.Fatalf("expected 3 alarm channels and alarmEmail, got %+v", updatedUser)
|
||||
}
|
||||
|
||||
// 3. Test Admin routes (default user is admin)
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/users", nil)
|
||||
@@ -261,7 +406,6 @@ func TestNewFullFeatures(t *testing.T) {
|
||||
"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")
|
||||
|
||||
Reference in new issue
Block a user