package storage import ( "errors" "path/filepath" "testing" "github.com/sandrodach/mindsift/internal/domain" ) func setSharingMode(t *testing.T, store *Store, mode string) { t.Helper() sys, _ := store.GetSystemSettings() sys.SharingMode = mode if err := store.UpdateSystemSettings(sys); err != nil { t.Fatalf("UpdateSystemSettings: %v", err) } } func TestVisibilityFollowsOrganizations(t *testing.T) { store, err := NewStore(filepath.Join(t.TempDir(), "visibility.db")) if err != nil { t.Fatalf("NewStore failed: %v", err) } defer store.Close() if store.SharingMode() != SharingOrganizations { t.Fatalf("default mode should be organizations, got %q", store.SharingMode()) } anna, _ := store.CreateUser("anna@example.com", "hash", "Anna", "member") ben, _ := store.CreateUser("ben@example.com", "hash", "Ben", "member") carl, _ := store.CreateUser("carl@example.com", "hash", "Carl", "admin") dora, _ := store.CreateUser("dora@example.com", "hash", "Dora", "member") org1, _ := store.CreateOrganization("Familie") org2, _ := store.CreateOrganization("Firma") _ = store.SetUserOrganization(anna.ID, org1.ID) _ = store.SetUserOrganization(ben.ID, org1.ID) _ = store.SetUserOrganization(carl.ID, org2.ID) // Who sees whom (server admins follow the same rule) cases := []struct { a, b *domain.User want bool }{ {anna, ben, true}, {ben, anna, true}, {anna, carl, false}, {carl, anna, false}, {dora, anna, false}, {anna, dora, false}, {dora, dora, true}, } for _, c := range cases { if got := store.CanSee(c.a.ID, c.b.ID); got != c.want { t.Errorf("CanSee(%s, %s) = %v, want %v", c.a.DisplayName, c.b.DisplayName, got, c.want) } } if users, _ := store.GetDiscoverableUsers(anna.ID); len(users) != 2 { t.Errorf("Anna should see herself and Ben, got %d users", len(users)) } if users, _ := store.GetDiscoverableUsers(dora.ID); len(users) != 1 || users[0].ID != dora.ID { t.Errorf("Dora without organization should only see herself, got %+v", users) } // Sharing: visible people only; an invisible e-mail looks like an unknown one doc, _ := store.CreateDocument(anna.ID, "", "Einkauf", "📄", "", nil, nil, false) if _, err := store.CreateDocShare(anna.ID, doc.ID, ben.ID, "write"); err != nil { t.Fatalf("share with Ben failed: %v", err) } if _, err := store.CreateDocShare(anna.ID, doc.ID, "carl@example.com", "read"); !errors.Is(err, ErrTargetNotFound) { t.Errorf("share with invisible Carl must look like not found, got %v", err) } if _, err := store.CreateDocShare(anna.ID, doc.ID, "nobody@example.com", "read"); !errors.Is(err, ErrTargetNotFound) { t.Errorf("share with unknown address must be not found, got %v", err) } // Only the owner shares a list list, _ := store.CreateList(anna.ID, "Wocheneinkauf", "🛒", "shopping") if _, err := store.AddListShare(anna.ID, list.ID, "user", ben.ID, "editor"); err != nil { t.Fatalf("list share with Ben failed: %v", err) } if _, err := store.AddListShare(ben.ID, list.ID, "user", anna.ID, "editor"); err == nil { t.Errorf("an editor must not reshare a list") } // Groups: only members may share with them team, _ := store.CreateTeam(org1.ID, "WG", "") _ = store.AddTeamMember(team.ID, anna.ID, "admin") _ = store.AddTeamMember(team.ID, ben.ID, "member") tl, _ := store.CreateTaskList(anna.ID, "", "", "Haushalt", "🏠", "") if _, err := store.AddTaskListShare(tl.ID, "team", team.ID, "editor", anna.ID); err != nil { t.Fatalf("group share failed: %v", err) } carlList, _ := store.CreateTaskList(carl.ID, "", "", "Fremd", "🔒", "") if _, err := store.AddTaskListShare(carlList.ID, "team", team.ID, "editor", carl.ID); !errors.Is(err, ErrTargetNotFound) { t.Errorf("non-member must not share with a group, got %v", err) } task, _ := store.CreateThoughtWithDetails(anna.ID, "Bad putzen", "normal", "manual", "", tl.ID, "todo", "") _, _ = store.UpdateThoughtTriage(anna.ID, task.ID, "keep", "task", "", "", tl.ID) if _, err := store.SetTaskAssignees(anna.ID, task.ID, []string{ben.ID}); err != nil { t.Fatalf("assign Ben failed: %v", err) } // A dry run counts without changing anything n, err := store.PruneSharing(SharingPrivate, true) if err != nil || n == 0 { t.Fatalf("dry run should report affected rows, got %d (%v)", n, err) } if shares, _ := store.GetDocShares(doc.ID); len(shares) != 1 { t.Fatalf("dry run must not remove shares") } // Ben moves to the other organization: he loses shares, group and assignment if err := store.SetUserOrganization(ben.ID, org2.ID); err != nil { t.Fatalf("SetUserOrganization failed: %v", err) } if shares, _ := store.GetDocShares(doc.ID); len(shares) != 0 { t.Errorf("doc share with Ben should be gone, got %+v", shares) } if l, _ := store.GetListByID(anna.ID, list.ID); len(l.Shares) != 0 { t.Errorf("list share with Ben should be gone, got %+v", l.Shares) } if members, _ := store.GetTeamMembers(team.ID); len(members) != 1 { t.Errorf("Ben should have left the group, members: %+v", members) } if th, _ := store.GetThoughtByID(anna.ID, task.ID); len(th.Assignees) != 0 { t.Errorf("Ben's assignment should be gone, got %v", th.Assignees) } // Open mode: everyone sees everyone setSharingMode(t, store, SharingOpen) if !store.CanSee(anna.ID, carl.ID) || !store.CanSee(dora.ID, anna.ID) { t.Errorf("open mode should make everyone visible") } // Private mode: no sharing at all, groups disappear if _, err := store.PruneSharing(SharingPrivate, false); err != nil { t.Fatalf("PruneSharing private failed: %v", err) } setSharingMode(t, store, SharingPrivate) if store.CanSee(anna.ID, ben.ID) || store.CanShareWith(anna.ID, "team", team.ID) { t.Errorf("private mode must not allow seeing or sharing") } if tls, _ := store.GetTaskListShares(tl.ID); len(tls) != 0 { t.Errorf("private mode should remove all task list shares, got %+v", tls) } if _, err := store.GetTeamByID(team.ID); err == nil { t.Errorf("groups should be gone in private mode") } } func TestOutdatedSchemaIsWiped(t *testing.T) { path := filepath.Join(t.TempDir(), "old.db") store, err := NewStore(path) if err != nil { t.Fatalf("NewStore failed: %v", err) } _, _ = store.CreateUser("old@example.com", "hash", "Alt", "member") _, _ = store.db.Exec(`PRAGMA user_version = 1`) _ = store.Close() store, err = NewStore(path) if err != nil { t.Fatalf("reopen failed: %v", err) } defer store.Close() if n, _ := store.CountUsers(); n != 0 { t.Fatalf("outdated database should start empty, found %d users", n) } var v int _ = store.db.QueryRow(`PRAGMA user_version`).Scan(&v) if v != schemaVersion { t.Fatalf("user_version = %d, want %d", v, schemaVersion) } }