All installations are past schema version 2, so the one-time reset is gone; data is never deleted on start (demo mode still resets). user_version is kept for future migrations. Legal test uses an in-memory DB (cleanup race). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
172 lines
6.6 KiB
Go
172 lines
6.6 KiB
Go
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")
|
|
}
|
|
}
|
|
|
|
// Existing data must survive a restart, whatever user_version the database carries.
|
|
func TestDataSurvivesRestart(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "keep.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 != 1 {
|
|
t.Fatalf("data must be kept on restart, 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)
|
|
}
|
|
}
|