package handler import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/service" "bytes" "context" "errors" "io" "log/slog" "net/http/httptest" "strings" "testing" "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/require" ) func TestWorksmobileHandlerRejectsNonHanmacTenant(t *testing.T) { h := NewWorksmobileHandler(&fakeWorksmobileAdminService{ overview: service.WorksmobileTenantOverview{ Tenant: domain.Tenant{ID: "tenant-1", Slug: "other"}, }, }) app := fiber.New() app.Get("/tenants/:tenantId/worksmobile", h.GetOverview) resp, err := app.Test(httptest.NewRequest("GET", "/tenants/tenant-1/worksmobile", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } func TestWorksmobileHandlerReturnsOverviewForHanmacTenant(t *testing.T) { h := NewWorksmobileHandler(&fakeWorksmobileAdminService{ overview: service.WorksmobileTenantOverview{ Tenant: domain.Tenant{ID: "hanmac-id", Slug: "hanmac-family"}, Config: service.WorksmobileConfigSummary{ Enabled: true, }, }, }) app := fiber.New() app.Get("/tenants/:tenantId/worksmobile", h.GetOverview) resp, err := app.Test(httptest.NewRequest("GET", "/tenants/hanmac-id/worksmobile", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) } func TestWorksmobileHandlerDownloadsInitialPasswordCSV(t *testing.T) { h := NewWorksmobileHandler(&fakeWorksmobileAdminService{ credentials: []service.WorksmobileInitialPasswordCredential{ { Email: "user@hanmaceng.co.kr", Name: "홍길동", PrimaryLeafOrgName: "인재성장", InitialPassword: "Aa1!Aa1!Aa1!Aa1!", Status: "processed", }, }, }) app := fiber.New() app.Get("/tenants/:tenantId/worksmobile/initial-passwords.csv", h.DownloadInitialPasswordsCSV) resp, err := app.Test(httptest.NewRequest("GET", "/tenants/hanmac-id/worksmobile/initial-passwords.csv", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Contains(t, resp.Header.Get("Content-Disposition"), "worksmobile_initial_passwords.csv") body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Contains(t, string(body), "email,name,primaryLeafOrgName,initialPassword,status,lastError") require.Contains(t, string(body), "user@hanmaceng.co.kr,홍길동,인재성장,Aa1!Aa1!Aa1!Aa1!,processed,") } func TestWorksmobileHandlerPassesInitialPasswordBatchID(t *testing.T) { fakeService := &fakeWorksmobileAdminService{ credentials: []service.WorksmobileInitialPasswordCredential{ {Email: "batch-user@hanmaceng.co.kr", InitialPassword: "BatchPass1!", Status: "pending"}, }, } h := NewWorksmobileHandler(fakeService) app := fiber.New() app.Get("/tenants/:tenantId/worksmobile/initial-passwords.csv", h.DownloadInitialPasswordsCSV) resp, err := app.Test(httptest.NewRequest("GET", "/tenants/hanmac-id/worksmobile/initial-passwords.csv?batchId=batch-1", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Equal(t, "batch-1", fakeService.downloadCredentialBatchID) } func TestWorksmobileHandlerPassesSyncUserCredentialBatchID(t *testing.T) { fakeService := &fakeWorksmobileAdminService{} h := NewWorksmobileHandler(fakeService) app := fiber.New() app.Post("/tenants/:tenantId/worksmobile/users/:userId/sync", h.SyncUser) req := httptest.NewRequest("POST", "/tenants/hanmac-id/worksmobile/users/user-1/sync", strings.NewReader(`{"credentialBatchId":"batch-1","initialPassword":"InputPass1!"}`)) req.Header.Set("Content-Type", "application/json") resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusAccepted, resp.StatusCode) require.Equal(t, "batch-1", fakeService.syncUserCredentialBatchID) require.Equal(t, "InputPass1!", fakeService.syncUserInitialPassword) } func TestWorksmobileHandlerPassesPasswordResetCredentialBatchID(t *testing.T) { fakeService := &fakeWorksmobileAdminService{} h := NewWorksmobileHandler(fakeService) app := fiber.New() app.Post("/tenants/:tenantId/worksmobile/users/:userId/password/reset", h.ResetUserPassword) req := httptest.NewRequest("POST", "/tenants/hanmac-id/worksmobile/users/user-1/password/reset", strings.NewReader(`{"credentialBatchId":"batch-1"}`)) req.Header.Set("Content-Type", "application/json") resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusAccepted, resp.StatusCode) require.Equal(t, "batch-1", fakeService.resetPasswordCredentialBatchID) } func TestWorksmobileHandlerReturnsCredentialBatchHistory(t *testing.T) { h := NewWorksmobileHandler(&fakeWorksmobileAdminService{ credentialBatches: []service.WorksmobileCredentialBatch{ {BatchID: "batch-1", UserCount: 2, HasPasswords: true}, }, }) app := fiber.New() app.Get("/tenants/:tenantId/worksmobile/credential-batches", h.ListCredentialBatches) resp, err := app.Test(httptest.NewRequest("GET", "/tenants/hanmac-id/worksmobile/credential-batches", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Contains(t, string(body), `"batchId":"batch-1"`) require.Contains(t, string(body), `"userCount":2`) } func TestWorksmobileHandlerDeletesCredentialBatchPasswords(t *testing.T) { fakeService := &fakeWorksmobileAdminService{} h := NewWorksmobileHandler(fakeService) app := fiber.New() app.Delete("/tenants/:tenantId/worksmobile/credential-batches/:batchId/passwords", h.DeleteCredentialBatchPasswords) resp, err := app.Test(httptest.NewRequest("DELETE", "/tenants/hanmac-id/worksmobile/credential-batches/batch-1/passwords", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Equal(t, "batch-1", fakeService.deletedCredentialBatchID) } func TestWorksmobileHandlerDeletesPendingJobs(t *testing.T) { fakeService := &fakeWorksmobileAdminService{ pendingJobsDeleteResult: service.WorksmobilePendingJobDeleteResult{DeletedCount: 3}, } h := NewWorksmobileHandler(fakeService) app := fiber.New() app.Delete("/tenants/:tenantId/worksmobile/jobs/pending", h.DeletePendingJobs) resp, err := app.Test(httptest.NewRequest("DELETE", "/tenants/hanmac-id/worksmobile/jobs/pending", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Equal(t, "hanmac-id", fakeService.deletedPendingJobsTenantID) body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Contains(t, string(body), `"deletedCount":3`) } func TestWorksmobileHandlerLogsActionFailures(t *testing.T) { var logs bytes.Buffer previous := slog.Default() slog.SetDefault(slog.New(slog.NewJSONHandler(&logs, nil))) t.Cleanup(func() { slog.SetDefault(previous) }) h := NewWorksmobileHandler(&fakeWorksmobileAdminService{ syncUserErr: errors.New("works user sync failed"), }) app := fiber.New() app.Post("/tenants/:tenantId/worksmobile/users/:userId/sync", h.SyncUser) resp, err := app.Test(httptest.NewRequest("POST", "/tenants/hanmac-id/worksmobile/users/user-1/sync", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) require.Contains(t, logs.String(), "worksmobile admin operation failed") require.Contains(t, logs.String(), "sync_user") require.Contains(t, logs.String(), "works user sync failed") } func TestWorksmobileHandlerReturnsBadRequestForOutOfScopeUserSync(t *testing.T) { h := NewWorksmobileHandler(&fakeWorksmobileAdminService{ syncUserErr: errors.New("target user tenant is excluded from Worksmobile sync"), }) app := fiber.New() app.Post("/tenants/:tenantId/worksmobile/users/:userId/sync", h.SyncUser) resp, err := app.Test(httptest.NewRequest("POST", "/tenants/hanmac-id/worksmobile/users/user-1/sync", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusBadRequest, resp.StatusCode) } type fakeWorksmobileAdminService struct { overview service.WorksmobileTenantOverview credentials []service.WorksmobileInitialPasswordCredential syncUserErr error syncUserCredentialBatchID string syncUserInitialPassword string resetPasswordCredentialBatchID string downloadCredentialBatchID string deletedCredentialBatchID string deletedPendingJobsTenantID string pendingJobsDeleteResult service.WorksmobilePendingJobDeleteResult credentialBatches []service.WorksmobileCredentialBatch } func (f *fakeWorksmobileAdminService) GetTenantOverview(ctx context.Context, tenantID string) (service.WorksmobileTenantOverview, error) { return f.overview, nil } func (f *fakeWorksmobileAdminService) GetComparison(ctx context.Context, tenantID string, includeMatched bool) (service.WorksmobileComparison, error) { return service.WorksmobileComparison{}, nil } func (f *fakeWorksmobileAdminService) EnqueueBackfillDryRun(ctx context.Context, tenantID string) (service.WorksmobileBackfillDryRun, error) { return service.WorksmobileBackfillDryRun{}, nil } func (f *fakeWorksmobileAdminService) EnqueueOrgUnitSync(ctx context.Context, tenantID, orgUnitID string) (*domain.WorksmobileOutbox, error) { return &domain.WorksmobileOutbox{ID: "job-orgunit", ResourceID: orgUnitID}, nil } func (f *fakeWorksmobileAdminService) EnqueueOrgUnitDelete(ctx context.Context, tenantID, orgUnitID string) (*domain.WorksmobileOutbox, error) { return &domain.WorksmobileOutbox{ID: "job-orgunit-delete", ResourceID: orgUnitID, Action: domain.WorksmobileActionDelete}, nil } func (f *fakeWorksmobileAdminService) EnqueueUserSync(ctx context.Context, tenantID, userID, credentialBatchID, initialPassword string) (*domain.WorksmobileOutbox, error) { f.syncUserCredentialBatchID = credentialBatchID f.syncUserInitialPassword = initialPassword if f.syncUserErr != nil { return nil, f.syncUserErr } return &domain.WorksmobileOutbox{ID: "job-user", ResourceID: userID}, nil } func (f *fakeWorksmobileAdminService) EnqueueUserPasswordReset(ctx context.Context, tenantID, userID, credentialBatchID string) (*domain.WorksmobileOutbox, error) { f.resetPasswordCredentialBatchID = credentialBatchID return &domain.WorksmobileOutbox{ID: "job-user-password-reset", ResourceID: userID}, nil } func (f *fakeWorksmobileAdminService) RetryJob(ctx context.Context, tenantID, jobID string) (*domain.WorksmobileOutbox, error) { return &domain.WorksmobileOutbox{ID: jobID}, nil } func (f *fakeWorksmobileAdminService) ListInitialPasswordCredentials(ctx context.Context, tenantID, credentialBatchID string) ([]service.WorksmobileInitialPasswordCredential, error) { f.downloadCredentialBatchID = credentialBatchID return f.credentials, nil } func (f *fakeWorksmobileAdminService) ListCredentialBatches(ctx context.Context, tenantID string) ([]service.WorksmobileCredentialBatch, error) { return f.credentialBatches, nil } func (f *fakeWorksmobileAdminService) DeleteCredentialBatchPasswords(ctx context.Context, tenantID, credentialBatchID string) (service.WorksmobileCredentialBatch, error) { f.deletedCredentialBatchID = credentialBatchID return service.WorksmobileCredentialBatch{BatchID: credentialBatchID}, nil } func (f *fakeWorksmobileAdminService) DeletePendingJobs(ctx context.Context, tenantID string) (service.WorksmobilePendingJobDeleteResult, error) { f.deletedPendingJobsTenantID = tenantID return f.pendingJobsDeleteResult, nil }