package organization import ( "context" "testing" "time" "git.sendico.io/tech/sxchnge/bff/pkg/model" "go.mongodb.org/mongo-driver/v2/bson" ) func provisionedOrganization(t *testing.T, db *OrganizationDB, ownerRef bson.ObjectID, name, leaseOwner string) *model.Organization { t.Helper() org := &model.Organization{Members: []bson.ObjectID{}} org.SetID(bson.NewObjectID()) org.Name = name org.SignupState = model.OrganizationSignupStatePending org.Provisioning = &model.OrganizationProvisioning{ OwnerRef: ownerRef, Name: name, Origin: "backoffice", StartedAt: time.Now().UTC(), LeaseOwner: leaseOwner, LeaseExpiresAt: time.Now().UTC().Add(10 * time.Minute), } if err := db.Unprotected().Create(context.Background(), org); err != nil { t.Fatalf("failed to store organization: %v", err) } return org } func TestAddMemberIsAPointPatch(t *testing.T) { db := newTestOrganizationDB(t) ownerRef := bson.NewObjectID() org := provisionedOrganization(t, db, ownerRef, "ProCorp", "operator/attempt-1") memberRef := bson.NewObjectID() added, err := db.AddMember(context.Background(), org.ID, memberRef) if err != nil || !added { t.Fatalf("first add must report success: added=%v err=%v", added, err) } // The second add must be a no-op, not an error: transaction replays and // concurrent joins both hit this path. added, err = db.AddMember(context.Background(), org.ID, memberRef) if err != nil || added { t.Fatalf("second add must report an existing member: added=%v err=%v", added, err) } stored := &model.Organization{} if err := db.Unprotected().Get(context.Background(), org.ID, stored); err != nil { t.Fatalf("failed to read organization back: %v", err) } if len(stored.Members) != 1 || stored.Members[0] != memberRef { t.Fatalf("members mismatch: got=%v", stored.Members) } // The membership write must not touch the provisioning lease: the fence in // FinalizeProvisioning checks the stored lease, not this attempt's copy. if stored.Provisioning == nil || stored.Provisioning.LeaseOwner != "operator/attempt-1" { t.Fatalf("membership write clobbered the provisioning lease: %+v", stored.Provisioning) } } func TestFinalizeProvisioningKeepsProvenance(t *testing.T) { db := newTestOrganizationDB(t) ownerRef := bson.NewObjectID() org := provisionedOrganization(t, db, ownerRef, "ProCorp", "operator/attempt-1") finalized, err := db.FinalizeProvisioning(context.Background(), org.ID, "operator/attempt-1") if err != nil || !finalized { t.Fatalf("finalization by the lease owner must succeed: finalized=%v err=%v", finalized, err) } stored := &model.Organization{} if err := db.Unprotected().Get(context.Background(), org.ID, stored); err != nil { t.Fatalf("failed to read organization back: %v", err) } if stored.SignupState != model.OrganizationSignupStateActive { t.Fatalf("organization must be active, got=%s", stored.SignupState) } // Provenance survives activation so a retry after a lost success response // can recognize the organization instead of provisioning a duplicate. if stored.Provisioning == nil || stored.Provisioning.OwnerRef != ownerRef { t.Fatalf("provisioning provenance must survive activation: %+v", stored.Provisioning) } if stored.Provisioning.LeaseOwner != "" || !stored.Provisioning.LeaseExpiresAt.IsZero() { t.Fatalf("lease must be cleared on activation: %+v", stored.Provisioning) } // A replayed finalization must not match: the organization left pending. finalized, err = db.FinalizeProvisioning(context.Background(), org.ID, "operator/attempt-1") if err != nil || finalized { t.Fatalf("an active organization must not finalize again: finalized=%v err=%v", finalized, err) } } func TestFinalizeProvisioningRejectsTakenOverLease(t *testing.T) { db := newTestOrganizationDB(t) org := provisionedOrganization(t, db, bson.NewObjectID(), "ProCorp", "operator/attempt-2") finalized, err := db.FinalizeProvisioning(context.Background(), org.ID, "operator/attempt-1") if err != nil || finalized { t.Fatalf("an attempt whose lease was taken over must not finalize: finalized=%v err=%v", finalized, err) } } func TestFindActiveProvisioned(t *testing.T) { db := newTestOrganizationDB(t) ownerRef := bson.NewObjectID() org := provisionedOrganization(t, db, ownerRef, "ProCorp", "operator/attempt-1") // Pending organizations belong to FindPendingProvisioning, not here. found, err := db.FindActiveProvisioned(context.Background(), ownerRef, "ProCorp") if err != nil || found != nil { t.Fatalf("a pending organization must not be reported as activated: found=%v err=%v", found, err) } if _, err := db.FinalizeProvisioning(context.Background(), org.ID, "operator/attempt-1"); err != nil { t.Fatalf("failed to finalize: %v", err) } found, err = db.FindActiveProvisioned(context.Background(), ownerRef, "ProCorp") if err != nil || found == nil || found.ID != org.ID { t.Fatalf("the activated organization must be found: found=%v err=%v", found, err) } if found, err = db.FindActiveProvisioned(context.Background(), ownerRef, "Other"); err != nil || found != nil { t.Fatalf("a different name must not match: found=%v err=%v", found, err) } } // The dedup key is the provisioning name snapshot: renaming an activated // organization must not detach it from a retry of the create that produced it, // or the retry would provision a duplicate. func TestFindActiveProvisionedSurvivesRename(t *testing.T) { db := newTestOrganizationDB(t) ownerRef := bson.NewObjectID() org := provisionedOrganization(t, db, ownerRef, "ProCorp", "operator/attempt-1") if _, err := db.FinalizeProvisioning(context.Background(), org.ID, "operator/attempt-1"); err != nil { t.Fatalf("failed to finalize: %v", err) } stored := &model.Organization{} if err := db.Unprotected().Get(context.Background(), org.ID, stored); err != nil { t.Fatalf("failed to read organization back: %v", err) } stored.Name = "Renamed Corp" if err := db.Unprotected().Update(context.Background(), stored); err != nil { t.Fatalf("failed to rename organization: %v", err) } found, err := db.FindActiveProvisioned(context.Background(), ownerRef, "ProCorp") if err != nil || found == nil || found.ID != org.ID { t.Fatalf("the renamed organization must still match its provisioning name: found=%v err=%v", found, err) } } // Archiving is how an operator retires a name: an archived organization must // stop blocking the creation of a fresh one with the same owner and name. func TestFindActiveProvisionedReleasesArchivedName(t *testing.T) { db := newTestOrganizationDB(t) ownerRef := bson.NewObjectID() org := provisionedOrganization(t, db, ownerRef, "ProCorp", "operator/attempt-1") if _, err := db.FinalizeProvisioning(context.Background(), org.ID, "operator/attempt-1"); err != nil { t.Fatalf("failed to finalize: %v", err) } stored := &model.Organization{} if err := db.Unprotected().Get(context.Background(), org.ID, stored); err != nil { t.Fatalf("failed to read organization back: %v", err) } stored.Archived = true if err := db.Unprotected().Update(context.Background(), stored); err != nil { t.Fatalf("failed to archive organization: %v", err) } found, err := db.FindActiveProvisioned(context.Background(), ownerRef, "ProCorp") if err != nil || found != nil { t.Fatalf("an archived organization must release its name: found=%v err=%v", found, err) } }