forked from baron/baron-sso
테스트 코드 추가
This commit is contained in:
139
backend/internal/handler/auth_handler_signup_test.go
Normal file
139
backend/internal/handler/auth_handler_signup_test.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"baron-sso-backend/internal/domain"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Local Mocks for Signup Test ---
|
||||||
|
|
||||||
|
type MockRedisForSignup struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockRedisForSignup) Set(key string, value string, ttl time.Duration) error {
|
||||||
|
return m.Called(key, value, ttl).Error(0)
|
||||||
|
}
|
||||||
|
func (m *MockRedisForSignup) Get(key string) (string, error) {
|
||||||
|
args := m.Called(key)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
func (m *MockRedisForSignup) Delete(key string) error {
|
||||||
|
return m.Called(key).Error(0)
|
||||||
|
}
|
||||||
|
func (m *MockRedisForSignup) StoreVerificationCode(phone, code string) error { return nil }
|
||||||
|
func (m *MockRedisForSignup) GetVerificationCode(phone string) (string, error) { return "", nil }
|
||||||
|
func (m *MockRedisForSignup) DeleteVerificationCode(phone string) error { return nil }
|
||||||
|
func (m *MockRedisForSignup) Ping(ctx context.Context) error { return nil }
|
||||||
|
|
||||||
|
type MockIdpForSignup struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockIdpForSignup) Name() string { return "mock-idp" }
|
||||||
|
func (m *MockIdpForSignup) GetMetadata() (*domain.IDPMetadata, error) {
|
||||||
|
return &domain.IDPMetadata{SupportedFields: []string{"email", "name", "phoneNumber", "grade", "department"}}, nil
|
||||||
|
}
|
||||||
|
func (m *MockIdpForSignup) CreateUser(user *domain.BrokerUser, password string) (string, error) {
|
||||||
|
args := m.Called(user, password)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
func (m *MockIdpForSignup) SignIn(loginID, password string) (*domain.AuthInfo, error) { return nil, nil }
|
||||||
|
func (m *MockIdpForSignup) UserExists(loginID string) (bool, error) { return false, nil }
|
||||||
|
func (m *MockIdpForSignup) IssueSession(loginID string) (*domain.AuthInfo, error) { return nil, nil }
|
||||||
|
func (m *MockIdpForSignup) InitiateLinkLogin(loginID, returnTo string) (*domain.LinkLoginInit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *MockIdpForSignup) VerifyLoginCode(loginID, flowID, code string) (*domain.AuthInfo, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *MockIdpForSignup) GetPasswordPolicy() (*domain.PasswordPolicy, error) {
|
||||||
|
return &domain.PasswordPolicy{MinLength: 12}, nil
|
||||||
|
}
|
||||||
|
func (m *MockIdpForSignup) InitiatePasswordReset(loginID, redirectUrl string) error { return nil }
|
||||||
|
func (m *MockIdpForSignup) VerifyPasswordResetToken(token string) (*domain.AuthInfo, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *MockIdpForSignup) UpdateUserPassword(loginID, newPassword string, r *http.Request) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignup_CompanyCodeValidation(t *testing.T) {
|
||||||
|
app := fiber.New()
|
||||||
|
mockTenantSvc := new(MockTenantService)
|
||||||
|
mockRedis := new(MockRedisForSignup)
|
||||||
|
mockIdp := new(MockIdpForSignup)
|
||||||
|
|
||||||
|
h := &AuthHandler{
|
||||||
|
TenantService: mockTenantSvc,
|
||||||
|
RedisService: mockRedis,
|
||||||
|
IdpProvider: mockIdp,
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Post("/signup", h.Signup)
|
||||||
|
|
||||||
|
// Prepare mock state (already verified email/phone)
|
||||||
|
verifiedState, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"verified": true,
|
||||||
|
"expires_at": time.Now().Add(time.Hour).Unix(),
|
||||||
|
})
|
||||||
|
mockRedis.On("Get", mock.Anything).Return(string(verifiedState), nil)
|
||||||
|
|
||||||
|
t.Run("Invalid Company Code", func(t *testing.T) {
|
||||||
|
reqBody := domain.SignupRequest{
|
||||||
|
Email: "user@gmail.com", // General domain
|
||||||
|
Password: "StrongPass123!",
|
||||||
|
Name: "Test User",
|
||||||
|
Phone: "010-1234-5678",
|
||||||
|
TermsAccepted: true,
|
||||||
|
CompanyCode: "non-existent-code",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(reqBody)
|
||||||
|
|
||||||
|
mockTenantSvc.On("GetTenantByDomain", mock.Anything, "gmail.com").Return(nil, nil)
|
||||||
|
mockTenantSvc.On("GetTenantBySlug", mock.Anything, "non-existent-code").Return(nil, nil)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/signup", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, _ := app.Test(req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
var res map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&res)
|
||||||
|
assert.Equal(t, "Invalid company code.", res["error"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Active Company Code", func(t *testing.T) {
|
||||||
|
reqBody := domain.SignupRequest{
|
||||||
|
Email: "user@gmail.com",
|
||||||
|
Password: "StrongPass123!",
|
||||||
|
Name: "Test User",
|
||||||
|
Phone: "010-1234-5678",
|
||||||
|
TermsAccepted: true,
|
||||||
|
CompanyCode: "valid-slug",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(reqBody)
|
||||||
|
|
||||||
|
validTenant := &domain.Tenant{ID: "t1", Slug: "valid-slug", Status: domain.TenantStatusActive}
|
||||||
|
mockTenantSvc.On("GetTenantByDomain", mock.Anything, "gmail.com").Return(nil, nil)
|
||||||
|
mockTenantSvc.On("GetTenantBySlug", mock.Anything, "valid-slug").Return(validTenant, nil)
|
||||||
|
mockIdp.On("CreateUser", mock.Anything, mock.Anything).Return("user-id", nil)
|
||||||
|
mockRedis.On("Delete", mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/signup", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, _ := app.Test(req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
178
backend/internal/service/tenant_service_test.go
Normal file
178
backend/internal/service/tenant_service_test.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"baron-sso-backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Local Mocks to avoid collisions ---
|
||||||
|
|
||||||
|
type MockTenantRepoForSvc struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockTenantRepoForSvc) Create(ctx context.Context, tenant *domain.Tenant) error {
|
||||||
|
return m.Called(ctx, tenant).Error(0)
|
||||||
|
}
|
||||||
|
func (m *MockTenantRepoForSvc) Update(ctx context.Context, tenant *domain.Tenant) error {
|
||||||
|
return m.Called(ctx, tenant).Error(0)
|
||||||
|
}
|
||||||
|
func (m *MockTenantRepoForSvc) FindByID(ctx context.Context, id string) (*domain.Tenant, error) {
|
||||||
|
args := m.Called(ctx, id)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||||
|
}
|
||||||
|
func (m *MockTenantRepoForSvc) FindBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
|
||||||
|
args := m.Called(ctx, slug)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||||
|
}
|
||||||
|
func (m *MockTenantRepoForSvc) FindByName(ctx context.Context, name string) (*domain.Tenant, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *MockTenantRepoForSvc) FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
|
||||||
|
args := m.Called(ctx, domainName)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||||
|
}
|
||||||
|
func (m *MockTenantRepoForSvc) FindByIDs(ctx context.Context, ids []string) ([]domain.Tenant, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *MockTenantRepoForSvc) AddDomain(ctx context.Context, tenantID string, domainName string, verified bool) error {
|
||||||
|
return m.Called(ctx, tenantID, domainName, verified).Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockKetoSvcForTenant struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockKetoSvcForTenant) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
||||||
|
return m.Called(ctx, namespace, object, relation, subject).Error(0)
|
||||||
|
}
|
||||||
|
func (m *MockKetoSvcForTenant) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
||||||
|
return m.Called(ctx, namespace, object, relation, subject).Error(0)
|
||||||
|
}
|
||||||
|
func (m *MockKetoSvcForTenant) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]RelationTuple, error) {
|
||||||
|
args := m.Called(ctx, namespace, object, relation, subject)
|
||||||
|
return args.Get(0).([]RelationTuple), args.Error(1)
|
||||||
|
}
|
||||||
|
func (m *MockKetoSvcForTenant) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
|
||||||
|
args := m.Called(ctx, namespace, relation, subject)
|
||||||
|
return args.Get(0).([]string), args.Error(1)
|
||||||
|
}
|
||||||
|
func (m *MockKetoSvcForTenant) CheckPermission(ctx context.Context, namespace, object, relation, subject string) (bool, error) {
|
||||||
|
args := m.Called(ctx, namespace, object, relation, subject)
|
||||||
|
return args.Bool(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockUserRepoForTenant struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUserRepoForTenant) Create(ctx context.Context, user *domain.User) error { return nil }
|
||||||
|
func (m *MockUserRepoForTenant) Update(ctx context.Context, user *domain.User) error { return nil }
|
||||||
|
func (m *MockUserRepoForTenant) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
|
args := m.Called(email)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*domain.User), args.Error(1)
|
||||||
|
}
|
||||||
|
func (m *MockUserRepoForTenant) FindByID(ctx context.Context, id string) (*domain.User, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *MockUserRepoForTenant) FindByIDs(ctx context.Context, ids []string) ([]domain.User, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *MockUserRepoForTenant) ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *MockUserRepoForTenant) List(ctx context.Context, offset, limit int, search string) ([]domain.User, int64, error) {
|
||||||
|
return nil, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
func TestTenantService_RegisterTenant_AutoVerify(t *testing.T) {
|
||||||
|
mockRepo := new(MockTenantRepoForSvc)
|
||||||
|
svc := NewTenantService(mockRepo, nil)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
name := "New Tenant"
|
||||||
|
slug := "new-tenant"
|
||||||
|
domains := []string{"example.com"}
|
||||||
|
|
||||||
|
// Use .Once() to ensure correct return values for sequential calls to FindBySlug
|
||||||
|
mockRepo.On("FindBySlug", ctx, slug).Return(nil, nil).Once()
|
||||||
|
mockRepo.On("Create", ctx, mock.Anything).Return(nil)
|
||||||
|
mockRepo.On("AddDomain", ctx, mock.Anything, "example.com", true).Return(nil)
|
||||||
|
mockRepo.On("FindBySlug", ctx, slug).Return(&domain.Tenant{ID: "t1", Slug: slug}, nil).Once()
|
||||||
|
|
||||||
|
tenant, err := svc.RegisterTenant(ctx, name, slug, "", domains)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, tenant)
|
||||||
|
assert.Equal(t, "t1", tenant.ID)
|
||||||
|
mockRepo.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTenantService_RequestRegistration_NoVerify(t *testing.T) {
|
||||||
|
mockRepo := new(MockTenantRepoForSvc)
|
||||||
|
svc := NewTenantService(mockRepo, nil)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
name := "Public Tenant"
|
||||||
|
slug := "public-tenant"
|
||||||
|
domainName := "public.com"
|
||||||
|
adminEmail := "admin@public.com"
|
||||||
|
|
||||||
|
mockRepo.On("Create", ctx, mock.MatchedBy(func(tenant *domain.Tenant) bool {
|
||||||
|
return tenant.Status == domain.TenantStatusPending
|
||||||
|
})).Return(nil)
|
||||||
|
mockRepo.On("AddDomain", ctx, mock.Anything, domainName, false).Return(nil)
|
||||||
|
|
||||||
|
tenant, err := svc.RequestRegistration(ctx, name, slug, "", domainName, adminEmail)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, tenant)
|
||||||
|
mockRepo.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTenantService_ApproveTenant_SyncAdmin(t *testing.T) {
|
||||||
|
mockRepo := new(MockTenantRepoForSvc)
|
||||||
|
mockUserRepo := new(MockUserRepoForTenant)
|
||||||
|
mockKeto := new(MockKetoSvcForTenant)
|
||||||
|
|
||||||
|
svc := NewTenantService(mockRepo, mockUserRepo)
|
||||||
|
svc.SetKetoService(mockKeto)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
tenantID := "t1"
|
||||||
|
adminEmail := "admin@tenant.com"
|
||||||
|
userID := "user-uuid"
|
||||||
|
|
||||||
|
tenant := &domain.Tenant{
|
||||||
|
ID: tenantID,
|
||||||
|
Slug: "tenant-slug",
|
||||||
|
Config: domain.JSONMap{"adminEmail": adminEmail},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockRepo.On("FindByID", ctx, tenantID).Return(tenant, nil)
|
||||||
|
mockRepo.On("Update", ctx, mock.Anything).Return(nil)
|
||||||
|
mockUserRepo.On("FindByEmail", adminEmail).Return(&domain.User{ID: userID, Email: adminEmail}, nil)
|
||||||
|
mockKeto.On("CreateRelation", ctx, "Tenant", tenantID, "admin", "User:"+userID).Return(nil)
|
||||||
|
|
||||||
|
err := svc.ApproveTenant(ctx, tenantID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
mockRepo.AssertExpectations(t)
|
||||||
|
mockUserRepo.AssertExpectations(t)
|
||||||
|
mockKeto.AssertExpectations(t)
|
||||||
|
}
|
||||||
56
backend/internal/utils/slug_test.go
Normal file
56
backend/internal/utils/slug_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateSlug_ReservedKeywords(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
slug string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{"my-tenant", true},
|
||||||
|
{"admin", false},
|
||||||
|
{"api", false},
|
||||||
|
{"static", false},
|
||||||
|
{"security", false},
|
||||||
|
{"billing", false},
|
||||||
|
{"ns", false},
|
||||||
|
{"mx", false},
|
||||||
|
{"webmaster", false},
|
||||||
|
{"status", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.slug, func(t *testing.T) {
|
||||||
|
valid, msg := ValidateSlug(tt.slug)
|
||||||
|
assert.Equal(t, tt.valid, valid, "Slug: "+tt.slug+" - "+msg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSlug_Format(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
slug string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{"abc", true},
|
||||||
|
{"a-b-c", true},
|
||||||
|
{"123", true},
|
||||||
|
{"ab", false}, // Too short
|
||||||
|
{"-abc", false}, // Starts with hyphen
|
||||||
|
{"abc-", false}, // Ends with hyphen
|
||||||
|
{"Abc", true}, // Case insensitive check (converted to lower)
|
||||||
|
{"invalid_slug", false}, // Contains underscore
|
||||||
|
{"too-long-slug-name-that-exceeds-thirty-two-chars", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.slug, func(t *testing.T) {
|
||||||
|
valid, _ := ValidateSlug(tt.slug)
|
||||||
|
assert.Equal(t, tt.valid, valid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user