// SPDX-License-Identifier: MPL-2.0 package organizations import ( "context" "errors" "strings" "testing" "time" "gamertan.com/web/access" ) type ownedRepositoryStub struct { repositoryStub setup OwnedOrganization calls int err error } func (repository *ownedRepositoryStub) CreateOwnedOrganization(_ context.Context, setup OwnedOrganization) error { repository.calls++ repository.setup = setup return repository.err } func TestOwnedOrganizationUsesConfiguredRoleAndAtomicRepository(t *testing.T) { now := time.Unix(1000, 0).UTC() repository := &ownedRepositoryStub{} service, err := New(repository, Options{OwnerRole: "customer.owner", Now: func() time.Time { return now }}) if err != nil { t.Fatal(err) } input := CreateOrganization{Slug: " CLIENT-BUSINESS ", Name: " Client Business ", OwnerUserID: "customer-12345", RequestID: "request-creation"} organization, err := service.CreateOwnedOrganization(t.Context(), input) if err != nil { t.Fatal(err) } setup := repository.setup if repository.calls != 1 || repository.organization.ID != "" || setup.Organization != organization || organization.Slug != "client-business" || organization.Name != "Client Business" { t.Fatalf("unexpected creation: %+v", setup) } if setup.Membership.UserID != input.OwnerUserID || setup.OwnerBinding.SubjectKind != access.User || setup.OwnerBinding.SubjectID != input.OwnerUserID || setup.OwnerBinding.Role != "customer.owner" || setup.OwnerBinding.Scope != (access.Scope{OrganizationID: organization.ID}) || setup.OwnerBinding.GrantedBy != input.OwnerUserID { t.Fatalf("unexpected owner: %+v", setup) } if setup.OrganizationAudit.RequestID != input.RequestID || setup.AccessAudit.RequestID != input.RequestID || setup.AccessAudit.ResourceID != setup.OwnerBinding.ID || !setup.OwnerBinding.GrantedAt.Equal(now) { t.Fatalf("unexpected audits: %+v", setup) } } func TestOwnedOrganizationFailsWithoutAtomicSupport(t *testing.T) { repository := &repositoryStub{} service, _ := New(repository, Options{OwnerRole: "customer.owner"}) organization, err := service.CreateOwnedOrganization(t.Context(), CreateOrganization{Slug: "client-business", Name: "Client Business", OwnerUserID: "customer-12345"}) if !errors.Is(err, ErrOwnedCreationUnsupported) || organization.ID != "" || repository.organization.ID != "" { t.Fatalf("non-atomic fallback: organization=%+v err=%v", organization, err) } } func TestOwnedOrganizationRejectsInvalidSetupBeforeStorage(t *testing.T) { for _, test := range []struct { name string role string request string random string }{ {name: "missing role", random: strings.Repeat("a", 200)}, {name: "bad request ID", role: "customer.owner", request: "request\nsecret", random: strings.Repeat("a", 200)}, {name: "random failure", role: "customer.owner", random: strings.Repeat("a", 40)}, } { t.Run(test.name, func(t *testing.T) { repository := &ownedRepositoryStub{} service, err := New(repository, Options{OwnerRole: test.role, Random: strings.NewReader(test.random)}) if err != nil { t.Fatal(err) } organization, err := service.CreateOwnedOrganization(t.Context(), CreateOrganization{Slug: "client-business", Name: "Client Business", OwnerUserID: "customer-12345", RequestID: test.request}) if err == nil || organization.ID != "" || repository.calls != 0 { t.Fatalf("organization=%+v calls=%d err=%v", organization, repository.calls, err) } }) } } func TestOwnedOrganizationDoesNotReturnUncommittedIdentity(t *testing.T) { want := errors.New("durability failure") repository := &ownedRepositoryStub{err: want} service, _ := New(repository, Options{OwnerRole: "customer.owner"}) organization, err := service.CreateOwnedOrganization(t.Context(), CreateOrganization{Slug: "client-business", Name: "Client Business", OwnerUserID: "customer-12345"}) if !errors.Is(err, want) || organization.ID != "" || repository.calls != 1 { t.Fatalf("organization=%+v calls=%d err=%v", organization, repository.calls, err) } } func TestOwnedManagementHasNoPreflightOnlyFallback(t *testing.T) { service, err := New(&repositoryStub{}, Options{OwnerRole: "customer.owner"}) if err != nil { t.Fatal(err) } _, err = service.UpdateOwnedOrganization(t.Context(), UpdateOrganization{ID: "organization-12345", Slug: "business", Name: "Business", ActorUserID: "customer-12345", ExpectedRevision: 1}) if !errors.Is(err, ErrOwnedManagementUnsupported) { t.Fatalf("profile fallback: %v", err) } err = service.ChangeOwnedMembershipStatus(t.Context(), MembershipStatusChange{OrganizationID: "organization-12345", UserID: "member-12345", ActorUserID: "customer-12345", ExpectedStatus: "active", Status: "suspended"}) if !errors.Is(err, ErrOwnedManagementUnsupported) { t.Fatalf("status fallback: %v", err) } err = service.RemoveOwnedMembershipIfCurrent(t.Context(), MembershipRemoval{OrganizationID: "organization-12345", UserID: "member-12345", ActorUserID: "customer-12345", ExpectedStatus: "active"}) if !errors.Is(err, ErrOwnedManagementUnsupported) { t.Fatalf("removal fallback: %v", err) } }