MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 56s
320 lines
10 KiB
Go
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)
|
|
}
|
|
}
|