Files
MindSift/internal/storage/task_assignees_test.go
T
sandrodachandClaude Opus 5.5 84395f3322
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m30s
feat: organizations decide who sees whom; sharing only by owners
- Server admins manage organizations (one per user); new admin section,
  organization on user creation and invites, setup creates the first one
- Sharing modes private / organizations / open, enforced centrally
  (CanSee, CanShareWith) for directory, all shares and groups; unknown and
  invisible targets give the same error
- Losing visibility (org change, mode change, leaving a group) prunes shares,
  group memberships and task assignments; dry run warns before tightening
- Only owners share; editors can no longer reshare lists or remove others'
  shares; legacy task_lists.team_id and old /organizations endpoints removed
  (FLAW-06, FLAW-10, FLAW-24)
- Schema version: outdated databases are wiped and recreated on start
- Fix setup sample data (createSampleData was ignored); setup and admin
  use a shared ToggleSwitch instead of checkboxes
- Docs: sharing-and-permissions rewritten, ui-patterns, REST, DB, flaws

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:00:33 +01:00

123 lines
4.5 KiB
Go

package storage
import (
"errors"
"path/filepath"
"testing"
)
func TestTaskAssigneesFollowListAccess(t *testing.T) {
store, err := NewStore(filepath.Join(t.TempDir(), "assignees.db"))
if err != nil {
t.Fatalf("NewStore failed: %v", err)
}
defer store.Close()
owner, _ := store.EnsureDefaultUser()
direct, _ := store.CreateUser("direct@example.com", "hash", "Direkt", "member")
viaTeam, _ := store.CreateUser("team@example.com", "hash", "Team", "member")
stranger, _ := store.CreateUser("stranger@example.com", "hash", "Fremd", "member")
tl, err := store.CreateTaskList(owner.ID, "", "", "Haushalt", "🏠", "")
if err != nil {
t.Fatalf("CreateTaskList failed: %v", err)
}
task, err := store.CreateThoughtWithDetails(owner.ID, "Fenster putzen", "normal", "manual", "", tl.ID, "todo", "")
if err != nil {
t.Fatalf("CreateThoughtWithDetails failed: %v", err)
}
if _, err := store.UpdateThoughtTriage(owner.ID, task.ID, "keep", "task", "", "", tl.ID); err != nil {
t.Fatalf("UpdateThoughtTriage failed: %v", err)
}
// Everyone but the stranger shares the organization, so they can see each other
org, _ := store.CreateOrganization("Familie")
for _, u := range []string{owner.ID, direct.ID, viaTeam.ID} {
if err := store.SetUserOrganization(u, org.ID); err != nil {
t.Fatalf("SetUserOrganization failed: %v", err)
}
}
team, err := store.CreateTeam(org.ID, "WG", "")
if err != nil {
t.Fatalf("CreateTeam failed: %v", err)
}
_ = store.AddTeamMember(team.ID, owner.ID, "admin")
_ = store.AddTeamMember(team.ID, viaTeam.ID, "member")
if _, err := store.AddTaskListShare(tl.ID, "user", direct.ID, "editor", owner.ID); err != nil {
t.Fatalf("AddTaskListShare user failed: %v", err)
}
if _, err := store.AddTaskListShare(tl.ID, "team", team.ID, "viewer", owner.ID); err != nil {
t.Fatalf("AddTaskListShare team failed: %v", err)
}
if _, err := store.SetTaskAssignees(owner.ID, task.ID, []string{stranger.ID}); !errors.Is(err, ErrAssigneeNoAccess) {
t.Fatalf("assigning someone without access must fail, got %v", err)
}
if _, err := store.SetTaskAssignees(viaTeam.ID, task.ID, []string{viaTeam.ID}); !errors.Is(err, ErrTaskListReadOnly) {
t.Fatalf("viewer must not assign, got %v", err)
}
got, err := store.SetTaskAssignees(direct.ID, task.ID, []string{owner.ID, direct.ID, viaTeam.ID, direct.ID})
if err != nil || len(got) != 3 {
t.Fatalf("expected 3 assignees, got %v (%v)", got, err)
}
tasks, _ := store.GetTasks(owner.ID, tl.ID)
if len(tasks) != 1 || len(tasks[0].Assignees) != 3 {
t.Fatalf("GetTasks should carry assignees, got %+v", tasks)
}
// Leaving the team removes the team member's assignment
if err := store.RemoveTeamMember(team.ID, viaTeam.ID); err != nil {
t.Fatalf("RemoveTeamMember failed: %v", err)
}
assertAssignees(t, store, owner.ID, task.ID, owner.ID, direct.ID)
// Removing the direct share removes that user's assignment, the owner stays
if err := store.RemoveTaskListShare(tl.ID, direct.ID); err != nil {
t.Fatalf("RemoveTaskListShare failed: %v", err)
}
assertAssignees(t, store, owner.ID, task.ID, owner.ID)
// Access through a second path keeps the assignment
_ = store.AddTeamMember(team.ID, direct.ID, "member")
_, _ = store.AddTaskListShare(tl.ID, "user", direct.ID, "editor", owner.ID)
if _, err := store.SetTaskAssignees(owner.ID, task.ID, []string{owner.ID, direct.ID}); err != nil {
t.Fatalf("re-assign failed: %v", err)
}
_ = store.RemoveTaskListShare(tl.ID, direct.ID)
assertAssignees(t, store, owner.ID, task.ID, owner.ID, direct.ID)
// Deleting the team takes the last path away
if err := store.DeleteTeam(team.ID); err != nil {
t.Fatalf("DeleteTeam failed: %v", err)
}
assertAssignees(t, store, owner.ID, task.ID, owner.ID)
// Moving the task out of the list drops all assignments
empty := ""
if _, err := store.UpdateThoughtDetails(owner.ID, task.ID, nil, nil, nil, nil, &empty, nil, nil, nil, nil); err != nil {
t.Fatalf("UpdateThoughtDetails failed: %v", err)
}
assertAssignees(t, store, owner.ID, task.ID)
}
func assertAssignees(t *testing.T, store *Store, viewerID, taskID string, want ...string) {
t.Helper()
th, err := store.GetThoughtByID(viewerID, taskID)
if err != nil {
t.Fatalf("GetThoughtByID failed: %v", err)
}
if len(th.Assignees) != len(want) {
t.Fatalf("expected assignees %v, got %v", want, th.Assignees)
}
have := map[string]bool{}
for _, id := range th.Assignees {
have[id] = true
}
for _, id := range want {
if !have[id] {
t.Fatalf("expected assignees %v, got %v", want, th.Assignees)
}
}
}