181 lines
7.3 KiB
Go
181 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|