Loading...
Loading...
Generate Go integration tests with real database/infrastructure via itestkit containers. Use when testing use cases against real databases, verifying end-to-end flows, or adding integration test coverage.
npx skill4agent add cristiano-pacheco/ai-tools go-integration-teststest/integration/internal/internal/modules/identity/usecase/user/user_register_usecase.gotest/integration/modules/identity/usecase/user/user_register_usecase_test.goTestExecute_ValidInput_ReturnsUserTestExecute_DuplicateEmail_ReturnsErrorsuite.SuitesutkitdbSetupSuiteTearDownSuiteSetupTest//go:build integration_testsuites.Equal(...)s.NotZero(...)s.Require()s.Require().NoError(err)s.Require().ErrorIs(...).AssertExpectations(s.T())//go:build integration
package user_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
"github.com/cristiano-pacheco/bricks/pkg/itestkit"
"github.com/cristiano-pacheco/bricks/pkg/validator"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/errs"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/model"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/repository"
"github.com/cristiano-pacheco/pingo/internal/modules/identity/usecase/user"
identity_validator "github.com/cristiano-pacheco/pingo/internal/modules/identity/validator"
"github.com/cristiano-pacheco/pingo/internal/shared/config"
"github.com/cristiano-pacheco/pingo/internal/shared/database"
"github.com/cristiano-pacheco/pingo/test/mocks"
)
// emailRecord captures emails sent during tests for assertion purposes.
type emailRecord struct {
to string
subject string
body string
}
func TestMain(m *testing.M) {
itestkit.TestMain(m)
}
type UserRegisterUseCaseTestSuite struct {
suite.Suite
kit *itestkit.ITestKit
db *database.PingoDB
sut *user.UserRegisterUseCase
emailSender *mocks.MockEmailSender
tokenGenerator *mocks.MockTokenGenerator
cfg config.Config
sentEmails []emailRecord
}
func TestUserRegisterUseCaseSuite(t *testing.T) {
suite.Run(t, new(UserRegisterUseCaseTestSuite))
}
func (s *UserRegisterUseCaseTestSuite) SetupSuite() {
s.kit = itestkit.New(itestkit.Config{
PostgresImage: "postgres:16-alpine",
RedisImage: "redis:7-alpine",
MigrationsPath: "file://migrations",
Database: "pingo_test",
User: "pingo_test",
Password: "pingo_test",
})
err := s.kit.StartPostgres()
s.Require().NoError(err)
err = s.kit.RunMigrations()
s.Require().NoError(err)
s.db = &database.PingoDB{DB: s.kit.DB()}
}
func (s *UserRegisterUseCaseTestSuite) TearDownSuite() {
if s.kit != nil {
s.kit.StopPostgres()
}
}
// SetupTest runs before every test. Create fresh mock objects here and reset
// any captured side-effect state. Then call createTestUseCase to wire everything up.
func (s *UserRegisterUseCaseTestSuite) SetupTest() {
s.kit.TruncateTables(s.T())
s.sentEmails = nil
s.emailSender = mocks.NewMockEmailSender(s.T())
s.tokenGenerator = mocks.NewMockTokenGenerator(s.T())
s.cfg = s.createTestConfig(true)
s.sut = s.createTestUseCase()
}
// createTestConfig accepts feature flags so individual tests can reconfigure the SUT.
func (s *UserRegisterUseCaseTestSuite) createTestConfig(registrationEnabled bool) config.Config {
return config.Config{
App: config.AppConfig{
BaseURL: "http://test.example.com",
Identity: config.IdentityConfig{
Registration: config.RegistrationConfig{
Enabled: registrationEnabled,
},
},
},
}
}
// createTestUseCase wires all dependencies and sets up mock expectations.
// Infrastructure mocks (metrics, logger) use .Maybe() so they satisfy calls
// without requiring them. Domain mocks (emailSender, tokenGenerator) use .Run()
// callbacks to capture side effects for later assertion.
func (s *UserRegisterUseCaseTestSuite) createTestUseCase() *user.UserRegisterUseCase {
log := new(mocks.MockLogger)
v, err := validator.New()
s.Require().NoError(err)
pwValidator := identity_validator.NewPasswordValidator()
pwHasher := service.NewPasswordHasherService(s.cfg)
useCaseMetrics := new(mocks.MockUseCaseMetrics)
useCaseMetrics.On("ObserveDuration", mock.Anything, mock.Anything).Return().Maybe()
useCaseMetrics.On("IncrementCount", mock.Anything).Return().Maybe()
useCaseMetrics.On("IncSuccess", mock.Anything).Return().Maybe()
useCaseMetrics.On("IncError", mock.Anything).Return().Maybe()
s.tokenGenerator.On("GenerateToken").Return("test-token-12345", nil).Maybe()
s.tokenGenerator.On("HashToken", mock.Anything).Return(func(token string) []byte {
hash := make([]byte, len(token))
copy(hash, token)
return hash
}).Maybe()
// Capture emails for later assertion in test methods.
s.emailSender.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
s.sentEmails = append(s.sentEmails, emailRecord{
to: args.String(1),
subject: args.String(2),
body: args.String(3),
})
}).Return(nil).Maybe()
userRepo := repository.NewUserRepository(s.db)
tokenRepo := repository.NewIdentityTokenRepository(s.db)
return user.NewUserRegisterUseCase(
userRepo,
tokenRepo,
pwHasher,
pwValidator,
s.tokenGenerator,
s.emailSender,
v,
s.cfg,
log,
useCaseMetrics,
)
}
// findSentEmail is a suite helper for looking up captured emails by recipient.
func (s *UserRegisterUseCaseTestSuite) findSentEmail(email string) *emailRecord {
for i := range s.sentEmails {
if s.sentEmails[i].to == email {
return &s.sentEmails[i]
}
}
return nil
}
func (s *UserRegisterUseCaseTestSuite) TestExecute_ValidInput_ReturnsUser() {
// Arrange
ctx := context.Background()
input := user.UserRegisterInput{
Email: "test@example.com",
Password: "Password123!",
FirstName: "John",
LastName: "Doe",
}
// Act
output, err := s.sut.Execute(ctx, input)
// Assert — output fields
s.Require().NoError(err)
s.NotZero(output.ID)
s.Equal(input.Email, output.Email)
s.Equal(input.FirstName, output.FirstName)
s.Equal(input.LastName, output.LastName)
s.Equal("pending_verification", output.Status)
// Assert — DB state
var savedUser model.UserModel
err = s.db.DB.Where("id = ?", output.ID).First(&savedUser).Error
s.Require().NoError(err)
s.Equal(input.Email, savedUser.Email)
s.Equal(input.FirstName, savedUser.FirstName)
s.Equal(input.LastName, savedUser.LastName)
s.NotEmpty(savedUser.PasswordHash)
s.NotZero(savedUser.CreatedAt)
s.NotZero(savedUser.UpdatedAt)
// Assert — token persisted
var savedToken model.IdentityTokenModel
err = s.db.DB.Where("user_id = ? AND token_type = ?", output.ID, "email_verification").
First(&savedToken).Error
s.Require().NoError(err)
s.True(savedToken.ExpiresAt.After(time.Now()))
// Assert — email side effect
email := s.findSentEmail(input.Email)
s.Require().NotNil(email)
s.Equal("Verify your email", email.subject)
s.Contains(email.body, "test-token-12345")
}
func (s *UserRegisterUseCaseTestSuite) TestExecute_DuplicateEmail_ReturnsError() {
// Arrange
ctx := context.Background()
input := user.UserRegisterInput{
Email: "test@example.com",
Password: "Password123!",
FirstName: "John",
LastName: "Doe",
}
// Act - First registration succeeds
output1, err := s.sut.Execute(ctx, input)
s.Require().NoError(err)
s.NotZero(output1.ID)
s.sentEmails = nil
// Act - Second registration with same email
_, err = s.sut.Execute(ctx, user.UserRegisterInput{
Email: "test@example.com",
Password: "Different456!",
FirstName: "Jane",
LastName: "Smith",
})
// Assert
s.Require().Error(err)
s.ErrorIs(err, errs.ErrDuplicateEmail)
var count int64
s.db.DB.Model(&model.UserModel{}).Where("email = ?", input.Email).Count(&count)
s.Equal(int64(1), count)
s.Nil(s.findSentEmail(input.Email))
}
func (s *UserRegisterUseCaseTestSuite) TestExecute_RegistrationDisabled_ReturnsError() {
// Arrange - recreate sut with registration disabled
s.cfg = s.createTestConfig(false)
s.sut = s.createTestUseCase()
ctx := context.Background()
input := user.UserRegisterInput{
Email: "test@example.com",
Password: "Password123!",
FirstName: "John",
LastName: "Doe",
}
// Act
_, err := s.sut.Execute(ctx, input)
// Assert
s.Require().Error(err)
s.ErrorIs(err, errs.ErrRegistrationDisabled)
var count int64
s.db.DB.Model(&model.UserModel{}).Where("email = ?", input.Email).Count(&count)
s.Equal(int64(0), count)
s.Nil(s.findSentEmail(input.Email))
}SetupTest.On(...)createTestUseCase.Maybe().Run()mock.Anything// 1. Define a record type at the top of the file
type emailRecord struct {
to string
subject string
body string
}
// 2. Add a slice field to the suite and a helper method
type MyTestSuite struct {
suite.Suite
sentEmails []emailRecord
// ...
}
func (s *MyTestSuite) findSentEmail(to string) *emailRecord {
for i := range s.sentEmails {
if s.sentEmails[i].to == to {
return &s.sentEmails[i]
}
}
return nil
}
// 3. Reset and capture in SetupTest / createTestUseCase
func (s *MyTestSuite) SetupTest() {
s.sentEmails = nil
// ...
}
// In createTestUseCase:
s.emailSender.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
s.sentEmails = append(s.sentEmails, emailRecord{
to: args.String(1),
subject: args.String(2),
body: args.String(3),
})
}).Return(nil).Maybe()s.Run()func (s *UserRegisterUseCaseTestSuite) TestExecute_InvalidPassword_ReturnsError() {
// Arrange
ctx := context.Background()
testCases := []struct {
name string
email string
password string
}{
{"too short", "tooshort@example.com", "Short1!"},
{"no uppercase", "nouppercase@example.com", "lowercase123!"},
{"no digit", "nodigit@example.com", "NoDigitsHere!"},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
input := user.UserRegisterInput{
Email: tc.email,
Password: tc.password,
FirstName: "John",
LastName: "Doe",
}
// Act
_, err := s.sut.Execute(ctx, input)
// Assert
s.Require().Error(err)
s.ErrorIs(err, errs.ErrPasswordPolicyViolation)
var count int64
s.db.DB.Model(&model.UserModel{}).Where("email = ?", tc.email).Count(&count)
s.Equal(int64(0), count)
})
}
}// Arrange
// Act
// Assert// Act - First registration succeeds
// Assert - First registration succeeds
// Act - Second registration with same email
// Assert - Second registration failstest/integration/| Source File | Integration Test File |
|---|---|
| |
| |
# Run all integration tests
make test-integration