forked from baron/baron-sso
린트 적용
This commit is contained in:
@@ -83,7 +83,7 @@ func (h *AuditHandler) ListLogs(c *fiber.Ctx) error {
|
|||||||
if profile.TenantID != nil {
|
if profile.TenantID != nil {
|
||||||
filterTenantID = *profile.TenantID
|
filterTenantID = *profile.TenantID
|
||||||
}
|
}
|
||||||
|
|
||||||
// If they requested a specific tenant, verify they can manage it
|
// If they requested a specific tenant, verify they can manage it
|
||||||
if requestedTenantID != "" && requestedTenantID != filterTenantID {
|
if requestedTenantID != "" && requestedTenantID != filterTenantID {
|
||||||
canManage := false
|
canManage := false
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ func (m *AsyncMockTenantService) ListTenants(ctx context.Context, limit, offset
|
|||||||
func (m *AsyncMockTenantService) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
func (m *AsyncMockTenantService) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *AsyncMockTenantService) IsDomainAllowed(ctx context.Context, domainName string) (bool, error) {
|
func (m *AsyncMockTenantService) IsDomainAllowed(ctx context.Context, domainName string) (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,24 +148,24 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
|
|||||||
tenantIDs = append(tenantIDs, t.ID)
|
tenantIDs = append(tenantIDs, t.ID)
|
||||||
slugs = append(slugs, t.Slug)
|
slugs = append(slugs, t.Slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
idCounts, _ := h.UserRepo.CountByTenantIDs(c.Context(), tenantIDs)
|
idCounts, _ := h.UserRepo.CountByTenantIDs(c.Context(), tenantIDs)
|
||||||
slugCounts, _ := h.UserRepo.CountByCompanyCodes(c.Context(), slugs)
|
slugCounts, _ := h.UserRepo.CountByCompanyCodes(c.Context(), slugs)
|
||||||
|
|
||||||
items := make([]tenantSummary, 0, len(tenants))
|
items := make([]tenantSummary, 0, len(tenants))
|
||||||
for _, t := range tenants {
|
for _, t := range tenants {
|
||||||
summary := mapTenantSummary(t)
|
summary := mapTenantSummary(t)
|
||||||
|
|
||||||
// Combine counts from both ID and Slug (Max to avoid double counting if some have one or the other)
|
// Combine counts from both ID and Slug (Max to avoid double counting if some have one or the other)
|
||||||
idCount := idCounts[t.ID]
|
idCount := idCounts[t.ID]
|
||||||
slugCount := slugCounts[strings.ToLower(t.Slug)]
|
slugCount := slugCounts[strings.ToLower(t.Slug)]
|
||||||
|
|
||||||
if idCount > slugCount {
|
if idCount > slugCount {
|
||||||
summary.MemberCount = idCount
|
summary.MemberCount = idCount
|
||||||
} else {
|
} else {
|
||||||
summary.MemberCount = slugCount
|
summary.MemberCount = slugCount
|
||||||
}
|
}
|
||||||
|
|
||||||
items = append(items, summary)
|
items = append(items, summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ func (h *TenantHandler) GetTenant(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
idCount := idCounts[tenant.ID]
|
idCount := idCounts[tenant.ID]
|
||||||
slugCount := slugCounts[strings.ToLower(tenant.Slug)]
|
slugCount := slugCounts[strings.ToLower(tenant.Slug)]
|
||||||
|
|
||||||
count := idCount
|
count := idCount
|
||||||
if slugCount > idCount {
|
if slugCount > idCount {
|
||||||
count = slugCount
|
count = slugCount
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
|
|||||||
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||||
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
||||||
compCode := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
|
compCode := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
|
||||||
|
|
||||||
// Check if the target user's companyCode is in requester's manageable tenants
|
// Check if the target user's companyCode is in requester's manageable tenants
|
||||||
allowed := false
|
allowed := false
|
||||||
for _, t := range requester.ManageableTenants {
|
for _, t := range requester.ManageableTenants {
|
||||||
@@ -539,7 +539,6 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
|||||||
PhoneNumber: normalizePhoneNumber(item.Phone),
|
PhoneNumber: normalizePhoneNumber(item.Phone),
|
||||||
Attributes: attributes,
|
Attributes: attributes,
|
||||||
}, password)
|
}, password)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: err.Error()})
|
results = append(results, bulkUserResult{Email: email, Success: false, Message: err.Error()})
|
||||||
continue
|
continue
|
||||||
@@ -561,7 +560,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
|||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if tItem.ID != "" {
|
if tItem.ID != "" {
|
||||||
localUser.TenantID = &tItem.ID
|
localUser.TenantID = &tItem.ID
|
||||||
}
|
}
|
||||||
@@ -575,11 +574,11 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
|||||||
if err := h.UserRepo.Update(c.Context(), localUser); err != nil {
|
if err := h.UserRepo.Update(c.Context(), localUser); err != nil {
|
||||||
slog.Error("Failed to sync bulk user to local DB", "email", email, "error", err)
|
slog.Error("Failed to sync bulk user to local DB", "email", email, "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.KetoOutboxRepo != nil {
|
if h.KetoOutboxRepo != nil {
|
||||||
// 1. Sync Role based relationship
|
// 1. Sync Role based relationship
|
||||||
h.syncKetoRole(c.Context(), localUser.ID, role, "", "", localUser.TenantID)
|
h.syncKetoRole(c.Context(), localUser.ID, role, "", "", localUser.TenantID)
|
||||||
|
|
||||||
// 2. Sync direct membership to the Tenant (for count)
|
// 2. Sync direct membership to the Tenant (for count)
|
||||||
if localUser.TenantID != nil && *localUser.TenantID != "" {
|
if localUser.TenantID != nil && *localUser.TenantID != "" {
|
||||||
_ = h.KetoOutboxRepo.Create(c.Context(), &domain.KetoOutbox{
|
_ = h.KetoOutboxRepo.Create(c.Context(), &domain.KetoOutbox{
|
||||||
@@ -803,7 +802,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
if req.CompanyCode != nil {
|
if req.CompanyCode != nil {
|
||||||
traits["companyCode"] = *req.CompanyCode
|
traits["companyCode"] = *req.CompanyCode
|
||||||
|
|
||||||
// Resolve and update tenant_id in traits if changed
|
// Resolve and update tenant_id in traits if changed
|
||||||
if tItem, exists := tenantCache[*req.CompanyCode]; exists {
|
if tItem, exists := tenantCache[*req.CompanyCode]; exists {
|
||||||
traits["tenant_id"] = tItem.ID
|
traits["tenant_id"] = tItem.ID
|
||||||
@@ -841,11 +840,19 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
|||||||
oldRole := extractTraitString(identity.Traits, "grade")
|
oldRole := extractTraitString(identity.Traits, "grade")
|
||||||
oldTenantID := extractTraitString(identity.Traits, "tenant_id")
|
oldTenantID := extractTraitString(identity.Traits, "tenant_id")
|
||||||
|
|
||||||
if req.Role != nil { localUser.Role = *req.Role }
|
if req.Role != nil {
|
||||||
if req.Status != nil { localUser.Status = *req.Status }
|
localUser.Role = *req.Role
|
||||||
if req.CompanyCode != nil { localUser.CompanyCode = *req.CompanyCode }
|
}
|
||||||
if req.Department != nil { localUser.Department = *req.Department }
|
if req.Status != nil {
|
||||||
|
localUser.Status = *req.Status
|
||||||
|
}
|
||||||
|
if req.CompanyCode != nil {
|
||||||
|
localUser.CompanyCode = *req.CompanyCode
|
||||||
|
}
|
||||||
|
if req.Department != nil {
|
||||||
|
localUser.Department = *req.Department
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve TenantID if changing companyCode
|
// Resolve TenantID if changing companyCode
|
||||||
if req.CompanyCode != nil && h.TenantService != nil {
|
if req.CompanyCode != nil && h.TenantService != nil {
|
||||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode); err == nil && tenant != nil {
|
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode); err == nil && tenant != nil {
|
||||||
@@ -857,7 +864,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// [Keto Sync]
|
// [Keto Sync]
|
||||||
if h.KetoOutboxRepo != nil {
|
if h.KetoOutboxRepo != nil {
|
||||||
h.syncKetoRole(c.Context(), localUser.ID,
|
h.syncKetoRole(c.Context(), localUser.ID,
|
||||||
localUser.Role, oldRole, oldTenantID, localUser.TenantID)
|
localUser.Role, oldRole, oldTenantID, localUser.TenantID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -992,12 +999,12 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// [Validation] Based on Tenant Schema (Multi-tenant aware)
|
// [Validation] Based on Tenant Schema (Multi-tenant aware)
|
||||||
isAdmin := requester != nil && (requester.Role == domain.RoleSuperAdmin || requester.Role == domain.RoleTenantAdmin)
|
isAdmin := requester != nil && (requester.Role == domain.RoleSuperAdmin || requester.Role == domain.RoleTenantAdmin)
|
||||||
|
|
||||||
// If metadata is namespaced (key is tenant ID), validate each namespace
|
// If metadata is namespaced (key is tenant ID), validate each namespace
|
||||||
// If it's flat, validate using schemaCompCode
|
// If it's flat, validate using schemaCompCode
|
||||||
for key, val := range req.Metadata {
|
for key, val := range req.Metadata {
|
||||||
// Basic check if key looks like a UUID (tenant ID)
|
// Basic check if key looks like a UUID (tenant ID)
|
||||||
if len(key) >= 32 {
|
if len(key) >= 32 {
|
||||||
// Namespaced metadata
|
// Namespaced metadata
|
||||||
if h.TenantService != nil {
|
if h.TenantService != nil {
|
||||||
tenant, err := h.TenantService.GetTenant(c.Context(), key)
|
tenant, err := h.TenantService.GetTenant(c.Context(), key)
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ func (m *MockKratosAdmin) ListIdentities(ctx context.Context) ([]service.KratosI
|
|||||||
args := m.Called(ctx)
|
args := m.Called(ctx)
|
||||||
return args.Get(0).([]service.KratosIdentity), args.Error(1)
|
return args.Get(0).([]service.KratosIdentity), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockKratosAdmin) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) {
|
func (m *MockKratosAdmin) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) {
|
||||||
args := m.Called(ctx, identifier)
|
args := m.Called(ctx, identifier)
|
||||||
return args.String(0), args.Error(1)
|
return args.String(0), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockKratosAdmin) GetIdentity(ctx context.Context, id string) (*service.KratosIdentity, error) {
|
func (m *MockKratosAdmin) GetIdentity(ctx context.Context, id string) (*service.KratosIdentity, error) {
|
||||||
args := m.Called(ctx, id)
|
args := m.Called(ctx, id)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
@@ -37,6 +39,7 @@ func (m *MockKratosAdmin) GetIdentity(ctx context.Context, id string) (*service.
|
|||||||
}
|
}
|
||||||
return args.Get(0).(*service.KratosIdentity), args.Error(1)
|
return args.Get(0).(*service.KratosIdentity), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockKratosAdmin) UpdateIdentity(ctx context.Context, id string, traits map[string]interface{}, state string) (*service.KratosIdentity, error) {
|
func (m *MockKratosAdmin) UpdateIdentity(ctx context.Context, id string, traits map[string]interface{}, state string) (*service.KratosIdentity, error) {
|
||||||
args := m.Called(ctx, id, traits, state)
|
args := m.Called(ctx, id, traits, state)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
@@ -44,9 +47,11 @@ func (m *MockKratosAdmin) UpdateIdentity(ctx context.Context, id string, traits
|
|||||||
}
|
}
|
||||||
return args.Get(0).(*service.KratosIdentity), args.Error(1)
|
return args.Get(0).(*service.KratosIdentity), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockKratosAdmin) UpdateIdentityPassword(ctx context.Context, id, pw string) error {
|
func (m *MockKratosAdmin) UpdateIdentityPassword(ctx context.Context, id, pw string) error {
|
||||||
return m.Called(ctx, id, pw).Error(0)
|
return m.Called(ctx, id, pw).Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockKratosAdmin) DeleteIdentity(ctx context.Context, id string) error {
|
func (m *MockKratosAdmin) DeleteIdentity(ctx context.Context, id string) error {
|
||||||
return m.Called(ctx, id).Error(0)
|
return m.Called(ctx, id).Error(0)
|
||||||
}
|
}
|
||||||
@@ -59,9 +64,11 @@ func (m *MockOryProvider) CreateUser(user *domain.BrokerUser, password string) (
|
|||||||
args := m.Called(user, password)
|
args := m.Called(user, password)
|
||||||
return args.String(0), args.Error(1)
|
return args.String(0), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockOryProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error {
|
func (m *MockOryProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error {
|
||||||
return m.Called(loginID, newPassword, r).Error(0)
|
return m.Called(loginID, newPassword, r).Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockOryProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) {
|
func (m *MockOryProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) {
|
||||||
args := m.Called()
|
args := m.Called()
|
||||||
return args.Get(0).(*domain.PasswordPolicy), args.Error(1)
|
return args.Get(0).(*domain.PasswordPolicy), args.Error(1)
|
||||||
@@ -162,7 +169,7 @@ func TestUserHandler_BulkCreateUsers(t *testing.T) {
|
|||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
json.NewDecoder(resp.Body).Decode(&result)
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
results := result["results"].([]interface{})
|
results := result["results"].([]interface{})
|
||||||
|
|
||||||
assert.False(t, results[0].(map[string]interface{})["success"].(bool))
|
assert.False(t, results[0].(map[string]interface{})["success"].(bool))
|
||||||
assert.Contains(t, results[0].(map[string]interface{})["message"].(string), "tenant not found")
|
assert.Contains(t, results[0].(map[string]interface{})["message"].(string), "tenant not found")
|
||||||
})
|
})
|
||||||
@@ -250,7 +257,7 @@ func TestUserHandler_BulkUpdateUsers(t *testing.T) {
|
|||||||
mockKratos.On("GetIdentity", mock.Anything, "u-1").Return(&service.KratosIdentity{
|
mockKratos.On("GetIdentity", mock.Anything, "u-1").Return(&service.KratosIdentity{
|
||||||
ID: "u-1", Traits: map[string]interface{}{"email": "u1@test.com"}, State: "active",
|
ID: "u-1", Traits: map[string]interface{}{"email": "u1@test.com"}, State: "active",
|
||||||
}, nil).Once()
|
}, nil).Once()
|
||||||
|
|
||||||
mockKratos.On("UpdateIdentity", mock.Anything, "u-1", mock.Anything, "inactive").Return(&service.KratosIdentity{}, nil).Once()
|
mockKratos.On("UpdateIdentity", mock.Anything, "u-1", mock.Anything, "inactive").Return(&service.KratosIdentity{}, nil).Once()
|
||||||
|
|
||||||
status := "inactive"
|
status := "inactive"
|
||||||
@@ -264,7 +271,7 @@ func TestUserHandler_BulkUpdateUsers(t *testing.T) {
|
|||||||
|
|
||||||
resp, _ := app.Test(req)
|
resp, _ := app.Test(req)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
json.NewDecoder(resp.Body).Decode(&result)
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
results := result["results"].([]interface{})
|
results := result["results"].([]interface{})
|
||||||
@@ -285,7 +292,7 @@ func TestUserHandler_BulkDeleteUsers(t *testing.T) {
|
|||||||
t.Run("Success - Delete multiple", func(t *testing.T) {
|
t.Run("Success - Delete multiple", func(t *testing.T) {
|
||||||
mockKratos.On("GetIdentity", mock.Anything, "u-1").Return(&service.KratosIdentity{ID: "u-1"}, nil).Once()
|
mockKratos.On("GetIdentity", mock.Anything, "u-1").Return(&service.KratosIdentity{ID: "u-1"}, nil).Once()
|
||||||
mockKratos.On("GetIdentity", mock.Anything, "u-2").Return(&service.KratosIdentity{ID: "u-2"}, nil).Once()
|
mockKratos.On("GetIdentity", mock.Anything, "u-2").Return(&service.KratosIdentity{ID: "u-2"}, nil).Once()
|
||||||
|
|
||||||
mockKratos.On("DeleteIdentity", mock.Anything, "u-1").Return(nil).Once()
|
mockKratos.On("DeleteIdentity", mock.Anything, "u-1").Return(nil).Once()
|
||||||
mockKratos.On("DeleteIdentity", mock.Anything, "u-2").Return(nil).Once()
|
mockKratos.On("DeleteIdentity", mock.Anything, "u-2").Return(nil).Once()
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func (r *userRepository) CountByTenantIDs(ctx context.Context, tenantIDs []strin
|
|||||||
}
|
}
|
||||||
var results []result
|
var results []result
|
||||||
counts := make(map[string]int64)
|
counts := make(map[string]int64)
|
||||||
|
|
||||||
if len(tenantIDs) == 0 {
|
if len(tenantIDs) == 0 {
|
||||||
return counts, nil
|
return counts, nil
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ func (r *userRepository) List(ctx context.Context, offset, limit int, search str
|
|||||||
if search != "" {
|
if search != "" {
|
||||||
searchTerm := "%" + search + "%"
|
searchTerm := "%" + search + "%"
|
||||||
// Search in basic fields and metadata (PostgreSQL JSONB)
|
// Search in basic fields and metadata (PostgreSQL JSONB)
|
||||||
db = db.Where("(email LIKE ? OR name LIKE ? OR company_code LIKE ? OR metadata::text LIKE ?)",
|
db = db.Where("(email LIKE ? OR name LIKE ? OR company_code LIKE ? OR metadata::text LIKE ?)",
|
||||||
searchTerm, searchTerm, searchTerm, searchTerm)
|
searchTerm, searchTerm, searchTerm, searchTerm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,13 +67,19 @@ func (s *tenantService) ListManageableTenants(ctx context.Context, userID string
|
|||||||
// Fallback: Check direct membership if list objects didn't catch everything
|
// Fallback: Check direct membership if list objects didn't catch everything
|
||||||
directAdminIDs, _ := s.keto.ListObjects(ctx, "Tenant", "admins", "User:"+userID)
|
directAdminIDs, _ := s.keto.ListObjects(ctx, "Tenant", "admins", "User:"+userID)
|
||||||
directOwnerIDs, _ := s.keto.ListObjects(ctx, "Tenant", "owners", "User:"+userID)
|
directOwnerIDs, _ := s.keto.ListObjects(ctx, "Tenant", "owners", "User:"+userID)
|
||||||
|
|
||||||
idMap := make(map[string]bool)
|
idMap := make(map[string]bool)
|
||||||
for _, id := range directAdminIDs { idMap[id] = true }
|
for _, id := range directAdminIDs {
|
||||||
for _, id := range directOwnerIDs { idMap[id] = true }
|
idMap[id] = true
|
||||||
|
}
|
||||||
|
for _, id := range directOwnerIDs {
|
||||||
|
idMap[id] = true
|
||||||
|
}
|
||||||
|
|
||||||
allIDs = make([]string, 0, len(idMap))
|
allIDs = make([]string, 0, len(idMap))
|
||||||
for id := range idMap { allIDs = append(allIDs, id) }
|
for id := range idMap {
|
||||||
|
allIDs = append(allIDs, id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(allIDs) == 0 {
|
if len(allIDs) == 0 {
|
||||||
|
|||||||
92
docs/TEST_GUIDE.md
Normal file
92
docs/TEST_GUIDE.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# 린트 체크 및 테스트 실행 가이드
|
||||||
|
|
||||||
|
이 문서는 Baron SSO 프로젝트의 각 모듈별 정적 분석(Lint) 및 테스트 수행 방법을 설명합니다.
|
||||||
|
|
||||||
|
## 1. 준비 사항
|
||||||
|
테스트를 실행하기 위해 다음 도구들이 설치되어 있어야 합니다.
|
||||||
|
- **Docker & Docker Compose** (백엔드 인프라 의존성용)
|
||||||
|
- **Go 1.22+**
|
||||||
|
- **Flutter SDK**
|
||||||
|
- **Node.js 20+**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 모듈별 실행 명령어
|
||||||
|
|
||||||
|
### 2.1 Backend (Go)
|
||||||
|
백엔드 테스트는 Redis와 ClickHouse 컨테이너가 실행 중이어야 합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 인프라 서비스 실행
|
||||||
|
docker compose -f compose.infra.yaml up -d redis clickhouse
|
||||||
|
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# 린트 및 포맷 확인
|
||||||
|
go fmt ./...
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
# 유닛 테스트 실행
|
||||||
|
export REDIS_ADDR=localhost:6379 CLICKHOUSE_HOST=localhost CLICKHOUSE_PORT_NATIVE=9000
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Userfront (Flutter)
|
||||||
|
```bash
|
||||||
|
cd userfront
|
||||||
|
|
||||||
|
# 코드 포맷 확인
|
||||||
|
dart format --output=none --set-exit-if-changed lib test
|
||||||
|
|
||||||
|
# 정적 분석 (경고/정보 메시지는 무시)
|
||||||
|
flutter analyze --no-fatal-warnings --no-fatal-infos
|
||||||
|
|
||||||
|
# 유닛 및 위젯 테스트
|
||||||
|
flutter test
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Adminfront & Devfront (React)
|
||||||
|
Biome을 사용하여 린트 및 포맷팅을 관리하며, Playwright로 E2E 테스트를 수행합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd adminfront # 또는 cd devfront
|
||||||
|
|
||||||
|
# 린트 및 포맷 자동 수정
|
||||||
|
npx biome check --write .
|
||||||
|
|
||||||
|
# 안전하지 않은 규칙까지 포함하여 자동 수정 (필요 시)
|
||||||
|
npx biome check --write --unsafe .
|
||||||
|
|
||||||
|
# E2E 테스트 실행
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 i18n 검증
|
||||||
|
코드 내에서 사용되는 다국어 키와 `locales/*.toml` 파일 간의 정합성을 검증합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 프로젝트 루트에서 실행
|
||||||
|
node tools/i18n-scanner/index.js
|
||||||
|
node tools/i18n-scanner/report.js
|
||||||
|
|
||||||
|
# 결과 확인
|
||||||
|
cat reports/i18n-report.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 통합 실행 (Batch)
|
||||||
|
프로젝트 루트의 스크립트를 통해 전체 과정을 한 번에 시도할 수 있습니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash run_local_checks.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 정리 (Cleanup)
|
||||||
|
테스트 완료 후 실행 중인 테스트용 인프라 서비스를 종료합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f compose.infra.yaml down
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user