132 lines
6.3 KiB
Go
132 lines
6.3 KiB
Go
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:8", 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
|
|
}
|