Files
kandev/apps/backend/internal/orgunit/service_test.go
T
João Salavisa 481359ddb2 feat(auth): organization units, tenancy, roles and scopes for team access (#2919)
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 13:40:43 +00:00

248 lines
7.2 KiB
Go

package orgunit
import (
"context"
"errors"
"sync"
"testing"
)
type stubCounter struct {
counts map[string]int
err error
}
func (s stubCounter) CountWorkspacesInUnit(_ context.Context, unitID string) (int, error) {
return s.counts[unitID], s.err
}
func newTestService(t *testing.T) *Service {
t.Helper()
svc := NewService(newTestStore(t), nil)
svc.SetWorkspaceCounter(stubCounter{counts: map[string]int{}})
return svc
}
// Both are idempotent because boot, the data migration, and organization or
// user creation all reach for them without coordinating. A second call that
// created a second unit would give a user two ancestries.
func TestEnsureRootAndPersonalAreIdempotent(t *testing.T) {
ctx := context.Background()
svc := newTestService(t)
first, err := svc.EnsureRoot(ctx, "org-1", "Acme")
if err != nil {
t.Fatalf("ensure root: %v", err)
}
second, err := svc.EnsureRoot(ctx, "org-1", "Acme")
if err != nil {
t.Fatalf("ensure root again: %v", err)
}
if first.ID != second.ID {
t.Fatalf("root ids differ: %s vs %s", first.ID, second.ID)
}
p1, err := svc.EnsurePersonal(ctx, "org-1", "ada", "Ada Lovelace")
if err != nil {
t.Fatalf("ensure personal: %v", err)
}
p2, err := svc.EnsurePersonal(ctx, "org-1", "ada", "Ada Lovelace")
if err != nil {
t.Fatalf("ensure personal again: %v", err)
}
if p1.ID != p2.ID {
t.Fatalf("personal ids differ: %s vs %s", p1.ID, p2.ID)
}
if p1.ParentID != first.ID {
t.Fatalf("personal unit parent = %q, want the root %q", p1.ParentID, first.ID)
}
}
// A personal unit is private because nobody else can be in it. If a member
// could be added, privacy would become a property some units have, which is
// the flag this model set out to remove.
func TestPersonalUnitRefusesMembers(t *testing.T) {
ctx := context.Background()
svc := newTestService(t)
if _, err := svc.EnsureRoot(ctx, "org-1", "Acme"); err != nil {
t.Fatalf("ensure root: %v", err)
}
personal, err := svc.EnsurePersonal(ctx, "org-1", "ada", "Ada")
if err != nil {
t.Fatalf("ensure personal: %v", err)
}
err = svc.SetMember(ctx, personal.ID, "grace", "collaborator", "ada")
if !errors.Is(err, ErrPersonalNoMember) {
t.Fatalf("adding a member to a personal unit returned %v, want ErrPersonalNoMember", err)
}
}
// Moving a unit under its own descendant would detach the subtree from the
// root, and every reach answer for it afterwards would be a guess.
func TestMoveRefusesCycle(t *testing.T) {
ctx := context.Background()
svc := newTestService(t)
root, err := svc.EnsureRoot(ctx, "org-1", "Acme")
if err != nil {
t.Fatalf("ensure root: %v", err)
}
dept, err := svc.Create(ctx, root.ID, "Platform")
if err != nil {
t.Fatalf("create dept: %v", err)
}
team, err := svc.Create(ctx, dept.ID, "Runtime")
if err != nil {
t.Fatalf("create team: %v", err)
}
if err := svc.Move(ctx, dept.ID, team.ID); !errors.Is(err, ErrCycle) {
t.Fatalf("moving a unit beneath its own child returned %v, want ErrCycle", err)
}
if err := svc.Move(ctx, root.ID, dept.ID); !errors.Is(err, ErrProtectedUnit) {
t.Fatalf("moving the root returned %v, want ErrProtectedUnit", err)
}
}
// Deleting a unit that still holds workspaces would strand them: they would
// have a placement pointing at nothing and no ancestry to resolve reach from.
func TestDeleteRefusesWhileOccupied(t *testing.T) {
ctx := context.Background()
svc := newTestService(t)
root, err := svc.EnsureRoot(ctx, "org-1", "Acme")
if err != nil {
t.Fatalf("ensure root: %v", err)
}
dept, err := svc.Create(ctx, root.ID, "Platform")
if err != nil {
t.Fatalf("create dept: %v", err)
}
team, err := svc.Create(ctx, dept.ID, "Runtime")
if err != nil {
t.Fatalf("create team: %v", err)
}
if err := svc.Delete(ctx, dept.ID); !errors.Is(err, ErrNotEmpty) {
t.Fatalf("deleting a unit with a child returned %v, want ErrNotEmpty", err)
}
svc.SetWorkspaceCounter(stubCounter{counts: map[string]int{team.ID: 1}})
if err := svc.Delete(ctx, team.ID); !errors.Is(err, ErrNotEmpty) {
t.Fatalf("deleting a unit holding a workspace returned %v, want ErrNotEmpty", err)
}
svc.SetWorkspaceCounter(stubCounter{counts: map[string]int{}})
if err := svc.Delete(ctx, team.ID); err != nil {
t.Fatalf("deleting an empty unit: %v", err)
}
}
// An unwired occupancy seam must not read as "no workspaces here". Failing
// closed keeps a construction mistake from deleting a populated unit.
func TestDeleteFailsClosedWithoutCounter(t *testing.T) {
ctx := context.Background()
svc := NewService(newTestStore(t), nil)
root, err := svc.EnsureRoot(ctx, "org-1", "Acme")
if err != nil {
t.Fatalf("ensure root: %v", err)
}
dept, err := svc.Create(ctx, root.ID, "Platform")
if err != nil {
t.Fatalf("create dept: %v", err)
}
if err := svc.Delete(ctx, dept.ID); !errors.Is(err, ErrNotEmpty) {
t.Fatalf("delete with no counter wired returned %v, want a refusal", err)
}
}
// Two first-time requests for one account can both pass the lookup inside
// EnsurePersonal. The unique index keeps the database right, but the loser
// used to get an error, and its caller's fallback was the organization root:
// a workspace meant to be private would have been created where everyone
// reaches it.
//
// This races for real rather than seeding the row first, because seeding it
// makes the early lookup succeed and the conflict path is never reached. That
// is exactly the mistake that made the first version of this test vacuous.
func TestEnsurePersonalIsRaceSafe(t *testing.T) {
ctx := context.Background()
svc := newTestService(t)
if _, err := svc.EnsureRoot(ctx, "org-1", "Acme"); err != nil {
t.Fatalf("ensure root: %v", err)
}
const racers = 8
ids := make([]string, racers)
errs := make([]error, racers)
start := make(chan struct{})
var wg sync.WaitGroup
for i := 0; i < racers; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
<-start
unit, err := svc.EnsurePersonal(ctx, "org-1", "ada", "Ada")
errs[i] = err
if unit != nil {
ids[i] = unit.ID
}
}(i)
}
close(start)
wg.Wait()
for i, err := range errs {
if err != nil {
t.Fatalf("racer %d got an error instead of the existing unit: %v", i, err)
}
}
for i, id := range ids {
if id == "" || id != ids[0] {
t.Fatalf("racer %d returned %q, want every caller to get %q", i, id, ids[0])
}
}
units, err := svc.Store().ListByOrg(ctx, "org-1")
if err != nil {
t.Fatalf("list units: %v", err)
}
if len(units) != 2 {
t.Fatalf("units = %d, want a root and exactly one personal unit", len(units))
}
}
// Same race on the root, where a spurious error breaks boot rather than reach.
func TestEnsureRootIsRaceSafe(t *testing.T) {
ctx := context.Background()
svc := newTestService(t)
const racers = 8
ids := make([]string, racers)
errs := make([]error, racers)
start := make(chan struct{})
var wg sync.WaitGroup
for i := 0; i < racers; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
<-start
unit, err := svc.EnsureRoot(ctx, "org-1", "Acme")
errs[i] = err
if unit != nil {
ids[i] = unit.ID
}
}(i)
}
close(start)
wg.Wait()
for i, err := range errs {
if err != nil {
t.Fatalf("racer %d got an error instead of the existing root: %v", i, err)
}
if ids[i] != ids[0] || ids[i] == "" {
t.Fatalf("racer %d returned %q, want every caller to get %q", i, ids[i], ids[0])
}
}
}