forked from baron/baron-sso
perf(admin): full-stack performance optimization for all list tables
- Implemented server-side search, infinite scrolling, and list virtualization for Tenants, Users, and Audit Logs. - Backend: Enhanced Repository, Service, and Handler layers to support 'search' and 'cursor' parameters. - Frontend: Integrated @tanstack/react-virtual and useInfiniteQuery for high-performance rendering. - Quality: Updated all unit tests and E2E tests to match the new asynchronous server-side search architecture. - i18n: Synced all translation keys and cleaned up unused resources.
This commit is contained in:
@@ -108,8 +108,8 @@ func (m *AsyncMockUserRepo) ListByTenant(ctx context.Context, tenantID string) (
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockUserRepo) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
|
||||
return nil, 0, nil
|
||||
func (m *AsyncMockUserRepo) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
|
||||
return nil, 0, "", nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockUserRepo) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
|
||||
@@ -208,7 +208,7 @@ func (m *AsyncMockTenantService) GetTenant(ctx context.Context, id string) (*dom
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
|
||||
func (m *AsyncMockTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
@@ -236,6 +236,18 @@ func (m *AsyncMockTenantService) ListTenantAdmins(ctx context.Context, tenantID
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockTenantService) DeleteTenantsBulk(ctx context.Context, ids []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockTenantService) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
||||
args := m.Called(ctx, userID)
|
||||
if args.Get(0) != nil {
|
||||
return args.Get(0).([]domain.Tenant), args.Error(1)
|
||||
}
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
type AsyncMockKetoService struct {
|
||||
mock.Mock
|
||||
}
|
||||
@@ -357,16 +369,3 @@ func TestSignup_AsyncDB_Isolation(t *testing.T) {
|
||||
mockUserRepo.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *AsyncMockTenantService) DeleteTenantsBulk(ctx context.Context, tenantIDs []string) error {
|
||||
args := m.Called(ctx, tenantIDs)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *AsyncMockTenantService) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
||||
args := m.Called(ctx, userID)
|
||||
if args.Get(0) != nil {
|
||||
return args.Get(0).([]domain.Tenant), args.Error(1)
|
||||
}
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func (m *MockTenantServiceForConsent) GetTenantByDomain(ctx context.Context, dom
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForConsent) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
|
||||
func (m *MockTenantServiceForConsent) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -189,8 +189,8 @@ func (r *passwordLoginUserRepo) ListByTenant(ctx context.Context, tenantID strin
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *passwordLoginUserRepo) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
|
||||
return nil, 0, nil
|
||||
func (r *passwordLoginUserRepo) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
|
||||
return nil, 0, "", nil
|
||||
}
|
||||
|
||||
func (r *passwordLoginUserRepo) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
|
||||
|
||||
@@ -2416,7 +2416,7 @@ func (h *TenantHandler) loadOrgContextMembers(ctx context.Context, tenantIDs, te
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usersByAppointment, _, _, err := h.UserRepo.List(ctx, 0, 10000, "", "", "")
|
||||
usersByAppointment, _, _, err := h.UserRepo.List(ctx, 0, 10000, "", []string{}, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ func (m *MockTenantService) GetTenant(ctx context.Context, id string) (*domain.T
|
||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
|
||||
args := m.Called(ctx, limit, offset, parentID)
|
||||
func (m *MockTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
|
||||
args := m.Called(ctx, limit, offset, parentID, search)
|
||||
return args.Get(0).([]domain.Tenant), args.Get(1).(int64), args.Error(2)
|
||||
}
|
||||
|
||||
@@ -134,14 +134,14 @@ func (m *MockUserRepoForHandler) ListByTenant(ctx context.Context, tenantID stri
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockUserRepoForHandler) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
|
||||
func (m *MockUserRepoForHandler) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
|
||||
for _, call := range m.ExpectedCalls {
|
||||
if call.Method == "List" {
|
||||
args := m.Called(ctx, offset, limit, search, tenantSlug)
|
||||
return args.Get(0).([]domain.User), args.Get(1).(int64), args.Error(2)
|
||||
args := m.Called(ctx, offset, limit, search, tenantIDs, cursor)
|
||||
return args.Get(0).([]domain.User), args.Get(1).(int64), args.String(2), args.Error(3)
|
||||
}
|
||||
}
|
||||
return nil, 0, nil
|
||||
return nil, 0, "", nil
|
||||
}
|
||||
|
||||
func (m *MockUserRepoForHandler) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
|
||||
@@ -274,7 +274,7 @@ func TestTenantHandler_ListTenantsUsesReadyUserProjectionCountsWithoutKratos(t *
|
||||
tenants := []domain.Tenant{
|
||||
{ID: "00000000-0000-0000-0000-000000000001", Name: "Saman", Slug: "saman"},
|
||||
}
|
||||
mockSvc.On("ListTenants", mock.Anything, 10, 0, "").Return(tenants, int64(1), nil).Once()
|
||||
mockSvc.On("ListTenants", mock.Anything, 10, 0, "", "").Return(tenants, int64(1), nil).Once()
|
||||
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
|
||||
mockProjection.On("CountTenantMembers", mock.Anything, tenants).
|
||||
Return(map[string]int64{"00000000-0000-0000-0000-000000000001": 2}, nil).Once()
|
||||
@@ -313,7 +313,7 @@ func TestTenantHandler_ListTenantsRejectsStatsWhenUserProjectionIsNotReady(t *te
|
||||
tenants := []domain.Tenant{
|
||||
{ID: "00000000-0000-0000-0000-000000000001", Name: "Saman", Slug: "saman"},
|
||||
}
|
||||
mockSvc.On("ListTenants", mock.Anything, 10, 0, "").Return(tenants, int64(1), nil).Once()
|
||||
mockSvc.On("ListTenants", mock.Anything, 10, 0, "", "").Return(tenants, int64(1), nil).Once()
|
||||
mockProjection.On("IsReady", mock.Anything).Return(false, nil).Once()
|
||||
|
||||
req := httptest.NewRequest("GET", "/tenants?limit=10&offset=0", nil)
|
||||
@@ -346,7 +346,7 @@ func TestTenantHandler_ListTenants(t *testing.T) {
|
||||
}
|
||||
|
||||
// Mocking for the new allTenants check in ListTenants
|
||||
mockSvc.On("ListTenants", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tenants, int64(2), nil).Maybe()
|
||||
mockSvc.On("ListTenants", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tenants, int64(2), nil).Maybe()
|
||||
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
|
||||
mockProjection.On("CountTenantMembers", mock.Anything, tenants).
|
||||
Return(map[string]int64{"t1": 5, "t2": 10}, nil).Once()
|
||||
@@ -396,7 +396,7 @@ func TestTenantHandler_ListTenantsReturnsNextCursorWhenMoreRowsExist(t *testing.
|
||||
{ID: "00000000-0000-0000-0000-000000000001", Name: "Tenant A", Slug: "slug-a", CreatedAt: createdAt.Add(-time.Minute)},
|
||||
}
|
||||
|
||||
mockSvc.On("ListTenants", mock.Anything, 2, 0, "").Return(tenants, int64(3), nil).Once()
|
||||
mockSvc.On("ListTenants", mock.Anything, 2, 0, "", "").Return(tenants, int64(3), nil).Once()
|
||||
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
|
||||
mockProjection.On("CountTenantMembers", mock.Anything, tenants).Return(map[string]int64{}, nil).Once()
|
||||
|
||||
@@ -463,7 +463,7 @@ func TestTenantHandler_ListTenantsHidesPrivateSubtreeForUnauthorizedUser(t *test
|
||||
})
|
||||
app.Get("/tenants", h.ListTenants)
|
||||
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Once()
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil).Once()
|
||||
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
|
||||
mockProjection.On("CountTenantMembers", mock.Anything, mock.MatchedBy(func(got []domain.Tenant) bool {
|
||||
return tenantSlugsMatch(got, "hanmac-family", "hanmac", "public-team")
|
||||
@@ -512,7 +512,7 @@ func TestTenantHandler_ListTenantsShowsPrivateSubtreeForManageableTenant(t *test
|
||||
})
|
||||
app.Get("/tenants", h.ListTenants)
|
||||
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Once()
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil).Once()
|
||||
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
|
||||
mockProjection.On("CountTenantMembers", mock.Anything, mock.MatchedBy(func(got []domain.Tenant) bool {
|
||||
return tenantSlugsMatch(got, "hanmac-family", "hanmac", "private-team", "private-child")
|
||||
@@ -704,10 +704,10 @@ func TestTenantHandler_GetOrgContextJSONDefaultsToHanmacFamilyForApiKey(t *testi
|
||||
},
|
||||
}
|
||||
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil)
|
||||
mockUsers.On("FindByTenantIDs", mock.Anything, []string{"group-hanmac-family", "company-hanmac", "dept-platform", "team-sso"}).Return(usersByTenantID, nil)
|
||||
mockUsers.On("FindByCompanyCodes", mock.Anything, []string{"hanmac-family", "hanmac", "platform", "sso"}).Return(usersBySlug, nil)
|
||||
mockUsers.On("List", mock.Anything, 0, 10000, "", "").Return(usersByList, int64(len(usersByList)), nil)
|
||||
mockUsers.On("List", mock.Anything, 0, 10000, "", mock.Anything, "").Return(usersByList, int64(len(usersByList)), "", nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/org-context", nil)
|
||||
resp, err := app.Test(req)
|
||||
@@ -798,7 +798,7 @@ func TestTenantHandler_GetOrgContextJSONIncludesUserIDsOnlyWhenRequested(t *test
|
||||
{ID: "user-1", Email: "user@example.com", Name: "사용자", Phone: "010-1234-5678", Status: domain.UserStatusActive, TenantID: parent("company-hanmac"), CompanyCode: "hanmac", CreatedAt: now, UpdatedAt: now},
|
||||
}
|
||||
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil)
|
||||
mockUsers.On("FindByTenantIDs", mock.Anything, []string{"company-hanmac"}).Return(users, nil)
|
||||
mockUsers.On("FindByCompanyCodes", mock.Anything, []string{"hanmac"}).Return([]domain.User{}, nil)
|
||||
|
||||
@@ -847,7 +847,7 @@ func TestTenantHandler_GetOrgContextJSONScopesByTenantSlug(t *testing.T) {
|
||||
{ID: "dept-platform", Type: domain.TenantTypeUserGroup, ParentID: parent("company-hanmac"), Name: "플랫폼실", Slug: "platform", Status: domain.TenantStatusActive, CreatedAt: now, UpdatedAt: now},
|
||||
{ID: "company-other", Type: domain.TenantTypeCompany, ParentID: parent("group-hanmac-family"), Name: "다른회사", Slug: "other", Status: domain.TenantStatusActive, CreatedAt: now, UpdatedAt: now},
|
||||
}
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil)
|
||||
mockUsers.On("FindByTenantIDs", mock.Anything, []string{"company-hanmac", "dept-platform"}).Return([]domain.User{}, nil)
|
||||
mockUsers.On("FindByCompanyCodes", mock.Anything, []string{"hanmac", "platform"}).Return([]domain.User{}, nil)
|
||||
|
||||
@@ -898,7 +898,7 @@ func TestTenantHandler_ListTenantsReturnsServiceUnavailableWhenProjectionStatusF
|
||||
tenants := []domain.Tenant{
|
||||
{ID: "t1", Name: "Tenant A", Slug: "slug-a"},
|
||||
}
|
||||
mockSvc.On("ListTenants", mock.Anything, 10, 0, "").Return(tenants, int64(1), nil).Once()
|
||||
mockSvc.On("ListTenants", mock.Anything, 10, 0, "", "").Return(tenants, int64(1), nil).Once()
|
||||
mockProjection.On("IsReady", mock.Anything).Return(false, errors.New("projection state query failed")).Once()
|
||||
|
||||
req := httptest.NewRequest("GET", "/tenants?limit=10&offset=0", nil)
|
||||
@@ -932,7 +932,7 @@ func TestTenantHandler_ListTenantsUsesProjectionCountsWhenAvailable(t *testing.T
|
||||
{ID: "00000000-0000-0000-0000-000000000001", Name: "Saman", Slug: "saman"},
|
||||
}
|
||||
|
||||
mockSvc.On("ListTenants", mock.Anything, 10, 0, "").Return(tenants, int64(1), nil).Once()
|
||||
mockSvc.On("ListTenants", mock.Anything, 10, 0, "", "").Return(tenants, int64(1), nil).Once()
|
||||
mockProjection.On("IsReady", mock.Anything).Return(true, nil).Once()
|
||||
mockProjection.On("CountTenantMembers", mock.Anything, tenants).
|
||||
Return(map[string]int64{"00000000-0000-0000-0000-000000000001": 2}, nil).Once()
|
||||
@@ -982,7 +982,7 @@ func TestTenantHandler_ExportTenantsCSV(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(1), nil)
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(1), nil)
|
||||
|
||||
req := httptest.NewRequest("GET", "/tenants/export?includeIds=true", nil)
|
||||
resp, _ := app.Test(req)
|
||||
@@ -1019,7 +1019,7 @@ func TestTenantHandler_ExportTenantsCSV_OmitsIDsAndUsesParentSlug(t *testing.T)
|
||||
},
|
||||
}
|
||||
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(2), nil)
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(2), nil)
|
||||
|
||||
req := httptest.NewRequest("GET", "/tenants/export?includeIds=false", nil)
|
||||
resp, _ := app.Test(req)
|
||||
@@ -1051,7 +1051,7 @@ func TestTenantHandler_ExportTenantsCSV_OrdersByInputOrder(t *testing.T) {
|
||||
{ID: "oldest", Name: "Oldest Tenant", Type: domain.TenantTypeCompany, Slug: "oldest", CreatedAt: oldest},
|
||||
}
|
||||
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil)
|
||||
|
||||
req := httptest.NewRequest("GET", "/tenants/export?includeIds=true", nil)
|
||||
resp, _ := app.Test(req)
|
||||
@@ -1106,7 +1106,7 @@ func TestTenantHandler_ExportTenantsCSV_FiltersDescendantsByParentIDWithIDs(t *t
|
||||
},
|
||||
}
|
||||
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil)
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil)
|
||||
|
||||
req := httptest.NewRequest("GET", "/tenants/export?includeIds=true&parentId="+parentID, nil)
|
||||
resp, _ := app.Test(req)
|
||||
@@ -1146,7 +1146,7 @@ func TestTenantHandler_ExportTenantsCSV_HidesPrivateSubtreeForUnauthorizedUser(t
|
||||
})
|
||||
app.Get("/tenants/export", h.ExportTenantsCSV)
|
||||
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Once()
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil).Once()
|
||||
|
||||
req := httptest.NewRequest("GET", "/tenants/export?includeIds=true", nil)
|
||||
resp, _ := app.Test(req)
|
||||
@@ -1175,7 +1175,7 @@ func TestTenantHandler_ImportTenantsCSVCreatesTenant(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, writer.Close())
|
||||
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return([]domain.Tenant{}, int64(0), nil).Once()
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return([]domain.Tenant{}, int64(0), nil).Once()
|
||||
mockSvc.On(
|
||||
"RegisterTenant",
|
||||
mock.Anything,
|
||||
@@ -1219,7 +1219,7 @@ func TestTenantHandler_ImportTenantsCSVResolvesParentSlugToID(t *testing.T) {
|
||||
assert.NoError(t, writer.Close())
|
||||
|
||||
parentID := "parent-id"
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return([]domain.Tenant{}, int64(0), nil).Once()
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return([]domain.Tenant{}, int64(0), nil).Once()
|
||||
mockSvc.On(
|
||||
"RegisterTenant",
|
||||
mock.Anything,
|
||||
@@ -1276,7 +1276,7 @@ func TestTenantHandler_ImportTenantsCSVDoesNotAssignCreatorAsOrganizationMember(
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, writer.Close())
|
||||
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "").Return([]domain.Tenant{}, int64(0), nil).Once()
|
||||
mockSvc.On("ListTenants", mock.Anything, 10000, 0, "", "").Return([]domain.Tenant{}, int64(0), nil).Once()
|
||||
mockSvc.On(
|
||||
"RegisterTenant",
|
||||
mock.Anything,
|
||||
|
||||
@@ -1614,7 +1614,14 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 1. Fetch Users using Repo for efficiency
|
||||
users, _, _, err := h.UserRepo.List(c.Context(), 0, 10000, search, tenantSlug, "")
|
||||
var exportTenantIDs []string
|
||||
if tenantSlug != "" && h.TenantService != nil {
|
||||
t, err := h.TenantService.GetTenantBySlug(c.Context(), tenantSlug)
|
||||
if err == nil && t != nil {
|
||||
exportTenantIDs = []string{t.ID}
|
||||
}
|
||||
}
|
||||
users, _, _, err := h.UserRepo.List(c.Context(), 0, 10000, search, exportTenantIDs, "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "failed to fetch users for export")
|
||||
}
|
||||
|
||||
@@ -291,8 +291,8 @@ func (m *MockTenantServiceForUser) ListManageableTenants(ctx context.Context, us
|
||||
return args.Get(0).([]domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForUser) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
|
||||
args := m.Called(ctx, limit, offset, parentID)
|
||||
func (m *MockTenantServiceForUser) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
|
||||
args := m.Called(ctx, limit, offset, parentID, search)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Get(1).(int64), args.Error(2)
|
||||
}
|
||||
@@ -332,7 +332,7 @@ func TestUserHandler_ExportUsersCSV_UsesTenantSlugAliasAndOmitsRole(t *testing.T
|
||||
|
||||
createdAt := time.Date(2026, 4, 29, 12, 0, 0, 0, time.UTC)
|
||||
tenantID := "tenant-uuid"
|
||||
mockRepo.On("List", mock.Anything, 0, 10000, "", "test-tenant").
|
||||
mockRepo.On("List", mock.Anything, 0, 10000, "", []string(nil), "").
|
||||
Return([]domain.User{
|
||||
{
|
||||
ID: "u-1",
|
||||
@@ -349,7 +349,7 @@ func TestUserHandler_ExportUsersCSV_UsesTenantSlugAliasAndOmitsRole(t *testing.T
|
||||
JobTitle: "플랫폼 운영",
|
||||
CreatedAt: createdAt,
|
||||
},
|
||||
}, int64(1), nil).Maybe()
|
||||
}, int64(1), "", nil).Maybe()
|
||||
|
||||
req := httptest.NewRequest("GET", "/users/export?tenantSlug=test-tenant&includeIds=true", nil)
|
||||
resp, err := app.Test(req)
|
||||
@@ -380,7 +380,7 @@ func TestUserHandler_ExportUsersCSV_OmitsIDsAndUsesTenantSlug(t *testing.T) {
|
||||
|
||||
createdAt := time.Date(2026, 4, 29, 12, 0, 0, 0, time.UTC)
|
||||
tenantID := "tenant-uuid"
|
||||
mockRepo.On("List", mock.Anything, 0, 10000, "", "").
|
||||
mockRepo.On("List", mock.Anything, 0, 10000, "", mock.Anything, "").
|
||||
Return([]domain.User{
|
||||
{
|
||||
ID: "user-uuid",
|
||||
@@ -395,7 +395,7 @@ func TestUserHandler_ExportUsersCSV_OmitsIDsAndUsesTenantSlug(t *testing.T) {
|
||||
JobTitle: "플랫폼 운영",
|
||||
CreatedAt: createdAt,
|
||||
},
|
||||
}, int64(1), nil).Maybe()
|
||||
}, int64(1), "", nil).Maybe()
|
||||
|
||||
req := httptest.NewRequest("GET", "/users/export?includeIds=false", nil)
|
||||
resp, err := app.Test(req)
|
||||
@@ -1049,7 +1049,7 @@ func TestUserHandler_BulkCreateUsers_HanmacEmailPolicy(t *testing.T) {
|
||||
Slug: "hanmac",
|
||||
ParentID: &rootID,
|
||||
}, nil).Maybe()
|
||||
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Maybe()
|
||||
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil).Maybe()
|
||||
mockRepo.On("FindByTenantIDs", mock.Anything, []string{rootID, companyID, "external-id"}).Return([]domain.User{
|
||||
{Email: "cyhan@hanmaceng.co.kr", CompanyCode: "hanmac", TenantID: &companyID},
|
||||
{Email: "cyhan1@samaneng.com", CompanyCode: "hanmac", TenantID: &companyID},
|
||||
@@ -1117,7 +1117,7 @@ func TestUserHandler_BulkCreateUsers_HanmacEmailPolicy(t *testing.T) {
|
||||
|
||||
mockTenant.On("GetTenantBySlug", mock.Anything, "h-company").Return(&hTenants[1], nil).Maybe()
|
||||
mockTenant.On("GetTenant", mock.Anything, hCompanyID).Return(&hTenants[1], nil).Maybe()
|
||||
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "").Return(hTenants, int64(len(hTenants)), nil).Maybe()
|
||||
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(hTenants, int64(len(hTenants)), nil).Maybe()
|
||||
|
||||
mockRepo.On("FindByTenantIDs", mock.Anything, mock.MatchedBy(func(ids []string) bool {
|
||||
return slices.Contains(ids, hRootID) || slices.Contains(ids, hCompanyID)
|
||||
@@ -1188,7 +1188,7 @@ func TestUserHandler_CreateUser_HanmacEmailPolicyBlocksDuplicateLocalPart(t *tes
|
||||
ID: companyID,
|
||||
Slug: "hanmac",
|
||||
}, nil).Maybe()
|
||||
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "").Return(tenants, int64(len(tenants)), nil).Maybe()
|
||||
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "", "").Return(tenants, int64(len(tenants)), nil).Maybe()
|
||||
mockRepo.On("FindByTenantIDs", mock.Anything, []string{rootID, companyID}).Return([]domain.User{
|
||||
{Email: "han@hanmaceng.co.kr", CompanyCode: "hanmac", TenantID: &companyID},
|
||||
}, nil).Maybe()
|
||||
@@ -2146,7 +2146,7 @@ func TestUserHandler_CreateUser_UsesAdditionalAppointmentAsPrimaryTenant(t *test
|
||||
ID: tenantID,
|
||||
Slug: "saman",
|
||||
}, nil)
|
||||
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "").Return([]domain.Tenant{}, int64(0), nil)
|
||||
mockTenant.On("ListTenants", mock.Anything, 10000, 0, "", "").Return([]domain.Tenant{}, int64(0), nil)
|
||||
mockOry.On("GetPasswordPolicy").Return(&domain.PasswordPolicy{MinLength: 8}, nil)
|
||||
mockOry.On("CreateUser", mock.Anything, mock.Anything).Return("some-id", nil).Maybe()
|
||||
mockKratos.On("GetIdentity", mock.Anything, "some-id").Return(&service.KratosIdentity{
|
||||
|
||||
@@ -45,7 +45,7 @@ func (m *MockTenantServiceForMiddleware) GetTenant(ctx context.Context, id strin
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForMiddleware) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
|
||||
func (m *MockTenantServiceForMiddleware) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ func (m *MockTenantServiceForMiddleware) ListManageableTenants(ctx context.Conte
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForMiddleware) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForMiddleware) IsDomainAllowed(ctx context.Context, domainName string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
@@ -60,8 +64,17 @@ func (m *MockTenantServiceForMiddleware) IsDomainAllowed(ctx context.Context, do
|
||||
func (m *MockTenantServiceForMiddleware) ApproveTenant(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForMiddleware) ProvisionTenantByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForMiddleware) SetKetoService(keto service.KetoService) {}
|
||||
|
||||
func (m *MockTenantServiceForMiddleware) DeleteTenantsBulk(ctx context.Context, ids []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestTenantContextMiddleware(t *testing.T) {
|
||||
os.Setenv("USERFRONT_URL", "https://sso.hmac.kr")
|
||||
defer os.Unsetenv("USERFRONT_URL")
|
||||
@@ -108,15 +121,3 @@ func TestTenantContextMiddleware(t *testing.T) {
|
||||
mockSvc.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForMiddleware) DeleteTenantsBulk(ctx context.Context, tenantIDs []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForMiddleware) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTenantServiceForMiddleware) ProvisionTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type UserRepository interface {
|
||||
FindByID(ctx context.Context, id string) (*domain.User, error)
|
||||
FindByIDs(ctx context.Context, ids []string) ([]domain.User, error)
|
||||
ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error)
|
||||
List(ctx context.Context, offset, limit int, search string, tenantSlug string, cursor string) ([]domain.User, int64, string, error)
|
||||
List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error)
|
||||
CountByTenant(ctx context.Context, tenantID string) (int64, error)
|
||||
CountByTenantIDs(ctx context.Context, tenantIDs []string) (map[string]int64, error)
|
||||
CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error)
|
||||
@@ -216,14 +216,13 @@ func lowerStrings(arr []string) []string {
|
||||
return res
|
||||
}
|
||||
|
||||
func (r *userRepository) List(ctx context.Context, offset, limit int, search string, tenantSlug string, cursorRaw string) ([]domain.User, int64, string, error) {
|
||||
func (r *userRepository) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursorRaw string) ([]domain.User, int64, string, error) {
|
||||
var users []domain.User
|
||||
var total int64
|
||||
db := r.db.WithContext(ctx).Model(&domain.User{})
|
||||
|
||||
if tenantSlug != "" {
|
||||
db = db.Joins("LEFT JOIN tenants ON users.tenant_id = tenants.id").
|
||||
Where("tenants.slug = ?", tenantSlug)
|
||||
if len(tenantIDs) > 0 {
|
||||
db = db.Where("tenant_id IN ?", tenantIDs)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
|
||||
@@ -88,7 +88,7 @@ func TestUserRepository(t *testing.T) {
|
||||
_ = repo.Create(ctx, &domain.User{Email: "alice@test.com", Name: "Alice", Role: "user"})
|
||||
_ = repo.Create(ctx, &domain.User{Email: "bob@test.com", Name: "Bob", Role: "user"})
|
||||
|
||||
users, total, err := repo.List(ctx, 0, 10, "Alice", "")
|
||||
users, total, _, err := repo.List(ctx, 0, 10, "Alice", []string{}, "")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, total >= 1)
|
||||
assert.Equal(t, "Alice", users[0].Name)
|
||||
|
||||
@@ -60,9 +60,9 @@ func (m *MockTenantRepoForSvc) AddDomain(ctx context.Context, tenantID string, d
|
||||
return m.Called(ctx, tenantID, domainName, verified).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepoForSvc) List(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
|
||||
args := m.Called(ctx, limit, offset, parentID)
|
||||
return args.Get(0).([]domain.Tenant), int64(args.Int(1)), args.Error(2)
|
||||
func (m *MockTenantRepoForSvc) List(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
|
||||
args := m.Called(ctx, limit, offset, parentID, search)
|
||||
return args.Get(0).([]domain.Tenant), args.Get(1).(int64), args.Error(2)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepoForSvc) ListByType(ctx context.Context, tenantType string) ([]domain.Tenant, error) {
|
||||
@@ -135,8 +135,8 @@ func (m *MockUserRepoForTenant) ListByTenant(ctx context.Context, tenantID strin
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockUserRepoForTenant) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
|
||||
return nil, 0, nil
|
||||
func (m *MockUserRepoForTenant) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
|
||||
return nil, 0, "", nil
|
||||
}
|
||||
|
||||
func (m *MockUserRepoForTenant) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
|
||||
@@ -335,9 +335,9 @@ func TestTenantService_ListTenants(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tenants := []domain.Tenant{{ID: "t1", Name: "Tenant 1"}}
|
||||
mockRepo.On("List", ctx, 10, 0, "").Return(tenants, 1, nil)
|
||||
mockRepo.On("List", ctx, 10, 0, "", "").Return(tenants, int64(1), nil)
|
||||
|
||||
result, total, err := svc.ListTenants(ctx, 10, 0, "")
|
||||
result, total, err := svc.ListTenants(ctx, 10, 0, "", "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, tenants, result)
|
||||
|
||||
@@ -84,8 +84,8 @@ func (m *MockUserRepository) ListByTenant(ctx context.Context, tenantID string)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
|
||||
return nil, 0, nil
|
||||
func (m *MockUserRepository) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
|
||||
return nil, 0, "", nil
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
|
||||
@@ -200,7 +200,7 @@ func (m *MockTenantRepository) FindByDomain(ctx context.Context, domainName stri
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) List(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
|
||||
func (m *MockTenantRepository) List(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1978,7 +1978,7 @@ func (f *fakeWorksmobileTenantService) GetTenant(ctx context.Context, id string)
|
||||
return &tenant, nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobileTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error) {
|
||||
func (f *fakeWorksmobileTenantService) ListTenants(ctx context.Context, limit, offset int, parentID string, search string) ([]domain.Tenant, int64, error) {
|
||||
return f.list, int64(len(f.list)), nil
|
||||
}
|
||||
|
||||
@@ -2033,8 +2033,8 @@ func (f *fakeWorksmobileUserRepo) ListByTenant(ctx context.Context, tenantID str
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobileUserRepo) List(ctx context.Context, offset, limit int, search string, tenantSlug string) ([]domain.User, int64, error) {
|
||||
return nil, 0, nil
|
||||
func (r *fakeWorksmobileUserRepo) List(ctx context.Context, offset, limit int, search string, tenantIDs []string, cursor string) ([]domain.User, int64, string, error) {
|
||||
return nil, 0, "", nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobileUserRepo) CountByTenant(ctx context.Context, tenantID string) (int64, error) {
|
||||
|
||||
Reference in New Issue
Block a user