// SPDX-License-Identifier: MPL-2.0 package access import ( "context" "errors" "slices" "strings" "testing" "time" ) type roleSetRepositoryStub struct { repositoryStub calls int expected []string roles []Binding audit AuditEvent } func (r *roleSetRepositoryStub) ReplaceOrganizationUserRoles(_ context.Context, expected []string, bindings []Binding, _ string, audit AuditEvent) error { r.calls++ r.expected, r.roles, r.audit = expected, bindings, audit return nil } func TestRoleSetServiceBoundsAndCanonicalCopies(t *testing.T) { policy := Policy{Roles: map[string]string{"owner": "Owner", "buyer": "Buyer", "billing": "Billing"}, Permissions: map[string]string{"purchase": "Purchase"}, Grants: map[string][]string{"owner": {"purchase"}, "buyer": {"purchase"}, "billing": {}}} r := &roleSetRepositoryStub{} service, err := New(r, policy, Options{OwnerRole: "owner"}) if err != nil { t.Fatal(err) } input := OrganizationUserRolesChange{OrganizationID: "organization-123", UserID: "member-12345678", ActorUserID: "owner-12345678", Roles: []string{"buyer", "billing"}, ExpectedBindingIDs: []string{"binding-second", "binding-first"}, RequestID: "request-roles"} bindings, err := service.ReplaceOrganizationUserRoles(t.Context(), input) if err != nil { t.Fatal(err) } if r.calls != 1 || len(bindings) != 2 || bindings[0].Role != "billing" || bindings[1].Role != "buyer" || bindings[0].ID == bindings[1].ID || !slices.Equal(r.expected, []string{"binding-first", "binding-second"}) { t.Fatalf("bindings=%v expected=%v calls=%d", bindings, r.expected, r.calls) } if !slices.Equal(input.Roles, []string{"buyer", "billing"}) || !slices.Equal(input.ExpectedBindingIDs, []string{"binding-second", "binding-first"}) { t.Fatal("caller input was sorted in place") } if r.audit.RequestID != input.RequestID || r.audit.ActorUserID != input.ActorUserID || r.audit.ResourceID != input.UserID { t.Fatalf("audit=%+v", r.audit) } for _, roles := range [][]string{nil, {"buyer", "buyer"}, {"missing"}, make([]string, 17)} { invalid := input invalid.Roles = roles if _, err = service.ReplaceOrganizationUserRoles(t.Context(), invalid); err == nil { t.Fatalf("invalid roles=%v", roles) } } for _, expected := range [][]string{{"bad"}, {"binding-first", "binding-first"}, make([]string, 17)} { invalid := input invalid.ExpectedBindingIDs = expected if _, err = service.ReplaceOrganizationUserRoles(t.Context(), invalid); err == nil { t.Fatalf("invalid IDs=%v", expected) } } if r.calls != 1 { t.Fatal("invalid input reached repository") } legacy, err := New(&repositoryStub{}, policy, Options{OwnerRole: "owner"}) if err != nil { t.Fatal(err) } if _, err = legacy.ReplaceOrganizationUserRoles(t.Context(), input); !errors.Is(err, ErrRoleSetUnsupported) { t.Fatalf("fallback=%v", err) } broken, err := New(r, policy, Options{OwnerRole: "owner", Random: strings.NewReader("")}) if err != nil { t.Fatal(err) } if _, err = broken.ReplaceOrganizationUserRoles(t.Context(), input); err == nil || r.calls != 1 { t.Fatal("random failure reached storage") } withoutOwner, err := New(r, policy, Options{Now: func() time.Time { return time.Unix(2000, 0) }}) if err != nil { t.Fatal(err) } if _, err = withoutOwner.ReplaceOrganizationUserRoles(t.Context(), input); err == nil || r.calls != 1 { t.Fatal("role set without owner boundary accepted") } }