package domain_test import ( "errors" "math" "reflect" "strings" "testing" party "github.com/pharos-ai-factory/pharos-platform/services/beto/contexts/party_governance/domain" foundation "github.com/pharos-ai-factory/pharos-platform/services/beto/foundation/domain" ) const ( partyID = "8f6a22e9-4cf0-4eb6-8414-a6c2e337ad4e" bindingID = "a2c588d3-cc27-4e34-9b4f-8776f74c8d51" ) func registeredParty(t *testing.T) *party.PartyAuthentication { t.Helper() p, err := party.RegisterParty(partyID, "https://identity.example/realm", "subject") if err != nil { t.Fatal(err) } return p } func expectRejection(t *testing.T, err error, code string) { t.Helper() var expected foundation.ExpectedError if !errors.As(err, &expected) || expected.Rejection().Code != code { t.Fatalf("expected %s rejection, got %v", code, err) } } func expectCorruption(t *testing.T, err error) { t.Helper() var expected foundation.ExpectedError if err == nil || errors.As(err, &expected) || !strings.Contains(err.Error(), "corrupt aggregate state") { t.Fatalf("expected a retryable corruption error, got %v", err) } } func TestRegistrationPreservesExactAuthenticationIdentity(t *testing.T) { for _, identity := range [][2]string{ {"https://identity.example/realm", "subject"}, {"https://IDENTITY.example/realm", "Subject"}, {"https://identity.example/realm/", " subject "}, {"https://other.example/realm", "subject"}, } { t.Run(identity[0]+"/"+identity[1], func(t *testing.T) { p, err := party.RegisterParty(partyID, identity[0], identity[1]) if err != nil { t.Fatal(err) } state := p.Snapshot() if state.PartyID != partyID || state.Issuer != identity[0] || state.Subject != identity[1] { t.Fatalf("registration changed supplied identity: %+v", state) } if state.AccessState != party.AccessEnabled || state.IdentityState != party.IdentityEnabled || state.AuthVersion != 1 || !p.AuthenticationEligible() { t.Fatalf("invalid registration state: %+v", state) } want := []foundation.DomainEvent{party.PartyRegistered{PartyID: partyID, AuthVersion: 1}} if !reflect.DeepEqual(p.Events(), want) { t.Fatalf("unexpected registration fact: %#v", p.Events()) } }) } } func TestRegistrationRejectsMalformedIdentity(t *testing.T) { for _, id := range []string{"", "provider-user-42", strings.ToUpper(partyID), "00000000-0000-0000-0000-000000000000"} { _, err := party.RegisterParty(id, "issuer", "subject") expectRejection(t, err, "invalid_id") } for _, identity := range [][2]string{{"", "subject"}, {"issuer", ""}, {"issuer", string([]byte{0xff})}} { _, err := party.RegisterParty(partyID, identity[0], identity[1]) expectRejection(t, err, "invalid_authentication_binding") } } func TestAccessChangesFencePreviousAuthenticationGenerations(t *testing.T) { p := registeredParty(t) p.DrainEvents() initial := p.Snapshot() if err := p.SetAccessState(party.AccessEnabled, party.ReasonAdministrative); err != nil { t.Fatal(err) } if !p.Snapshot().Equal(initial) || len(p.Events()) != 0 { t.Fatal("unchanged access altered authority or emitted a fact") } if err := p.SetAccessState(party.AccessDisabled, party.ReasonSecurity); err != nil { t.Fatal(err) } disabled := p.Snapshot() if p.AuthenticationEligible() || disabled.AuthVersion != 2 { t.Fatalf("disabled Party retained authentication authority: %+v", disabled) } want := []foundation.DomainEvent{party.PartyAuthenticationChanged{ PartyID: partyID, AuthVersion: 2, AccessState: party.AccessDisabled, AuthenticationEligible: false, ReasonCategory: party.ReasonSecurity, }} if !reflect.DeepEqual(p.DrainEvents(), want) { t.Fatal("disablement fact does not describe the new authority") } if err := p.SetAccessState(party.AccessDisabled, party.ReasonAdministrative); err != nil { t.Fatal(err) } if !p.Snapshot().Equal(disabled) || len(p.Events()) != 0 { t.Fatal("repeated disablement advanced generation or emitted a fact") } if err := p.SetAccessState(party.AccessEnabled, party.ReasonAdministrative); err != nil { t.Fatal(err) } if !p.AuthenticationEligible() || p.Snapshot().AuthVersion != 3 { t.Fatal("reenablement did not establish a new generation") } event, ok := p.Events()[0].(party.PartyAuthenticationChanged) if !ok || !event.AuthenticationEligible || event.AuthVersion != 3 || event.AccessState != party.AccessEnabled || event.ReasonCategory != party.ReasonAdministrative { t.Fatalf("unexpected reenablement fact: %#v", p.Events()) } if p.Snapshot().Issuer != initial.Issuer || p.Snapshot().Subject != initial.Subject { t.Fatal("access transition changed the authentication binding") } } func TestRejectedAccessChangesPreserveStateAndFacts(t *testing.T) { p := registeredParty(t) before, facts := p.Snapshot(), p.Events() expectRejection(t, p.SetAccessState("unknown", party.ReasonSecurity), "invalid_access_state") for _, reason := range []party.ReasonCategory{"", "operator note", party.ReasonIdentityRecovery, party.ReasonProviderAccountRemoved} { expectRejection(t, p.SetAccessState(party.AccessDisabled, reason), "invalid_reason_category") } if !p.Snapshot().Equal(before) || !reflect.DeepEqual(p.Events(), facts) { t.Fatal("rejected change mutated authority") } state := p.Snapshot() state.AuthVersion = math.MaxUint64 p, err := party.RestorePartyAuthentication(state) if err != nil { t.Fatal(err) } expectRejection(t, p.SetAccessState(party.AccessDisabled, party.ReasonSecurity), "auth_version_exhausted") if !p.Snapshot().Equal(state) || len(p.Events()) != 0 { t.Fatal("generation overflow changed authority") } } func TestAuthenticationRestoreValidatesAuthorityWithoutReplayingFacts(t *testing.T) { state := registeredParty(t).Snapshot() mutations := map[string]func(*party.PartyAuthenticationSnapshot){ "missing party": func(s *party.PartyAuthenticationSnapshot) { s.PartyID = "" }, "provider party": func(s *party.PartyAuthenticationSnapshot) { s.PartyID = "provider-1" }, "missing issuer": func(s *party.PartyAuthenticationSnapshot) { s.Issuer = "" }, "missing subject": func(s *party.PartyAuthenticationSnapshot) { s.Subject = "" }, "invalid text": func(s *party.PartyAuthenticationSnapshot) { s.Subject = string([]byte{0xff}) }, "unknown access": func(s *party.PartyAuthenticationSnapshot) { s.AccessState = "pending" }, "unknown identity": func(s *party.PartyAuthenticationSnapshot) { s.IdentityState = "purged" }, "zero generation": func(s *party.PartyAuthenticationSnapshot) { s.AuthVersion = 0 }, } for name, mutate := range mutations { t.Run(name, func(t *testing.T) { bad := state mutate(&bad) p, err := party.RestorePartyAuthentication(bad) expectCorruption(t, err) if p != nil { t.Fatal("corrupt authority was restored") } }) } p, err := party.RestorePartyAuthentication(state) if err != nil || !p.Snapshot().Equal(state) || len(p.Events()) != 0 { t.Fatalf("restore changed state or emitted facts: %v", err) } var uninitialised party.PartyAuthentication expectCorruption(t, uninitialised.SetAccessState(party.AccessEnabled, party.ReasonSecurity)) if uninitialised.AuthenticationEligible() { t.Fatal("uninitialised aggregate is eligible") } } func TestAuthenticationSnapshotsAndEventsHaveIndependentOwnership(t *testing.T) { p := registeredParty(t) snapshot := p.Snapshot() snapshot.PartyID, snapshot.Subject = bindingID, "replacement" if p.Snapshot().PartyID != partyID || p.Snapshot().Subject != "subject" { t.Fatal("snapshot mutation changed aggregate state") } events := p.Events() events[0] = party.PartyRegistered{PartyID: bindingID, AuthVersion: 9} if p.Events()[0].(party.PartyRegistered).PartyID != partyID { t.Fatal("event slice mutation changed pending facts") } events = p.DrainEvents() if len(events) != 1 || len(p.Events()) != 0 || len(p.DrainEvents()) != 0 { t.Fatal("draining did not transfer exactly the pending facts") } events[0] = nil if err := p.SetAccessState(party.AccessDisabled, party.ReasonSecurity); err != nil { t.Fatal(err) } if len(p.Events()) != 1 || p.Events()[0] == nil { t.Fatal("drained events retained mutable ownership") } }