package webhook_test import ( "bytes" "context" "encoding/json" "fmt" "net" "net/http" "testing" "time" "superwork-tui/internal/webhook" ) // freePort returns a random available TCP port on localhost. func freePort(t *testing.T) int { t.Helper() l, err := net.Listen("tcp", ":0") if err != nil { t.Fatalf("freePort: %v", err) } port := l.Addr().(*net.TCPAddr).Port l.Close() return port } func TestServerLifecycle_EventArrivesViaChannel(t *testing.T) { ch := make(chan webhook.Event, 4) srv := webhook.NewServer(func(ev webhook.Event) { ch <- ev }) port := freePort(t) addr := fmt.Sprintf(":%d", port) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) errCh := make(chan error, 1) go func() { errCh <- srv.Start(ctx, addr) }() // Wait until the server accepts connections. deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { conn, err := net.DialTimeout("tcp", addr, 50*time.Millisecond) if err == nil { conn.Close() break } time.Sleep(20 * time.Millisecond) } // POST a push event. payload := map[string]any{ "ref": "refs/heads/main", } body, _ := json.Marshal(payload) req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("http://localhost%s/webhook", addr), bytes.NewReader(body)) if err != nil { t.Fatalf("build request: %v", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Gitea-Event", "push") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("POST /webhook: %v", err) } resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("want 200, got %d", resp.StatusCode) } // Expect a PushEvent on the channel. select { case ev := <-ch: pushEv, ok := ev.(webhook.PushEvent) if !ok { t.Fatalf("want PushEvent, got %T", ev) } if pushEv.Branch != "main" { t.Errorf("want Branch=main, got %q", pushEv.Branch) } case <-time.After(2 * time.Second): t.Fatal("timed out waiting for PushEvent") } // Cancel context → server shuts down; Start should return. cancel() select { case <-errCh: // OK — server stopped case <-time.After(2 * time.Second): t.Fatal("timed out waiting for server shutdown") } } func TestServerLifecycle_IssueCommentEvent(t *testing.T) { ch := make(chan webhook.Event, 4) srv := webhook.NewServer(func(ev webhook.Event) { ch <- ev }) port := freePort(t) addr := fmt.Sprintf(":%d", port) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) go srv.Start(ctx, addr) //nolint:errcheck deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { conn, err := net.DialTimeout("tcp", addr, 50*time.Millisecond) if err == nil { conn.Close() break } time.Sleep(20 * time.Millisecond) } payload := map[string]any{ "action": "created", "issue": map[string]any{ "number": float64(42), }, "comment": map[string]any{ "body": "\nLooks good", "html_url": "https://example.com/comment/1", }, } body, _ := json.Marshal(payload) req, _ := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("http://localhost%s/webhook", addr), bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Gitea-Event", "issue_comment") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("POST /webhook: %v", err) } resp.Body.Close() select { case ev := <-ch: ce, ok := ev.(webhook.IssueCommentEvent) if !ok { t.Fatalf("want IssueCommentEvent, got %T", ev) } if ce.IssueNumber != 42 { t.Errorf("want IssueNumber=42, got %d", ce.IssueNumber) } if !webhook.IsReviewMarker(ce.CommentBody) { t.Errorf("expected comment to contain review marker") } case <-time.After(2 * time.Second): t.Fatal("timed out waiting for IssueCommentEvent") } }