package idear import ( "errors" "testing" "time" ) // member is a small constructor for test fixtures: an active member // with the given id and role, or a deactivated one when deactivated is // true. Kept separate from the struct literals below so each case in // the table reads as data, not setup logic. func member(id int64, role Role, deactivated bool) *Member { m := &Member{ID: id, Role: role} if deactivated { t := time.Unix(0, 0) m.DeactivatedAt = &t } return m } // TestMayActOn_Matrix states, cell by cell, whether actor may act on // target. Every want is written down by hand rather than recomputed // from rank() or AtLeast() — a test that re-derives the implementation's // own comparison proves only that the comparison equals itself, which // is exactly the bug round 1's auditor found in the allow-list tests. func TestMayActOn_Matrix(t *testing.T) { // Distinct ids on both sides so "different person" cases are never // accidentally also self-action cases. activeOwner := member(1, RoleOwner, false) activeAdmin := member(2, RoleAdmin, false) activeMember := member(3, RoleMember, false) otherOwner := member(10, RoleOwner, false) otherAdmin := member(20, RoleAdmin, false) otherMember := member(30, RoleMember, false) deactivatedOwner := member(1, RoleOwner, true) deactivatedAdmin := member(2, RoleAdmin, true) cases := []struct { name string actor *Member target *Member wantErr bool }{ // The full 3x3 actor/target matrix, actor and target always // distinct people. {"owner acts on other owner", activeOwner, otherOwner, true}, {"owner acts on admin", activeOwner, otherAdmin, false}, {"owner acts on member", activeOwner, otherMember, false}, {"admin acts on owner", activeAdmin, otherOwner, true}, {"admin acts on other admin", activeAdmin, otherAdmin, true}, {"admin acts on member", activeAdmin, otherMember, false}, {"member acts on owner", activeMember, otherOwner, true}, {"member acts on admin", activeMember, otherAdmin, true}, {"member acts on other member", activeMember, otherMember, true}, // Self-action: refused for every rank, including the Owner. {"owner acts on self", activeOwner, member(1, RoleOwner, false), true}, {"admin acts on self", activeAdmin, member(2, RoleAdmin, false), true}, {"member acts on self", activeMember, member(3, RoleMember, false), true}, // Inactive actor: refused regardless of rank, before rank is // even considered. {"deactivated owner acts on member", deactivatedOwner, otherMember, true}, {"deactivated admin acts on member", deactivatedAdmin, otherMember, true}, // Defensive: a nil actor or nil target can never be permitted. {"nil actor", nil, otherMember, true}, {"nil target", activeOwner, nil, true}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { err := MayActOn(c.actor, c.target) if c.wantErr { if err == nil { t.Fatalf("MayActOn(%v, %v) = nil, want ErrForbidden", c.actor, c.target) } if !errors.Is(err, ErrForbidden) { t.Fatalf("MayActOn(%v, %v) = %v, want wrapped ErrForbidden", c.actor, c.target, err) } return } if err != nil { t.Fatalf("MayActOn(%v, %v) = %v, want nil", c.actor, c.target, err) } }) } }