Files
MindSift/internal/storage/sqlite_test.go
T
sandrodach 7bc5fbe001
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 56s
feat(lists): add direct user invitation to shared lists and streamline live-sync indicator
2026-10-25 11:05:03 +01:00

320 lines
10 KiB
Go

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)
}
// Test AddSpaceMember & RemoveSpaceMember
u2, err := store.CreateUser("collab_space@example.com", "hash", "Collab User", "member")
if err != nil {
t.Fatalf("CreateUser u2 failed: %v", err)
}
member, err := store.AddSpaceMember(user.ID, sp.ID, u2.ID, "editor")
if err != nil || member.UserID != u2.ID {
t.Fatalf("AddSpaceMember failed: %v", err)
}
members, err := store.GetSpaceMembers(sp.ID)
if err != nil || len(members) != 2 {
t.Fatalf("expected 2 members, got %d", len(members))
}
err = store.RemoveSpaceMember(user.ID, sp.ID, u2.ID)
if err != nil {
t.Fatalf("RemoveSpaceMember failed: %v", err)
}
membersAfter, _ := store.GetSpaceMembers(sp.ID)
if len(membersAfter) != 1 {
t.Fatalf("expected 1 member after remove, got %d", len(membersAfter))
}
// 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)
}
}