1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/handler/auth_handler_async_test.go
2026-03-05 17:20:46 +09:00

300 lines
9.2 KiB
Go

package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/service"
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// --- Async Test Mocks ---
type AsyncMockIdpProvider struct {
mock.Mock
}
func (m *AsyncMockIdpProvider) Name() string { return "mock-idp" }
func (m *AsyncMockIdpProvider) GetMetadata() (*domain.IDPMetadata, error) {
return &domain.IDPMetadata{}, nil
}
func (m *AsyncMockIdpProvider) UserExists(loginID string) (bool, error) {
args := m.Called(loginID)
return args.Bool(0), args.Error(1)
}
func (m *AsyncMockIdpProvider) CreateUser(user *domain.BrokerUser, password string) (string, error) {
args := m.Called(user, password)
return args.String(0), args.Error(1)
}
func (m *AsyncMockIdpProvider) SignIn(loginID, password string) (*domain.AuthInfo, error) {
return nil, nil
}
func (m *AsyncMockIdpProvider) IssueSession(loginID string) (*domain.AuthInfo, error) {
return nil, nil
}
func (m *AsyncMockIdpProvider) InitiateLinkLogin(loginID, returnTo string) (*domain.LinkLoginInit, error) {
return nil, nil
}
func (m *AsyncMockIdpProvider) VerifyLoginCode(loginID, flowID, code string) (*domain.AuthInfo, error) {
return nil, nil
}
func (m *AsyncMockIdpProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) {
return &domain.PasswordPolicy{MinLength: 12}, nil
}
func (m *AsyncMockIdpProvider) InitiatePasswordReset(loginID, redirectUrl string) error { return nil }
func (m *AsyncMockIdpProvider) VerifyPasswordResetToken(token string) (*domain.AuthInfo, error) {
return nil, nil
}
func (m *AsyncMockIdpProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error {
return nil
}
type AsyncMockUserRepo struct {
mock.Mock
createCalled chan bool
}
func (m *AsyncMockUserRepo) Create(ctx context.Context, user *domain.User) error {
// Simulate DB latency
time.Sleep(50 * time.Millisecond)
args := m.Called(ctx, user)
if m.createCalled != nil {
m.createCalled <- true
}
return args.Error(0)
}
func (m *AsyncMockUserRepo) Update(ctx context.Context, user *domain.User) error { return nil }
func (m *AsyncMockUserRepo) Delete(ctx context.Context, id string) error { return nil }
func (m *AsyncMockUserRepo) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
return nil, nil
}
func (m *AsyncMockUserRepo) FindByID(ctx context.Context, id string) (*domain.User, error) {
return nil, nil
}
func (m *AsyncMockUserRepo) FindByIDs(ctx context.Context, ids []string) ([]domain.User, error) {
return nil, nil
}
func (m *AsyncMockUserRepo) ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error) {
return nil, nil
}
func (m *AsyncMockUserRepo) List(ctx context.Context, offset, limit int, search string, companyCode string) ([]domain.User, int64, error) {
return nil, 0, nil
}
func (m *AsyncMockUserRepo) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
return 0, nil
}
func (m *AsyncMockUserRepo) CountByTenantIDs(ctx context.Context, tenantIDs []string) (map[string]int64, error) {
return nil, nil
}
func (m *AsyncMockUserRepo) CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error) {
return nil, nil
}
type AsyncMockRedisRepo struct {
mock.Mock
}
func (m *AsyncMockRedisRepo) Set(key string, value string, expiration time.Duration) error {
args := m.Called(key, value, expiration)
return args.Error(0)
}
func (m *AsyncMockRedisRepo) Get(key string) (string, error) {
args := m.Called(key)
return args.String(0), args.Error(1)
}
func (m *AsyncMockRedisRepo) Delete(key string) error {
args := m.Called(key)
return args.Error(0)
}
func (m *AsyncMockRedisRepo) StoreVerificationCode(phone, code string) error { return nil }
func (m *AsyncMockRedisRepo) GetVerificationCode(phone string) (string, error) { return "", nil }
func (m *AsyncMockRedisRepo) DeleteVerificationCode(phone string) error { return nil }
type AsyncMockTenantService struct {
mock.Mock
}
func (m *AsyncMockTenantService) RegisterTenant(ctx context.Context, name, slug, tenantType, description string, domains []string, parentID *string, creatorID string) (*domain.Tenant, error) {
return nil, nil
}
func (m *AsyncMockTenantService) RequestRegistration(ctx context.Context, name, slug, description string, domainName string, adminEmail string) (*domain.Tenant, error) {
return nil, nil
}
func (m *AsyncMockTenantService) GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error) {
args := m.Called(ctx, emailDomain)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*domain.Tenant), args.Error(1)
}
func (m *AsyncMockTenantService) GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
return nil, nil
}
func (m *AsyncMockTenantService) GetTenant(ctx context.Context, id string) (*domain.Tenant, error) {
return nil, nil
}
func (m *AsyncMockTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
return nil, 0, nil
}
func (m *AsyncMockTenantService) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
return nil, nil
}
func (m *AsyncMockTenantService) IsDomainAllowed(ctx context.Context, domainName string) (bool, error) {
return false, nil
}
func (m *AsyncMockTenantService) ApproveTenant(ctx context.Context, id string) error { return nil }
func (m *AsyncMockTenantService) SetKetoService(keto service.KetoService) {}
func (m *AsyncMockTenantService) AddTenantAdmin(ctx context.Context, tenantID, userID string) error {
return nil
}
func (m *AsyncMockTenantService) RemoveTenantAdmin(ctx context.Context, tenantID, userID string) error {
return nil
}
func (m *AsyncMockTenantService) ListTenantAdmins(ctx context.Context, tenantID string) ([]string, error) {
return nil, nil
}
type AsyncMockKetoService struct {
mock.Mock
}
func (m *AsyncMockKetoService) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error {
args := m.Called(ctx, namespace, object, relation, subject)
return args.Error(0)
}
func (m *AsyncMockKetoService) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error {
return nil
}
func (m *AsyncMockKetoService) CheckPermission(ctx context.Context, namespace, object, relation, subject string) (bool, error) {
return false, nil
}
func (m *AsyncMockKetoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
return nil, nil
}
func (m *AsyncMockKetoService) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]service.RelationTuple, error) {
return nil, nil
}
// --- Tests ---
func TestSignup_AsyncDB_Isolation(t *testing.T) {
mockIdp := new(AsyncMockIdpProvider)
mockUserRepo := new(AsyncMockUserRepo)
mockRedis := new(AsyncMockRedisRepo)
mockTenant := new(AsyncMockTenantService)
mockKeto := new(AsyncMockKetoService)
h := &AuthHandler{
IdpProvider: mockIdp,
UserRepo: mockUserRepo,
RedisService: mockRedis,
TenantService: mockTenant,
KetoService: mockKeto,
}
app := fiber.New()
app.Post("/signup", h.Signup)
t.Run("SoT_DB_Failure_Ignored_And_Async", func(t *testing.T) {
email := "test@example.com"
phone := "010-1234-5678"
emailKey := "signup:email:" + email
phoneKey := "signup:phone:" + "01012345678"
// Redis Mocks
mockRedis.On("Get", emailKey).Return(`{"verified": true, "expires_at": 9999999999}`, nil)
mockRedis.On("Get", phoneKey).Return(`{"verified": true, "expires_at": 9999999999}`, nil)
mockRedis.On("Delete", emailKey).Return(nil)
mockRedis.On("Delete", phoneKey).Return(nil)
// Tenant Mocks
mockTenant.On("GetTenantByDomain", mock.Anything, "example.com").Return(nil, errors.New("not found"))
// Kratos Mocks (Success)
mockIdp.On("CreateUser", mock.Anything, "Password123!").Return("new-user-uuid", nil)
// UserRepo Mocks (Async & Failure)
mockUserRepo.createCalled = make(chan bool, 1)
mockUserRepo.On("Create", mock.Anything, mock.MatchedBy(func(u *domain.User) bool {
return u.Email == email
})).Return(errors.New("db connection error"))
// Keto Mocks (Optional, since it's also async)
// We won't block on this either
body, _ := json.Marshal(domain.SignupRequest{
Email: email,
Password: "Password123!",
Name: "Test User",
Phone: phone,
TermsAccepted: true,
})
req := httptest.NewRequest("POST", "/signup", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
start := time.Now()
resp, err := app.Test(req, 5000)
elapsed := time.Since(start)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
assert.Equal(t, 200, resp.StatusCode)
// Ensure API responded faster than DB latency (50ms)
assert.Less(t, int64(elapsed), int64(60*time.Millisecond), "API should return before DB timeout")
// Wait for async execution
select {
case <-mockUserRepo.createCalled:
// Pass
case <-time.After(2 * time.Second):
t.Fatal("UserRepo.Create was not called asynchronously")
}
mockRedis.AssertExpectations(t)
mockIdp.AssertExpectations(t)
mockUserRepo.AssertExpectations(t)
})
}