1
0
forked from baron/baron-sso

chore: snapshot local state before dev merge

This commit is contained in:
2026-06-17 21:25:42 +09:00
parent b2808759d2
commit 49560e8a8c
107 changed files with 8958 additions and 939 deletions

View File

@@ -993,6 +993,22 @@ func TestUserHandler_BulkCreateUsers_ResolvesAdditionalAppointment(t *testing.T)
mockOry.AssertExpectations(t)
}
func TestApplyTenantBoundGradeNormalizesDirectorLevelNames(t *testing.T) {
tenant := &domain.Tenant{ID: "tenant-1", Slug: "tenant", Name: "Tenant"}
metadata := applyTenantBoundGrade(nil, tenant, "상무")
appointments := userAppointmentSliceFromRaw(metadata["additionalAppointments"])
require.Len(t, appointments, 1)
appointment := appointments[0].(map[string]any)
require.Equal(t, "상무이사", appointment["grade"])
metadata = applyTenantBoundGrade(metadata, tenant, "전무")
appointments = userAppointmentSliceFromRaw(metadata["additionalAppointments"])
require.Len(t, appointments, 1)
appointment = appointments[0].(map[string]any)
require.Equal(t, "전무이사", appointment["grade"])
}
func TestUserHandler_BulkCreateUsers_AppendsEmailDomainTenantAtLowestPriority(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
@@ -1098,9 +1114,17 @@ func TestUserHandler_BulkCreateUsers_UsesEmailDomainTenantAsPrimaryWhenExplicitT
type identityMirrorRedisStub struct {
mockRedisRepo
pageCalls int
fullCalls int
failFull bool
lastQuery service.IdentityMirrorPageQuery
}
func (s *identityMirrorRedisStub) ListIdentityMirrors(ctx context.Context) ([]service.KratosIdentity, error) {
s.fullCalls++
if s.failFull {
return nil, errors.New("full identity mirror materialization is forbidden")
}
identities := make([]service.KratosIdentity, 0, len(s.data))
for key, raw := range s.data {
if !strings.HasPrefix(key, "identity:mirror:") || key == "identity:mirror:state" {
@@ -1118,6 +1142,35 @@ func (s *identityMirrorRedisStub) ListIdentityMirrors(ctx context.Context) ([]se
return identities, nil
}
func (s *identityMirrorRedisStub) ListIdentityMirrorPage(ctx context.Context, query service.IdentityMirrorPageQuery) (service.IdentityMirrorPageResult, error) {
s.pageCalls++
s.lastQuery = query
identities := make([]service.KratosIdentity, 0, len(s.data))
for key, raw := range s.data {
if !strings.HasPrefix(key, "identity:mirror:") || key == "identity:mirror:state" {
continue
}
var identity service.KratosIdentity
if err := json.Unmarshal([]byte(raw), &identity); err != nil {
continue
}
if strings.TrimSpace(identity.ID) == "" {
continue
}
identities = append(identities, identity)
}
return pageIdentityMirrorSlice(identities, query)
}
func (s *identityMirrorRedisStub) StoreIdentityMirror(ctx context.Context, identity service.KratosIdentity) error {
raw, err := json.Marshal(identity)
if err != nil {
return err
}
s.data[identityMirrorKey(identity.ID)] = string(raw)
return nil
}
func (s *identityMirrorRedisStub) GetIdentityCacheStatus(ctx context.Context) (domain.IdentityCacheStatus, error) {
raw := s.data["identity:mirror:state"]
if strings.TrimSpace(raw) == "" {
@@ -1174,7 +1227,7 @@ func TestUserHandler_ListUsersUsesIdentityMirrorAndDoesNotUseUserRepo(t *testing
h := &UserHandler{
KratosAdmin: mockKratos,
UserRepo: mockRepo,
IdentityCache: &identityMirrorRedisStub{mockRedisRepo{data: map[string]string{
IdentityCache: &identityMirrorRedisStub{mockRedisRepo: mockRedisRepo{data: map[string]string{
identityMirrorKey(mirrorIdentity.ID): string(rawMirrorIdentity),
"identity:mirror:state": string(rawState),
}}},
@@ -1200,14 +1253,71 @@ func TestUserHandler_ListUsersUsesIdentityMirrorAndDoesNotUseUserRepo(t *testing
require.Len(t, res.Items, 1)
require.Equal(t, "mirror-user-1", res.Items[0].ID)
require.Equal(t, "mirror1@example.com", res.Items[0].Email)
cache := h.IdentityCache.(*identityMirrorRedisStub)
require.Equal(t, 1, cache.pageCalls)
require.Equal(t, 0, cache.fullCalls)
mockRepo.AssertNotCalled(t, "List", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
mockKratos.AssertNotCalled(t, "ListIdentities", mock.Anything)
}
func TestUserHandler_ListUsersPassesQueryToIdentityMirrorPageInsteadOfLoadingFullMirror(t *testing.T) {
app := fiber.New()
createdAt := time.Date(2026, 6, 17, 8, 50, 0, 0, time.UTC)
identities := []service.KratosIdentity{
{ID: "user-new", State: "active", CreatedAt: createdAt.Add(2 * time.Minute), UpdatedAt: createdAt, Traits: map[string]any{"email": "new@example.com", "name": "New User"}},
{ID: "user-needle", State: "active", CreatedAt: createdAt.Add(time.Minute), UpdatedAt: createdAt, Traits: map[string]any{"email": "needle@example.com", "name": "Needle User"}},
{ID: "user-old", State: "active", CreatedAt: createdAt, UpdatedAt: createdAt, Traits: map[string]any{"email": "old@example.com", "name": "Old User"}},
}
data := map[string]string{}
for _, identity := range identities {
raw, err := json.Marshal(identity)
require.NoError(t, err)
data[identityMirrorKey(identity.ID)] = string(raw)
}
state := domain.IdentityCacheStatus{
Status: "ready",
RedisReady: true,
MirrorVersion: identityMirrorVersion,
ObservedCount: int64(len(identities)),
}
rawState, err := json.Marshal(state)
require.NoError(t, err)
data["identity:mirror:state"] = string(rawState)
cache := &identityMirrorRedisStub{
mockRedisRepo: mockRedisRepo{data: data},
failFull: true,
}
h := &UserHandler{
KratosAdmin: new(MockKratosAdmin),
IdentityCache: cache,
}
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleSuperAdmin})
return c.Next()
})
app.Get("/users", h.ListUsers)
req := httptest.NewRequest("GET", "/users?limit=1&search=needle", 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.Len(t, res.Items, 1)
require.Equal(t, "user-needle", res.Items[0].ID)
require.Equal(t, 1, cache.pageCalls)
require.Equal(t, 0, cache.fullCalls)
require.Equal(t, 1, cache.lastQuery.Limit)
require.Equal(t, "needle", cache.lastQuery.Search)
}
func TestUserHandler_ListUsersWarmsIdentityMirrorFromKratosWhenMirrorEmpty(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
redis := &identityMirrorRedisStub{mockRedisRepo{data: map[string]string{}}}
redis := &identityMirrorRedisStub{mockRedisRepo: mockRedisRepo{data: map[string]string{}}}
createdAt := time.Date(2026, 6, 8, 6, 40, 0, 0, time.UTC)
h := &UserHandler{
@@ -1330,7 +1440,7 @@ func TestUserHandler_ListUsersTenantSlugFilterIncludesAdditionalAppointments(t *
func TestUserHandler_WarmIdentityMirrorRebuildsRedisFromKratos(t *testing.T) {
mockKratos := new(MockKratosAdmin)
redis := &identityMirrorRedisStub{mockRedisRepo{data: map[string]string{
redis := &identityMirrorRedisStub{mockRedisRepo: mockRedisRepo{data: map[string]string{
identityMirrorKey("stale-user"): `{"id":"stale-user"}`,
}}}
createdAt := time.Date(2026, 6, 12, 18, 30, 0, 0, time.UTC)
@@ -1382,7 +1492,7 @@ func TestUserHandler_ListUsersRebuildsLegacyReadyMirrorWithoutVersion(t *testing
}
rawLegacyState, err := json.Marshal(legacyState)
require.NoError(t, err)
redis := &identityMirrorRedisStub{mockRedisRepo{data: map[string]string{
redis := &identityMirrorRedisStub{mockRedisRepo: mockRedisRepo{data: map[string]string{
identityMirrorKey(legacyIdentity.ID): string(rawLegacyIdentity),
"identity:mirror:state": string(rawLegacyState),
}}}
@@ -1436,7 +1546,7 @@ func TestUserHandler_ListUsersRebuildsPartialMirrorFromKratos(t *testing.T) {
}
rawPartialIdentity, err := json.Marshal(partialIdentity)
require.NoError(t, err)
redis := &identityMirrorRedisStub{mockRedisRepo{data: map[string]string{
redis := &identityMirrorRedisStub{mockRedisRepo: mockRedisRepo{data: map[string]string{
identityMirrorKey(partialIdentity.ID): string(rawPartialIdentity),
}}}
kratosIdentities := []service.KratosIdentity{
@@ -3946,18 +4056,24 @@ func TestUserHandler_BulkUpdateUsersRejectsInternalDomainMoveToPersonalTenant(t
mockTenant.AssertExpectations(t)
}
func TestUserHandler_MapToLocalUserKeepsRoleAndGradeSeparate(t *testing.T) {
func TestUserHandler_MapToLocalUserPreservesTenantBoundGradeForCompatibility(t *testing.T) {
handler := &UserHandler{}
identity := service.KratosIdentity{
ID: "user-grade-id",
State: "active",
Traits: map[string]any{
"email": "grade@example.com",
"name": "Grade User",
"role": domain.RoleUser,
"grade": "수석",
"position": "팀장",
"companyCode": "hanmac",
"email": "grade@example.com",
"name": "Grade User",
"role": domain.RoleUser,
"grade": "수석",
"position": "팀장",
"tenant_id": "tenant-1",
"additionalAppointments": []any{
map[string]any{
"tenantId": "tenant-1",
"grade": "수석",
},
},
},
}
@@ -3967,6 +4083,7 @@ func TestUserHandler_MapToLocalUserKeepsRoleAndGradeSeparate(t *testing.T) {
assert.Equal(t, "수석", localUser.Grade)
assert.Equal(t, "팀장", localUser.Position)
assert.NotContains(t, localUser.Metadata, "grade")
assert.Contains(t, localUser.Metadata, "additionalAppointments")
}
func (m *MockKratosAdmin) CreateUser(ctx context.Context, user *domain.BrokerUser, password string) (string, error) {