6ccc755999
The issues endpoint returns pull requests alongside issues, and nothing filtered them. The first submission this deployment ever made, kami/test-e2e#8, came straight back as task 06G4E83E4KRXM8DS90M2648MGM with the submission packet as its description. That task would have implemented, reviewed and submitted again, opening a pull request per cycle. The webhook had the same hole from the other side: a pull_request delivery leaves the issue key empty, so it would have appended a task numbered 0 with no title. Both routes now refuse a pull request, and the poll count reports what was ingested rather than what was listed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
181 lines
6.2 KiB
Go
181 lines
6.2 KiB
Go
package provider
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"orchestra/internal/domain"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestGiteaSourceNameDefaultsToRepo(t *testing.T) {
|
|
if got := (Gitea{Repo: "orchestra"}).SourceName(); got != "gitea" {
|
|
t.Fatalf("expected legacy unnamespaced source, got %q", got)
|
|
}
|
|
if got := (Gitea{Repo: "orchestra", Project: "correx"}).SourceName(); got != "gitea:correx" {
|
|
t.Fatalf("expected namespaced source, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestGiteaIngestWebhookTagsProject(t *testing.T) {
|
|
g := Gitea{Owner: "kami", Repo: "correx-repo", Project: "correx", WebhookSecret: "s3cret"}
|
|
body, _ := json.Marshal(map[string]any{
|
|
"action": "opened",
|
|
"issue": map[string]any{"number": 42, "title": "fix thing", "body": "", "state": "open"},
|
|
})
|
|
mac := hmac.New(sha256.New, []byte("s3cret"))
|
|
mac.Write(body)
|
|
sig := hex.EncodeToString(mac.Sum(nil))
|
|
|
|
sk := &sink{}
|
|
if err := g.IngestWebhook(body, sig, sk); err != nil {
|
|
t.Fatalf("ingest: %v", err)
|
|
}
|
|
if len(sk.events) != 1 {
|
|
t.Fatalf("expected 1 event, got %d", len(sk.events))
|
|
}
|
|
var p struct {
|
|
Project string `json:"project"`
|
|
Source string `json:"source"`
|
|
ExternalID string `json:"external_id"`
|
|
}
|
|
if err := json.Unmarshal(sk.events[0].Payload, &p); err != nil {
|
|
t.Fatalf("unmarshal payload: %v", err)
|
|
}
|
|
if p.Project != "correx" || p.Source != "gitea:correx" || p.ExternalID != "42" {
|
|
t.Fatalf("unexpected payload: %+v", p)
|
|
}
|
|
}
|
|
|
|
func TestGiteaIngestWebhookRejectsBadSignature(t *testing.T) {
|
|
g := Gitea{Owner: "kami", Repo: "r", Project: "p", WebhookSecret: "s3cret"}
|
|
if err := g.IngestWebhook([]byte(`{"action":"opened","issue":{"number":1}}`), "wrong", &sink{}); err == nil {
|
|
t.Fatal("expected signature rejection")
|
|
}
|
|
}
|
|
|
|
func TestMultiGiteaReflectDispatchesByTaskSource(t *testing.T) {
|
|
var hitCorrex, hitMaven bool
|
|
correxSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
hitCorrex = true
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer correxSrv.Close()
|
|
mavenSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
hitMaven = true
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer mavenSrv.Close()
|
|
|
|
m := MultiGitea{Sources: map[string]Gitea{
|
|
"gitea:correx": {BaseURL: correxSrv.URL, Owner: "kami", Repo: "correx-repo", Project: "correx"},
|
|
"gitea:maven": {BaseURL: mavenSrv.URL, Owner: "kami", Repo: "maven-repo", Project: "maven"},
|
|
}}
|
|
|
|
if err := m.ReflectTask(domain.Task{Source: "gitea:correx", ExternalID: "7"}, domain.Event{Type: "TaskCompleted"}); err != nil {
|
|
t.Fatalf("reflect to correx: %v", err)
|
|
}
|
|
if !hitCorrex || hitMaven {
|
|
t.Fatalf("expected only correx server hit, got correx=%v maven=%v", hitCorrex, hitMaven)
|
|
}
|
|
|
|
hitCorrex, hitMaven = false, false
|
|
if err := m.ReflectTask(domain.Task{Source: "gitea:maven", ExternalID: "3"}, domain.Event{Type: "TaskFailed"}); err != nil {
|
|
t.Fatalf("reflect to maven: %v", err)
|
|
}
|
|
if hitCorrex || !hitMaven {
|
|
t.Fatalf("expected only maven server hit, got correx=%v maven=%v", hitCorrex, hitMaven)
|
|
}
|
|
|
|
// A task from a non-Gitea source (or an unregistered Gitea project) must
|
|
// be a silent no-op, not an error.
|
|
if err := m.ReflectTask(domain.Task{Source: "jsonl"}, domain.Event{Type: "TaskCompleted"}); err != nil {
|
|
t.Fatalf("unmatched source should no-op, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadGiteaConfigsValidatesAndRejectsDuplicates(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "gitea.json")
|
|
good := `[
|
|
{"project":"correx","base_url":"https://gitea.internal","owner":"kami","repo":"correx"},
|
|
{"project":"maven","base_url":"https://gitea.internal","owner":"kami","repo":"maven"}
|
|
]`
|
|
if err := os.WriteFile(path, []byte(good), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfgs, err := LoadGiteaConfigs(path)
|
|
if err != nil || len(cfgs) != 2 {
|
|
t.Fatalf("cfgs=%d err=%v", len(cfgs), err)
|
|
}
|
|
|
|
dupPath := filepath.Join(dir, "dup.json")
|
|
dup := `[
|
|
{"project":"correx","base_url":"https://gitea.internal","owner":"kami","repo":"correx"},
|
|
{"project":"correx","base_url":"https://gitea.internal","owner":"kami","repo":"other"}
|
|
]`
|
|
if err := os.WriteFile(dupPath, []byte(dup), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := LoadGiteaConfigs(dupPath); err == nil {
|
|
t.Fatal("expected duplicate project rejection")
|
|
}
|
|
|
|
incompletePath := filepath.Join(dir, "incomplete.json")
|
|
if err := os.WriteFile(incompletePath, []byte(`[{"project":"x"}]`), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := LoadGiteaConfigs(incompletePath); err == nil {
|
|
t.Fatal("expected missing-field rejection")
|
|
}
|
|
}
|
|
|
|
// Orchestra opens pull requests itself. The Gitea issues endpoint returns them
|
|
// alongside issues, so a poller that accepts them ingests its own submission
|
|
// back as a task whose description is the submission packet, and that task
|
|
// submits again. Seen live on 2026-08-28: pull request #8 became task
|
|
// 06G4E83E4KRXM8DS90M2648MGM.
|
|
func TestPollIgnoresPullRequests(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode([]map[string]any{
|
|
{"number": 7, "title": "a real issue", "body": "", "state": "open"},
|
|
{"number": 8, "title": "orchestra submission", "body": "", "state": "open",
|
|
"pull_request": map[string]any{"merged": false}},
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
sk := &sink{}
|
|
n, err := (Gitea{BaseURL: srv.URL, Owner: "kami", Repo: "test-e2e"}).Poll(context.Background(), sk)
|
|
if err != nil {
|
|
t.Fatalf("poll: %v", err)
|
|
}
|
|
if n != 1 || len(sk.events) != 1 {
|
|
t.Fatalf("ingested %d events (reported %d), want exactly the issue", len(sk.events), n)
|
|
}
|
|
}
|
|
|
|
func TestIngestWebhookIgnoresAPullRequestDelivery(t *testing.T) {
|
|
g := Gitea{Owner: "kami", Repo: "test-e2e", WebhookSecret: "s3cret"}
|
|
body, _ := json.Marshal(map[string]any{
|
|
"action": "opened",
|
|
"pull_request": map[string]any{"number": 8, "title": "orchestra submission", "state": "open"},
|
|
})
|
|
mac := hmac.New(sha256.New, []byte("s3cret"))
|
|
mac.Write(body)
|
|
|
|
sk := &sink{}
|
|
if err := g.IngestWebhook(body, hex.EncodeToString(mac.Sum(nil)), sk); err != nil {
|
|
t.Fatalf("ingest: %v", err)
|
|
}
|
|
if len(sk.events) != 0 {
|
|
t.Fatalf("expected no task from a pull request delivery, got %d", len(sk.events))
|
|
}
|
|
}
|