1
0
forked from baron/baron-sso

chore: consolidate local integration changes

This commit is contained in:
2026-06-09 21:03:05 +09:00
parent aa2848c3b6
commit 1341f07ef9
158 changed files with 10995 additions and 1490 deletions

View File

@@ -320,7 +320,8 @@ func (m *MockTenantServiceForUser) RegisterTenant(ctx context.Context, name, slu
func TestUserHandler_ExportUsersCSV_UsesTenantSlugAliasAndOmitsRole(t *testing.T) {
app := fiber.New()
mockRepo := new(MockUserRepoForHandler)
h := &UserHandler{UserRepo: mockRepo}
mockTenant := new(MockTenantServiceForUser)
h := &UserHandler{UserRepo: mockRepo, TenantService: mockTenant}
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{
@@ -332,7 +333,11 @@ 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, "", []string(nil), "").
mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{
ID: tenantID,
Slug: "test-tenant",
}, nil).Once()
mockRepo.On("List", mock.Anything, 0, 10000, "", []string{tenantID}, "").
Return([]domain.User{
{
ID: "u-1",
@@ -362,9 +367,34 @@ func TestUserHandler_ExportUsersCSV_UsesTenantSlugAliasAndOmitsRole(t *testing.T
assert.Contains(t, body, "u-1,user@test.com,Test User,010-1111-2222,active,tenant-uuid,test-tenant,책임,팀장")
assert.NotContains(t, body, "Role")
assert.NotContains(t, body, "Department")
mockTenant.AssertExpectations(t)
mockRepo.AssertExpectations(t)
}
func TestUserHandler_ExportUsersCSV_UnknownTenantSlugDoesNotFallbackToAllUsers(t *testing.T) {
app := fiber.New()
mockRepo := new(MockUserRepoForHandler)
mockTenant := new(MockTenantServiceForUser)
h := &UserHandler{UserRepo: mockRepo, TenantService: mockTenant}
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{
Role: domain.RoleSuperAdmin,
})
return c.Next()
})
app.Get("/users/export", h.ExportUsersCSV)
mockTenant.On("GetTenantBySlug", mock.Anything, "missing-tenant").Return(nil, nil).Once()
req := httptest.NewRequest("GET", "/users/export?tenantSlug=missing-tenant&includeIds=true", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
mockRepo.AssertNotCalled(t, "List", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
mockTenant.AssertExpectations(t)
}
func TestUserHandler_ExportUsersCSV_OmitsIDsAndUsesTenantSlug(t *testing.T) {
app := fiber.New()
mockRepo := new(MockUserRepoForHandler)
@@ -951,10 +981,11 @@ func TestUserHandler_BulkCreateUsers_UsesEmailDomainTenantAsPrimaryWhenExplicitT
mockOry.AssertExpectations(t)
}
func TestUserHandler_ListUsersReturnsServiceUnavailableWhenKratosFails(t *testing.T) {
func TestUserHandler_ListUsersUsesLocalProjectionWhenKratosFails(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
mockRepo := new(MockUserRepoForHandler)
createdAt := time.Date(2026, 6, 8, 6, 30, 0, 0, time.UTC)
h := &UserHandler{
KratosAdmin: mockKratos,
@@ -970,14 +1001,86 @@ func TestUserHandler_ListUsersReturnsServiceUnavailableWhenKratosFails(t *testin
app.Get("/users", h.ListUsers)
mockKratos.On("ListIdentities", mock.Anything).Return([]service.KratosIdentity{}, errors.New("kratos down")).Maybe()
mockRepo.On("List", mock.Anything, 0, 10, "", []string(nil), "").Return([]domain.User{
{
ID: "local-user-1",
Email: "local1@example.com",
Name: "Local One",
Role: domain.RoleUser,
Status: domain.UserStatusActive,
CreatedAt: createdAt,
UpdatedAt: createdAt,
},
}, int64(1), "", nil)
req := httptest.NewRequest("GET", "/users?limit=10&offset=0", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
mockRepo.AssertNotCalled(t, "List", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
mockKratos.AssertExpectations(t)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var res userListResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&res))
require.Equal(t, int64(1), res.Total)
require.Len(t, res.Items, 1)
require.Equal(t, "local1@example.com", res.Items[0].Email)
mockRepo.AssertExpectations(t)
}
func TestUserHandler_ListUsersUsesLocalProjectionTotalBeyondKratosPageLimit(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
mockRepo := new(MockUserRepoForHandler)
createdAt := time.Date(2026, 6, 8, 6, 40, 0, 0, time.UTC)
h := &UserHandler{
KratosAdmin: mockKratos,
UserRepo: mockRepo,
}
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{
Role: domain.RoleSuperAdmin,
})
return c.Next()
})
app.Get("/users", h.ListUsers)
kratosIdentities := make([]service.KratosIdentity, 250)
for i := range kratosIdentities {
kratosIdentities[i] = service.KratosIdentity{
ID: "kratos-user",
State: "active",
CreatedAt: createdAt.Add(-time.Duration(i) * time.Second),
Traits: map[string]any{"email": "kratos@example.com", "name": "Kratos"},
}
}
mockKratos.On("ListIdentities", mock.Anything).Return(kratosIdentities, nil).Maybe()
mockRepo.On("List", mock.Anything, 0, 50, "", []string(nil), "").Return([]domain.User{
{
ID: "local-user-1",
Email: "local1@example.com",
Name: "Local One",
Role: domain.RoleUser,
Status: domain.UserStatusActive,
CreatedAt: createdAt,
UpdatedAt: createdAt,
},
}, int64(2114), "next-local-cursor", nil)
req := httptest.NewRequest("GET", "/users?limit=50&offset=0", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
var res userListResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&res))
require.Equal(t, int64(2114), res.Total)
require.Len(t, res.Items, 1)
require.Equal(t, "local1@example.com", res.Items[0].Email)
require.Equal(t, "next-local-cursor", res.NextCursor)
mockRepo.AssertExpectations(t)
}
func TestUserHandler_ListUsersReturnsNextCursorWhenMoreRowsExist(t *testing.T) {
@@ -2363,6 +2466,157 @@ func TestUserHandler_UpdateUserAcceptsTenantSlugAndRejectsCompanyCode(t *testing
mockKratos.AssertExpectations(t)
}
func TestUserHandler_UpdateUserAddTenantKeepsPrimaryAndAddsAppointment(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
mockTenant := new(MockTenantServiceForUser)
h := &UserHandler{
KratosAdmin: mockKratos,
TenantService: mockTenant,
}
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{
ID: "admin-id",
Role: domain.RoleSuperAdmin,
})
return c.Next()
})
app.Put("/users/:id", h.UpdateUser)
mockKratos.On("GetIdentity", mock.Anything, "user-id").Return(&service.KratosIdentity{
ID: "user-id",
State: "active",
Traits: map[string]any{
"email": "user@test.com",
"name": "Test User",
"tenant_id": "primary-tenant-id",
"role": domain.RoleUser,
"additionalAppointments": []any{
map[string]any{
"tenantId": "primary-tenant-id",
"tenantSlug": "primary-tenant",
"tenantName": "대표 조직",
"isPrimary": true,
},
},
},
}, nil)
mockTenant.On("GetTenantBySlug", mock.Anything, "private-team").Return(&domain.Tenant{
ID: "private-team-id",
Name: "비공개 팀",
Slug: "private-team",
Config: domain.JSONMap{
"visibility": "private",
},
}, nil)
mockTenant.On("GetTenant", mock.Anything, "private-team-id").Return(&domain.Tenant{
ID: "private-team-id",
Name: "비공개 팀",
Slug: "private-team",
Config: domain.JSONMap{
"visibility": "private",
},
}, nil).Maybe()
mockTenant.On("GetTenant", mock.Anything, "primary-tenant-id").Return(&domain.Tenant{
ID: "primary-tenant-id",
Name: "대표 조직",
Slug: "primary-tenant",
}, nil).Maybe()
var capturedTraits map[string]any
mockKratos.On("UpdateIdentity", mock.Anything, "user-id", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
capturedTraits = args.Get(2).(map[string]any)
}).Return(&service.KratosIdentity{
ID: "user-id",
State: "active",
Traits: map[string]any{
"email": "user@test.com",
"name": "Test User",
"tenant_id": "primary-tenant-id",
"role": domain.RoleUser,
"additionalAppointments": []any{
map[string]any{
"tenantId": "primary-tenant-id",
"tenantSlug": "primary-tenant",
"tenantName": "대표 조직",
"isPrimary": true,
},
map[string]any{
"tenantId": "private-team-id",
"tenantSlug": "private-team",
"tenantName": "비공개 팀",
"isPrimary": false,
},
},
},
}, nil)
body := `{"tenantSlug":"private-team","isAddTenant":true}`
req := httptest.NewRequest(http.MethodPut, "/users/user-id", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, "primary-tenant-id", capturedTraits["tenant_id"])
appointments, ok := capturedTraits["additionalAppointments"].([]any)
require.True(t, ok)
require.Len(t, appointments, 2)
added := appointments[1].(map[string]any)
require.Equal(t, "private-team-id", added["tenantId"])
require.Equal(t, "private-team", added["tenantSlug"])
require.Equal(t, "비공개 팀", added["tenantName"])
require.Equal(t, false, added["isPrimary"])
}
func TestUserHandler_UpdateUserAddTenantRejectsUnmanageableTenantForTenantAdmin(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
mockTenant := new(MockTenantServiceForUser)
allowedTenantID := "allowed-tenant-id"
h := &UserHandler{
KratosAdmin: mockKratos,
TenantService: mockTenant,
}
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{
ID: "tenant-admin-id",
Role: "tenant_admin",
ManageableTenants: []domain.Tenant{
{ID: allowedTenantID, Slug: "allowed-team"},
},
})
return c.Next()
})
app.Put("/users/:id", h.UpdateUser)
mockKratos.On("GetIdentity", mock.Anything, "user-id").Return(&service.KratosIdentity{
ID: "user-id",
State: "active",
Traits: map[string]any{
"email": "user@test.com",
"name": "Test User",
"tenant_id": allowedTenantID,
"role": domain.RoleUser,
},
}, nil)
mockTenant.On("GetTenantBySlug", mock.Anything, "outside-team").Return(&domain.Tenant{
ID: "outside-tenant-id",
Name: "관리 외부 팀",
Slug: "outside-team",
}, nil).Once()
body := `{"tenantSlug":"outside-team","isAddTenant":true}`
req := httptest.NewRequest(http.MethodPut, "/users/user-id", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
mockKratos.AssertNotCalled(t, "UpdateIdentity", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
mockTenant.AssertExpectations(t)
}
func TestUserHandler_BulkUpdateUsersAcceptsTenantSlugAndRejectsCompanyCode(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)