From 6cbece048b66861dad60415c83cea677c2cf92c9 Mon Sep 17 00:00:00 2001 From: Brian Ojeda <9335829+sgtoj@users.noreply.github.com> Date: Sun, 7 Dec 2025 08:34:24 -0500 Subject: [PATCH] feat: add endpoint to trigger test messages --- internal/app/app.go | 99 ++++++++++++++++++++++- internal/app/app_test.go | 171 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 internal/app/app_test.go diff --git a/internal/app/app.go b/internal/app/app.go index 6d9b1d2..d13c66f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -13,6 +13,7 @@ import ( "github.com/cruxstack/github-ops-app/internal/github" "github.com/cruxstack/github-ops-app/internal/notifiers" "github.com/cruxstack/github-ops-app/internal/okta" + gh "github.com/google/go-github/v79/github" ) // App is the main application instance containing all clients and @@ -86,11 +87,14 @@ func (a *App) ProcessScheduledEvent(ctx context.Context, evt ScheduledEvent) err a.Logger.Debug("received scheduled event", slog.String("event", string(j))) } - if evt.Action == "okta-sync" { + switch evt.Action { + case "okta-sync": return a.handleOktaSync(ctx) + case "slack-test": + return a.handleSlackTest(ctx) + default: + return errors.Newf("unknown scheduled action: %s", evt.Action) } - - return errors.Newf("unknown scheduled action: %s", evt.Action) } // ProcessWebhook handles incoming GitHub webhook events. @@ -349,6 +353,95 @@ func (a *App) shouldIgnoreMembershipChange(ctx context.Context, event *github.Me return false } +// handleSlackTest sends test notifications to Slack with sample data. +// useful for verifying Slack connectivity and previewing message formats. +func (a *App) handleSlackTest(ctx context.Context) error { + if a.Notifier == nil { + return errors.New("slack is not configured") + } + + // test 1: PR bypass notification + if err := a.Notifier.NotifyPRBypass(ctx, fakePRComplianceResult(), "acme-corp/demo-repo"); err != nil { + return errors.Wrap(err, "failed to send test pr bypass notification") + } + a.Logger.Info("sent test pr bypass notification") + + // test 2: Okta sync notification + if err := a.Notifier.NotifyOktaSync(ctx, fakeOktaSyncReports(), "acme-corp"); err != nil { + return errors.Wrap(err, "failed to send test okta sync notification") + } + a.Logger.Info("sent test okta sync notification") + + // test 3: Orphaned users notification + if err := a.Notifier.NotifyOrphanedUsers(ctx, fakeOrphanedUsersReport()); err != nil { + return errors.Wrap(err, "failed to send test orphaned users notification") + } + a.Logger.Info("sent test orphaned users notification") + + return nil +} + +// fakePRComplianceResult returns sample PR compliance data for testing. +func fakePRComplianceResult() *github.PRComplianceResult { + prNumber := 42 + prTitle := "Add new authentication feature" + prURL := "https://github.com/acme-corp/demo-repo/pull/42" + mergedByLogin := "test-user" + + return &github.PRComplianceResult{ + PR: &gh.PullRequest{ + Number: &prNumber, + Title: &prTitle, + HTMLURL: &prURL, + MergedBy: &gh.User{ + Login: &mergedByLogin, + }, + }, + BaseBranch: "main", + UserHasBypass: true, + UserBypassReason: "repository admin", + Violations: []github.ComplianceViolation{ + {Type: "insufficient_reviews", Description: "required 2 approving reviews, had 0"}, + {Type: "missing_status_check", Description: "required check 'ci/build' did not pass"}, + }, + } +} + +// fakeOktaSyncReports returns sample Okta sync reports for testing. +func fakeOktaSyncReports() []*okta.SyncReport { + return []*okta.SyncReport{ + { + Rule: "engineering-team", + OktaGroup: "Engineering", + GitHubTeam: "engineering", + MembersAdded: []string{"alice", "bob"}, + MembersRemoved: []string{"charlie"}, + }, + { + Rule: "platform-team", + OktaGroup: "Platform", + GitHubTeam: "platform", + // no changes + }, + { + Rule: "security-team", + OktaGroup: "Security", + GitHubTeam: "security", + MembersAdded: []string{"dave"}, + MembersSkippedExternal: []string{"external-contractor"}, + MembersSkippedNoGHUsername: []string{"new-hire@example.com"}, + Errors: []string{"failed to fetch group members: rate limited"}, + }, + } +} + +// fakeOrphanedUsersReport returns sample orphaned users data for testing. +func fakeOrphanedUsersReport() *okta.OrphanedUsersReport { + return &okta.OrphanedUsersReport{ + OrphanedUsers: []string{"orphan-user-1", "orphan-user-2", "legacy-bot"}, + } +} + // StatusResponse contains application status and feature flags. type StatusResponse struct { Status string `json:"status"` diff --git a/internal/app/app_test.go b/internal/app/app_test.go new file mode 100644 index 0000000..f76247c --- /dev/null +++ b/internal/app/app_test.go @@ -0,0 +1,171 @@ +package app + +import ( + "context" + "log/slog" + "os" + "testing" + + "github.com/cruxstack/github-ops-app/internal/config" + "github.com/cruxstack/github-ops-app/internal/github" + "github.com/cruxstack/github-ops-app/internal/okta" +) + +func TestHandleSlackTest_NotConfigured(t *testing.T) { + app := &App{ + Config: &config.Config{}, + Logger: slog.New(slog.NewTextHandler(os.Stderr, nil)), + Notifier: nil, + } + + err := app.handleSlackTest(context.Background()) + if err == nil { + t.Error("expected error when slack is not configured") + } + + if err.Error() != "slack is not configured" { + t.Errorf("expected 'slack is not configured' error, got: %v", err) + } +} + +func TestFakePRComplianceResult(t *testing.T) { + result := fakePRComplianceResult() + + if result == nil { + t.Fatal("expected non-nil result") + } + + if result.PR == nil { + t.Fatal("expected non-nil PR") + } + + if result.PR.Number == nil || *result.PR.Number != 42 { + t.Error("expected PR number to be 42") + } + + if result.PR.Title == nil || *result.PR.Title != "Add new authentication feature" { + t.Error("expected PR title to be 'Add new authentication feature'") + } + + if result.BaseBranch != "main" { + t.Errorf("expected base branch 'main', got %q", result.BaseBranch) + } + + if !result.UserHasBypass { + t.Error("expected UserHasBypass to be true") + } + + if result.UserBypassReason != "repository admin" { + t.Errorf("expected bypass reason 'repository admin', got %q", result.UserBypassReason) + } + + if len(result.Violations) != 2 { + t.Errorf("expected 2 violations, got %d", len(result.Violations)) + } + + // verify it passes the WasBypassed check (used by notifier) + if !result.WasBypassed() { + t.Error("expected WasBypassed() to return true") + } +} + +func TestFakeOktaSyncReports(t *testing.T) { + reports := fakeOktaSyncReports() + + if len(reports) != 3 { + t.Fatalf("expected 3 reports, got %d", len(reports)) + } + + // first report: has changes + if !reports[0].HasChanges() { + t.Error("expected first report to have changes") + } + if len(reports[0].MembersAdded) != 2 { + t.Errorf("expected 2 members added, got %d", len(reports[0].MembersAdded)) + } + if len(reports[0].MembersRemoved) != 1 { + t.Errorf("expected 1 member removed, got %d", len(reports[0].MembersRemoved)) + } + + // second report: no changes + if reports[1].HasChanges() { + t.Error("expected second report to have no changes") + } + + // third report: has changes, errors, and skipped members + if !reports[2].HasChanges() { + t.Error("expected third report to have changes") + } + if !reports[2].HasErrors() { + t.Error("expected third report to have errors") + } + if len(reports[2].MembersSkippedExternal) != 1 { + t.Errorf("expected 1 skipped external member, got %d", + len(reports[2].MembersSkippedExternal)) + } + if len(reports[2].MembersSkippedNoGHUsername) != 1 { + t.Errorf("expected 1 skipped member without GH username, got %d", + len(reports[2].MembersSkippedNoGHUsername)) + } +} + +func TestFakeOrphanedUsersReport(t *testing.T) { + report := fakeOrphanedUsersReport() + + if report == nil { + t.Fatal("expected non-nil report") + } + + if len(report.OrphanedUsers) != 3 { + t.Errorf("expected 3 orphaned users, got %d", len(report.OrphanedUsers)) + } + + expectedUsers := []string{"orphan-user-1", "orphan-user-2", "legacy-bot"} + for i, user := range report.OrphanedUsers { + if user != expectedUsers[i] { + t.Errorf("expected user %q at index %d, got %q", expectedUsers[i], i, user) + } + } +} + +func TestProcessScheduledEvent_SlackTest(t *testing.T) { + app := &App{ + Config: &config.Config{}, + Logger: slog.New(slog.NewTextHandler(os.Stderr, nil)), + Notifier: nil, + } + + evt := ScheduledEvent{Action: "slack-test"} + err := app.ProcessScheduledEvent(context.Background(), evt) + + // should fail because slack is not configured + if err == nil { + t.Error("expected error when slack is not configured") + } +} + +func TestProcessScheduledEvent_UnknownAction(t *testing.T) { + app := &App{ + Config: &config.Config{}, + Logger: slog.New(slog.NewTextHandler(os.Stderr, nil)), + } + + evt := ScheduledEvent{Action: "unknown-action"} + err := app.ProcessScheduledEvent(context.Background(), evt) + + if err == nil { + t.Error("expected error for unknown action") + } +} + +// verify fake data types match expected interfaces +func TestFakeDataTypes(t *testing.T) { + // ensure fake PR result is compatible with notifier + var _ *github.PRComplianceResult = fakePRComplianceResult() + + // ensure fake sync reports are compatible with notifier + var _ []*okta.SyncReport = fakeOktaSyncReports() + + // ensure fake orphaned users report is compatible with notifier + var _ *okta.OrphanedUsersReport = fakeOrphanedUsersReport() +}