package accountserviceimp import ( "context" "git.sendico.io/tech/sxchnge/product/auth/config" customermodel "git.sendico.io/tech/sxchnge/product/customers/model" sessionmodel "git.sendico.io/tech/sxchnge/product/sessions/model" verificationmodel "git.sendico.io/tech/sxchnge/product/verification/model" "git.sendico.io/tech/sxchnge/product/verification/store" "git.sendico.io/tech/sxchnge/shared/database" edgemongo "git.sendico.io/tech/sxchnge/shared/database/dependencies" "git.sendico.io/tech/sxchnge/shared/errors" sharedmodel "git.sendico.io/tech/sxchnge/shared/model" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go/modules/mongodb" "go.mongodb.org/mongo-driver/v2/bson" "go.uber.org/zap" "net/url" "testing" "time" ) func customer(login string) *customermodel.Account { return (&customermodel.AccountData{LoginData: customermodel.LoginData{UserDataBase: customermodel.UserDataBase{Login: login}, Password: "CustomerPassword1!"}, Describable: sharedmodel.Describable{Name: "Customer"}}).ToAccount() } // Uses the actual copied repositories against an empty SXCHNGE database. // There is deliberately no seed, organization, role, policy or Sendico connection. func TestCustomerLifecycleWithoutTenancy(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() container, err := mongodb.Run(ctx, "mongo:latest", mongodb.WithReplicaSet("rs")) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, container.Terminate(context.Background())) }) uri, err := container.ConnectionString(ctx) require.NoError(t, err) endpoint, err := url.Parse(uri) require.NoError(t, err) query := endpoint.Query() query.Set("directConnection", "true") endpoint.RawQuery = query.Encode() uri = endpoint.String() factory, err := db.NewConnection(zap.NewNop(), &db.Config{Driver: db.Mongo, Settings: sharedmodel.SettingsT{"uri": uri, "database": "sxchnge_customers"}}) require.NoError(t, err) t.Cleanup(factory.CloseConnection) // Custom collections prove lifecycle and login storage select the same data. config := &middleware.AuthStorageConfig{AccountsCollection: "customers", VerificationTokensCollection: "customer_verification", RefreshTokensCollection: "customer_sessions"} svc, err := NewAccountService(zap.NewNop(), factory, &middleware.PasswordConfig{Check: middleware.PasswordChecks{MinLength: 12}}, config) require.NoError(t, err) first, second := customer("first@example.test"), customer("second@example.test") firstToken, err := svc.CreateAccount(ctx, first) require.NoError(t, err) require.NotEmpty(t, firstToken) require.Equal(t, customermodel.AccountPendingVerification, first.Status) require.True(t, first.MatchPassword("CustomerPassword1!")) _, err = svc.CreateAccount(ctx, second) require.NoError(t, err) require.NotEqual(t, first.ID, second.ID) // Activation tokens cannot be used as another customer's token. _, err = svc.vdb.Consume(ctx, second.ID, verificationmodel.PurposeAccountActivation, "", firstToken) require.Error(t, err) _, err = svc.vdb.Consume(ctx, first.ID, verificationmodel.PurposeAccountActivation, "", firstToken) require.NoError(t, err) _, err = svc.CreateAccount(ctx, customer("first@example.test")) require.ErrorIs(t, err, merrors.ErrDataConflict) require.NoError(t, svc.ValidateAccount(customer("validate@example.test"))) require.ErrorIs(t, svc.ValidateAccount(nil), merrors.ErrInvalidArg) _, err = svc.CreateAccount(ctx, nil) require.ErrorIs(t, err, merrors.ErrInvalidArg) require.ErrorIs(t, svc.DeleteAccount(ctx, bson.NilObjectID), merrors.ErrInvalidArg) for _, c := range []*customermodel.Account{first, second} { require.NoError(t, svc.sessions.Create(ctx, &sessionmodel.RefreshToken{AccountRef: &c.ID, ExpiresAt: time.Now().Add(time.Hour), ClientRefreshToken: sessionmodel.ClientRefreshToken{SessionIdentifier: sessionmodel.SessionIdentifier{ClientID: "web", DeviceID: "device", SessionVersion: "v1"}, RefreshToken: c.ID.Hex()}})) } require.NoError(t, svc.DeleteAccount(ctx, first.ID)) var stored customermodel.Account require.ErrorIs(t, svc.accountDB.Get(ctx, first.ID, &stored), merrors.ErrNoData) require.NoError(t, svc.accountDB.Get(ctx, second.ID, &stored)) session := &sessionmodel.SessionIdentifier{ClientID: "web", DeviceID: "device", SessionVersion: "v1"} _, err = svc.sessions.GetSession(ctx, first.ID, session) require.Error(t, err) _, err = svc.sessions.GetSession(ctx, second.ID, session) require.NoError(t, err) // The final local customer can be deleted; there is no sole-member restriction. require.NoError(t, svc.DeleteAccount(ctx, second.ID)) deps, err := edgemongo.ResolveDependencies(factory) require.NoError(t, err) collections, err := deps.Database.ListCollectionNames(ctx, bson.D{}) require.NoError(t, err) for _, forbidden := range []string{"organizations", "roles", "policies", "policy_assignments", "invitations", "accounts"} { require.NotContains(t, collections, forbidden) } // A token issuance failure must roll back the preceding customer insert. realVerification := svc.vdb failure := merrors.Internal("injected verification failure") svc.vdb = &failingVerification{DB: realVerification, failure: failure} candidate := customer("rollback@example.test") _, err = svc.CreateAccount(ctx, candidate) require.ErrorIs(t, err, failure) require.True(t, candidate.ID.IsZero()) _, err = svc.accountDB.GetByEmail(ctx, candidate.Login) require.ErrorIs(t, err, merrors.ErrNoData) svc.vdb = realVerification // If an outer workflow fails after token issuance, neither write may commit. outerFailure := merrors.Internal("injected outer failure") _, err = factory.TransactionFactory().CreateTransaction().Execute(ctx, func(tx context.Context) (any, error) { _, e := svc.CreateAccount(tx, customer("outer@example.test")) if e != nil { return nil, e } return nil, outerFailure }) require.ErrorIs(t, err, outerFailure) _, err = svc.accountDB.GetByEmail(ctx, "outer@example.test") require.ErrorIs(t, err, merrors.ErrNoData) count, err := deps.Database.Collection(config.VerificationTokensCollection).CountDocuments(ctx, bson.D{}) require.NoError(t, err) require.EqualValues(t, 2, count) } type failingVerification struct { verification.DB failure error } func (f *failingVerification) Create(context.Context, *verification.Request) (string, error) { return "", f.failure }