Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 96 additions & 3 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"`
Expand Down
171 changes: 171 additions & 0 deletions internal/app/app_test.go
Original file line number Diff line number Diff line change
@@ -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()
}