package handler import ( "baron-sso-backend/internal/domain" "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/require" ) type fakeDataIntegrityChecker struct { calls int listCalls int deleteCalls int deletedIDs []string report domain.DataIntegrityReport orphans []domain.OrphanUserLoginID deleteResult domain.DeleteOrphanUserLoginIDsResult err error } func (f *fakeDataIntegrityChecker) CheckDataIntegrity(ctx context.Context) (domain.DataIntegrityReport, error) { f.calls++ return f.report, f.err } func (f *fakeDataIntegrityChecker) ListOrphanUserLoginIDs(ctx context.Context) ([]domain.OrphanUserLoginID, error) { f.listCalls++ return f.orphans, f.err } func (f *fakeDataIntegrityChecker) DeleteOrphanUserLoginIDs(ctx context.Context, ids []string) (domain.DeleteOrphanUserLoginIDsResult, error) { f.deleteCalls++ f.deletedIDs = append([]string(nil), ids...) return f.deleteResult, f.err } func TestAdminHandler_GetDataIntegrityRequiresSuperAdmin(t *testing.T) { checker := &fakeDataIntegrityChecker{} h := &AdminHandler{IntegrityChecker: checker} app := fiber.New() app.Use(func(c *fiber.Ctx) error { c.Locals("user_profile", &domain.UserProfileResponse{ID: "tenant-admin", Role: "tenant_admin"}) return c.Next() }) app.Get("/api/v1/admin/integrity", h.GetDataIntegrity) req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/integrity", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, http.StatusForbidden, resp.StatusCode) require.Equal(t, 0, checker.calls) } func TestAdminHandler_GetDataIntegrityReturnsReportForSuperAdmin(t *testing.T) { checkedAt := time.Date(2026, 5, 14, 0, 0, 0, 0, time.UTC) checker := &fakeDataIntegrityChecker{ report: domain.DataIntegrityReport{ Status: domain.DataIntegrityStatusFail, CheckedAt: checkedAt, Summary: domain.DataIntegritySummary{ TotalChecks: 1, Failures: 1, }, Sections: []domain.DataIntegritySection{ { Key: "tenant_integrity", Label: "테넌트 정합성", Status: domain.DataIntegrityStatusFail, Checks: []domain.DataIntegrityCheck{ { Key: "duplicate_tenant_slugs", Label: "중복 테넌트 slug", Status: domain.DataIntegrityStatusFail, Count: 1, Severity: "error", }, }, }, }, }, } h := &AdminHandler{IntegrityChecker: checker} app := fiber.New() app.Use(func(c *fiber.Ctx) error { c.Locals("user_profile", &domain.UserProfileResponse{ID: "super", Role: domain.RoleSuperAdmin}) return c.Next() }) app.Get("/api/v1/admin/integrity", h.GetDataIntegrity) req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/integrity", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, 1, checker.calls) var body domain.DataIntegrityReport require.NoError(t, json.NewDecoder(resp.Body).Decode(&body)) require.Equal(t, domain.DataIntegrityStatusFail, body.Status) require.Equal(t, int64(1), body.Summary.Failures) require.Len(t, body.Sections, 1) require.Equal(t, "tenant_integrity", body.Sections[0].Key) } func TestAdminHandler_ListOrphanUserLoginIDsReturnsTargetsForSuperAdmin(t *testing.T) { checker := &fakeDataIntegrityChecker{ orphans: []domain.OrphanUserLoginID{ { ID: "login-id-1", UserID: "user-1", TenantID: "tenant-1", FieldKey: "emp_id", LoginID: "EMP001", Reasons: []string{"missing_tenant"}, }, }, } h := &AdminHandler{IntegrityChecker: checker} app := fiber.New() app.Use(func(c *fiber.Ctx) error { c.Locals("user_profile", &domain.UserProfileResponse{ID: "super", Role: domain.RoleSuperAdmin}) return c.Next() }) app.Get("/api/v1/admin/integrity/orphan-user-login-ids", h.ListOrphanUserLoginIDs) req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/integrity/orphan-user-login-ids", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, 1, checker.listCalls) var body struct { Items []domain.OrphanUserLoginID `json:"items"` Total int `json:"total"` } require.NoError(t, json.NewDecoder(resp.Body).Decode(&body)) require.Equal(t, 1, body.Total) require.Equal(t, "login-id-1", body.Items[0].ID) require.Equal(t, []string{"missing_tenant"}, body.Items[0].Reasons) } func TestAdminHandler_DeleteOrphanUserLoginIDsRequiresSuperAdminAndDeletesSelectedTargets(t *testing.T) { checker := &fakeDataIntegrityChecker{ deleteResult: domain.DeleteOrphanUserLoginIDsResult{ DeletedCount: 1, Deleted: []domain.OrphanUserLoginID{ {ID: "login-id-1", LoginID: "EMP001", Reasons: []string{"missing_user"}}, }, SkippedIDs: []string{"valid-login-id"}, }, } h := &AdminHandler{IntegrityChecker: checker} app := fiber.New() app.Use(func(c *fiber.Ctx) error { c.Locals("user_profile", &domain.UserProfileResponse{ID: "super", Role: domain.RoleSuperAdmin}) return c.Next() }) app.Delete("/api/v1/admin/integrity/orphan-user-login-ids", h.DeleteOrphanUserLoginIDs) req := httptest.NewRequest(http.MethodDelete, "/api/v1/admin/integrity/orphan-user-login-ids", strings.NewReader(`{"ids":["login-id-1","valid-login-id"]}`)) 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, 1, checker.deleteCalls) require.Equal(t, []string{"login-id-1", "valid-login-id"}, checker.deletedIDs) var body domain.DeleteOrphanUserLoginIDsResult require.NoError(t, json.NewDecoder(resp.Body).Decode(&body)) require.Equal(t, int64(1), body.DeletedCount) require.Equal(t, []string{"valid-login-id"}, body.SkippedIDs) } func TestAdminHandler_DeleteOrphanUserLoginIDsRejectsTenantAdmin(t *testing.T) { checker := &fakeDataIntegrityChecker{} h := &AdminHandler{IntegrityChecker: checker} app := fiber.New() app.Use(func(c *fiber.Ctx) error { c.Locals("user_profile", &domain.UserProfileResponse{ID: "tenant-admin", Role: "tenant_admin"}) return c.Next() }) app.Delete("/api/v1/admin/integrity/orphan-user-login-ids", h.DeleteOrphanUserLoginIDs) req := httptest.NewRequest(http.MethodDelete, "/api/v1/admin/integrity/orphan-user-login-ids", strings.NewReader(`{"ids":["login-id-1"]}`)) req.Header.Set("Content-Type", "application/json") resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, http.StatusForbidden, resp.StatusCode) require.Equal(t, 0, checker.deleteCalls) }