forked from baron/baron-sso
280 lines
8.5 KiB
Go
280 lines
8.5 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) ([]domain.User, int64, error) {
|
|
return nil, 0, 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, description string, domains []string, parentID *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) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
|
return nil, 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)
|
|
})
|
|
}
|