package storage import ( "path/filepath" "testing" "github.com/sandrodach/mindsift/internal/auth" "github.com/sandrodach/mindsift/internal/domain" ) func TestStoreLifecycle(t *testing.T) { tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "test_mindsift.db") store, err := NewStore(dbPath) if err != nil { t.Fatalf("NewStore failed: %v", err) } defer store.Close() user, err := store.EnsureDefaultUser() if err != nil { t.Fatalf("EnsureDefaultUser failed: %v", err) } if user.ID == "" { t.Fatal("expected valid user ID") } // 1. Test Instant Capture thought, err := store.CreateThought(user.ID, "Milch und Brot kaufen #einkauf #dringend", "high", "manual", "") if err != nil { t.Fatalf("CreateThought failed: %v", err) } if thought.Status != domain.StatusInbox { t.Fatalf("expected status inbox, got %s", thought.Status) } if len(thought.Tags) != 2 { t.Fatalf("expected 2 extracted hashtags, got %d", len(thought.Tags)) } // 2. Test Tinder-Style Triage -> Convert to Document triaged, err := store.UpdateThoughtTriage(user.ID, thought.ID, "keep", "document", "", "", "") if err != nil { t.Fatalf("UpdateThoughtTriage failed: %v", err) } if triaged.Status != domain.StatusConverted || triaged.ConvertedType != "document" || triaged.ConvertedRefID == "" { t.Fatalf("expected converted document, got status=%s type=%s ref=%s", triaged.Status, triaged.ConvertedType, triaged.ConvertedRefID) } // 3. Test Shared Space & Shopping List sp, err := store.CreateSpace(user.ID, "Test WG", "๐Ÿ•", "Test Space", false) if err != nil { t.Fatalf("CreateSpace failed: %v", err) } lst, err := store.CreateList(sp.ID, "REWE Einkauf", "๐Ÿ›’", "shopping") if err != nil { t.Fatalf("CreateList failed: %v", err) } item, err := store.CreateListItem(lst.ID, "Hafermilch", "Kรผhlregal", "2L", 0) if err != nil { t.Fatalf("CreateListItem failed: %v", err) } toggled, err := store.ToggleListItem(item.ID, true, user.ID, user.DisplayName) if err != nil || !toggled.IsCompleted { t.Fatalf("ToggleListItem failed: %v", err) } // 4. Test Webhook API Key generation & validation apiKey, err := store.CreateAPIKey(user.ID, "Home Assistant") if err != nil { t.Fatalf("CreateAPIKey failed: %v", err) } validatedUser, err := store.ValidateAPIKeyAndGetUser(apiKey.RawKey) if err != nil || validatedUser.ID != user.ID { t.Fatalf("ValidateAPIKeyAndGetUser failed: %v", err) } // 5. Test TaskLists CRUD taskList, err := store.CreateTaskList(user.ID, sp.ID, "Haus & Garten", "๐Ÿก", "#10b981") if err != nil || taskList.ID == "" { t.Fatalf("CreateTaskList failed: %v", err) } taskLists, err := store.GetTaskLists(user.ID) if err != nil || len(taskLists) == 0 { t.Fatalf("GetTaskLists failed: %v", err) } // 6. Test Organizations & Teams org, err := store.CreateOrganization(user.ID, "Mindsift Inc") if err != nil || org.ID == "" { t.Fatalf("CreateOrganization failed: %v", err) } team, err := store.CreateTeam(org.ID, "DevOps", "Infrastructure team") if err != nil || team.ID == "" { t.Fatalf("CreateTeam failed: %v", err) } err = store.AddTeamMember(team.ID, user.ID, "owner") if err != nil { t.Fatalf("AddTeamMember failed: %v", err) } // 7. Test System Settings sysSettings, err := store.GetSystemSettings() if err != nil || sysSettings.InstanceName == "" { t.Fatalf("GetSystemSettings failed: %v", err) } sysSettings.SharingMode = "teams_only" err = store.UpdateSystemSettings(sysSettings) if err != nil { t.Fatalf("UpdateSystemSettings failed: %v", err) } // 8. Test Document Sharing & Permissions user2, err := store.CreateUser("collab@example.com", "hash", "Collab User", "member") if err != nil { t.Fatalf("CreateUser collab failed: %v", err) } newDoc, err := store.CreateDocument(user.ID, "", "Test Note", "๐Ÿ“„", "", []domain.DocumentBlock{ {ID: "blk_1", Type: "bullet", Content: "Geheim", Level: 0}, }, []string{"test"}, false) if err != nil { t.Fatalf("CreateDocument failed: %v", err) } // Share with user2 as "read" share, err := store.CreateDocShare(user.ID, newDoc.ID, user2.ID, "read") if err != nil || share.Permission != "read" { t.Fatalf("CreateDocShare failed: %v", err) } // user2 can get document docForUser2, err := store.GetDocumentByID(user2.ID, newDoc.ID) if err != nil || docForUser2.Permission != "read" || docForUser2.IsOwner { t.Fatalf("GetDocumentByID for user2 failed or wrong permission: %v", err) } // user2 tries to edit read-only doc -> must fail updatedTitle := "Hacked Title" _, err = store.UpdateDocument(user2.ID, newDoc.ID, &updatedTitle, nil, nil, nil, nil, nil) if err == nil { t.Fatalf("expected UpdateDocument to fail for read-only user, but succeeded") } // Upgrade to write permission _, err = store.CreateDocShare(user.ID, newDoc.ID, user2.ID, "write") if err != nil { t.Fatalf("Upgrade CreateDocShare failed: %v", err) } // user2 edits writeable doc -> must succeed _, err = store.UpdateDocument(user2.ID, newDoc.ID, &updatedTitle, nil, nil, nil, nil, nil) if err != nil { t.Fatalf("UpdateDocument failed for write collaborator: %v", err) } // Remove share err = store.DeleteDocShare(user.ID, newDoc.ID, user2.ID) if err != nil { t.Fatalf("DeleteDocShare failed: %v", err) } _, err = store.GetDocumentByID(user2.ID, newDoc.ID) if err == nil { t.Fatalf("expected GetDocumentByID to fail after share removed, but succeeded") } } func TestPerUserDocPinning(t *testing.T) { tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "pin_test.db") store, err := NewStore(dbPath) if err != nil { t.Fatalf("NewStore failed: %v", err) } defer store.Close() user1, _ := store.CreateUser("user1@example.com", "hash", "User 1", "admin") user2, _ := store.CreateUser("user2@example.com", "hash", "User 2", "member") // user1 creates document and pins it doc, err := store.CreateDocument(user1.ID, "", "Doc 1", "๐Ÿ“", "", []domain.DocumentBlock{ {ID: "b1", Type: "text", Content: "Hello"}, }, []string{"test"}, true) if err != nil { t.Fatalf("CreateDocument failed: %v", err) } if !doc.IsPinned { t.Fatalf("expected doc to be pinned for user1") } // Share with user2 as read-only _, err = store.CreateDocShare(user1.ID, doc.ID, user2.ID, "read") if err != nil { t.Fatalf("CreateDocShare failed: %v", err) } // user2 reads document: should NOT be pinned for user2 docForUser2, err := store.GetDocumentByID(user2.ID, doc.ID) if err != nil { t.Fatalf("GetDocumentByID user2 failed: %v", err) } if docForUser2.IsPinned { t.Fatalf("expected doc to NOT be pinned for user2 initially") } // user2 pins the shared document (even with read-only access) pinTrue := true updatedDoc2, err := store.UpdateDocument(user2.ID, doc.ID, nil, nil, nil, nil, nil, &pinTrue) if err != nil { t.Fatalf("user2 pinning read-only doc failed: %v", err) } if !updatedDoc2.IsPinned { t.Fatalf("expected doc to be pinned for user2") } // Verify user1's doc is still pinned docForUser1, err := store.GetDocumentByID(user1.ID, doc.ID) if err != nil { t.Fatalf("GetDocumentByID user1 failed: %v", err) } if !docForUser1.IsPinned { t.Fatalf("expected doc to still be pinned for user1") } // user1 unpins the doc pinFalse := false _, err = store.UpdateDocument(user1.ID, doc.ID, nil, nil, nil, nil, nil, &pinFalse) if err != nil { t.Fatalf("user1 unpinning doc failed: %v", err) } // user1 doc is now unpinned, but user2 doc must STILL be pinned! docForUser1, _ = store.GetDocumentByID(user1.ID, doc.ID) if docForUser1.IsPinned { t.Fatalf("expected doc to be unpinned for user1") } docForUser2, _ = store.GetDocumentByID(user2.ID, doc.ID) if !docForUser2.IsPinned { t.Fatalf("expected doc to remain pinned for user2") } } func TestEnsureDemoAccountsAndLoginLookup(t *testing.T) { tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "demo_test.db") store, err := NewStore(dbPath) if err != nil { t.Fatalf("NewStore failed: %v", err) } defer store.Close() if err := store.EnsureDemoAccounts(); err != nil { t.Fatalf("EnsureDemoAccounts failed: %v", err) } // 1. Lookup by username "admin" adminUser, err := store.GetUserByLogin("admin") if err != nil || adminUser == nil { t.Fatalf("expected to find admin user by username 'admin', got %v", err) } if adminUser.Role != "admin" { t.Fatalf("expected role 'admin', got %s", adminUser.Role) } if !auth.CheckPassword("admin", adminUser.PasswordHash) { t.Fatalf("admin password check failed") } // 2. Lookup by email "admin@mindsift.local" adminByEmail, err := store.GetUserByLogin("admin@mindsift.local") if err != nil || adminByEmail.ID != adminUser.ID { t.Fatalf("lookup by email failed: got %v", err) } // 3. Lookup with case insensitivity "ADMIN" adminCaps, err := store.GetUserByLogin("ADMIN") if err != nil || adminCaps.ID != adminUser.ID { t.Fatalf("lookup by uppercase failed: got %v", err) } // 4. Lookup by username "user" normalUser, err := store.GetUserByLogin("user") if err != nil || normalUser == nil { t.Fatalf("expected to find user by username 'user', got %v", err) } if normalUser.Role != "member" { t.Fatalf("expected role 'member', got %s", normalUser.Role) } if !auth.CheckPassword("user", normalUser.PasswordHash) { t.Fatalf("user password check failed") } // 5. Ensure re-running is idempotent and preserves/refreshes credentials if err := store.EnsureDemoAccounts(); err != nil { t.Fatalf("second EnsureDemoAccounts call failed: %v", err) } }