- ProviderAdapter 인터페이스를 정의하고 AWS/GitLab provider 구현체를 추가한다 - CRD model에 ProviderRef 및 status 필드를 추가하여 provider 관리를 표준화한다 - ControlPlane에 provider registry를 통합하여 runtime/periodic event routing을 provider별로 분기한다 - Router에 routeByProvider 메서드를 추가하여 provider별 event 라우팅을 지원한다 - Event 시스템에 outbound event 전송 기능을 추가한다 - 관련 테스트를 추가하여 provider별 동작을 검증한다
554 lines
19 KiB
Go
554 lines
19 KiB
Go
package controlplane
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
|
"git.toki-labs.com/toki/gito/services/core/internal/protosocket"
|
|
)
|
|
|
|
func TestHealthz(t *testing.T) {
|
|
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
|
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status: got %d", rec.Code)
|
|
}
|
|
var body map[string]string
|
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if body["status"] != "ok" {
|
|
t.Fatalf("body: %#v", body)
|
|
}
|
|
}
|
|
|
|
func TestProtoSocketRegistry(t *testing.T) {
|
|
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
|
req := httptest.NewRequest(http.MethodGet, "/proto-socket", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status: got %d", rec.Code)
|
|
}
|
|
var body ProtoSocketRegistry
|
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if body.Transport != "proto-socket" {
|
|
t.Fatalf("transport: got %q", body.Transport)
|
|
}
|
|
channels := make(map[string]ProtoSocketChannel, len(body.Channels))
|
|
for _, channel := range body.Channels {
|
|
channels[channel.Name] = channel
|
|
}
|
|
assertPlaceholderChannel(t, channels, "operation", []string{"create", "cancel", "get", "stream"})
|
|
assertPlaceholderChannel(t, channels, "change_request", []string{"create", "update", "comment", "request_review", "merge", "close", "list"})
|
|
assertChannelActions(t, channels, "event", "mvp", map[string]string{
|
|
"subscribe": "mvp",
|
|
"list": "mvp",
|
|
"ack": "placeholder",
|
|
})
|
|
}
|
|
|
|
func assertPlaceholderChannel(t *testing.T, channels map[string]ProtoSocketChannel, name string, actions []string) {
|
|
t.Helper()
|
|
channel, ok := channels[name]
|
|
if !ok {
|
|
t.Fatalf("missing channel %q", name)
|
|
}
|
|
if channel.Status != "placeholder" {
|
|
t.Fatalf("channel %q status: got %q", name, channel.Status)
|
|
}
|
|
actionStatuses := make(map[string]string, len(channel.Actions))
|
|
for _, action := range channel.Actions {
|
|
actionStatuses[action.Name] = action.Status
|
|
}
|
|
for _, action := range actions {
|
|
if actionStatuses[action] != "placeholder" {
|
|
t.Fatalf("channel %q action %q status: got %q", name, action, actionStatuses[action])
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertChannelActions(t *testing.T, channels map[string]ProtoSocketChannel, name, status string, actions map[string]string) {
|
|
t.Helper()
|
|
channel, ok := channels[name]
|
|
if !ok {
|
|
t.Fatalf("missing channel %q", name)
|
|
}
|
|
if channel.Status != status {
|
|
t.Fatalf("channel %q status: got %q want %q", name, channel.Status, status)
|
|
}
|
|
actionStatuses := make(map[string]string, len(channel.Actions))
|
|
for _, action := range channel.Actions {
|
|
actionStatuses[action.Name] = action.Status
|
|
}
|
|
for action, want := range actions {
|
|
if actionStatuses[action] != want {
|
|
t.Fatalf("channel %q action %q status: got %q want %q", name, action, actionStatuses[action], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestForgejoPushCreatesWatchedBranchEvent(t *testing.T) {
|
|
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
|
|
|
registerBranchListener(t, router, `{"repo_id":"nomadcode","branch":"develop","provider":"forgejo"}`)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/callbacks/forgejo/push?repo_id=nomadcode", bytes.NewBufferString(`{
|
|
"ref": "refs/heads/develop",
|
|
"before": "111",
|
|
"after": "222",
|
|
"repository": {"name": "nomadcode", "full_name": "toki/nomadcode"},
|
|
"commits": [{"id": "222", "modified": ["agent-roadmap/phase/a/milestones/b.md"]}]
|
|
}`))
|
|
req.Header.Set("X-Forgejo-Event", "push")
|
|
req.Header.Set("X-Forgejo-Delivery", "delivery-1")
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusAccepted {
|
|
t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var pushed map[string]any
|
|
if err := json.NewDecoder(rec.Body).Decode(&pushed); err != nil {
|
|
t.Fatalf("decode push response: %v", err)
|
|
}
|
|
if pushed["matched"] != true {
|
|
t.Fatalf("expected push to match listener: %#v", pushed)
|
|
}
|
|
|
|
eventsReq := httptest.NewRequest(http.MethodGet, "/api/events", nil)
|
|
eventsRec := httptest.NewRecorder()
|
|
router.ServeHTTP(eventsRec, eventsReq)
|
|
if eventsRec.Code != http.StatusOK {
|
|
t.Fatalf("events status: got %d", eventsRec.Code)
|
|
}
|
|
var body map[string][]map[string]any
|
|
if err := json.NewDecoder(eventsRec.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode events: %v", err)
|
|
}
|
|
if len(body["events"]) != 1 {
|
|
t.Fatalf("events: %#v", body)
|
|
}
|
|
event := body["events"][0]
|
|
if event["type"] != "branch.updated" || event["provider"] != "forgejo" || event["delivery_id"] != "delivery-1" {
|
|
t.Fatalf("unexpected event: %#v", event)
|
|
}
|
|
revision := event["revision"].(map[string]any)
|
|
if revision["repo_id"] != "nomadcode" || revision["branch"] != "develop" || revision["before"] != "111" || revision["after"] != "222" {
|
|
t.Fatalf("unexpected revision: %#v", revision)
|
|
}
|
|
}
|
|
|
|
func TestForgejoPushIgnoresUnwatchedBranch(t *testing.T) {
|
|
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
|
|
|
registerBranchListener(t, router, `{"repo_id":"nomadcode","branch":"develop","provider":"forgejo"}`)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/callbacks/forgejo/push?repo_id=nomadcode", bytes.NewBufferString(`{
|
|
"ref": "refs/heads/feature/a",
|
|
"before": "111",
|
|
"after": "222",
|
|
"repository": {"name": "nomadcode", "full_name": "toki/nomadcode"}
|
|
}`))
|
|
req.Header.Set("X-Forgejo-Event", "push")
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusAccepted {
|
|
t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var pushed map[string]any
|
|
if err := json.NewDecoder(rec.Body).Decode(&pushed); err != nil {
|
|
t.Fatalf("decode push response: %v", err)
|
|
}
|
|
if pushed["matched"] != false {
|
|
t.Fatalf("expected push to be ignored: %#v", pushed)
|
|
}
|
|
if _, ok := pushed["event"]; ok {
|
|
t.Fatalf("unmatched push must not expose an emitted event: %#v", pushed)
|
|
}
|
|
}
|
|
|
|
func TestForgejoPushRequiresSignatureBeforeIgnoringUnsupportedEvent(t *testing.T) {
|
|
router := NewRouter(config.Config{
|
|
AppEnv: "test",
|
|
ProtoSocketPath: "/proto-socket",
|
|
ForgejoWebhookSecret: "forgejo-secret",
|
|
}, slog.Default())
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/callbacks/forgejo/push", bytes.NewBufferString(`{"unsupported":true}`))
|
|
req.Header.Set("X-Forgejo-Event", "repository")
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestProviderCallbackRecordsReceivedWebhookEvent(t *testing.T) {
|
|
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/callbacks/providers/fake", bytes.NewBufferString(`{
|
|
"action": "ping",
|
|
"token": "top-secret"
|
|
}`))
|
|
req.Header.Set("X-Gito-Provider-Event", "ping")
|
|
req.Header.Set("X-Gito-Delivery", "fake-delivery-1")
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusAccepted {
|
|
t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if bytes.Contains(rec.Body.Bytes(), []byte("top-secret")) ||
|
|
bytes.Contains(rec.Body.Bytes(), []byte(`"token"`)) {
|
|
t.Fatalf("provider callback response leaked raw payload: %s", rec.Body.String())
|
|
}
|
|
var created map[string]any
|
|
if err := json.NewDecoder(rec.Body).Decode(&created); err != nil {
|
|
t.Fatalf("decode callback response: %v", err)
|
|
}
|
|
event, ok := created["event"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("missing event: %#v", created)
|
|
}
|
|
if event["type"] != "provider.webhook.received" ||
|
|
event["provider"] != "fake" ||
|
|
event["delivery_id"] != "fake-delivery-1" {
|
|
t.Fatalf("unexpected event: %#v", event)
|
|
}
|
|
webhook, ok := event["webhook"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("missing webhook metadata: %#v", event)
|
|
}
|
|
if webhook["event_type"] != "ping" || webhook["external_id"] != "fake-delivery-1" {
|
|
t.Fatalf("unexpected webhook metadata: %#v", webhook)
|
|
}
|
|
if _, ok := webhook["payload_size"].(float64); !ok {
|
|
t.Fatalf("payload_size missing: %#v", webhook)
|
|
}
|
|
|
|
eventsReq := httptest.NewRequest(http.MethodGet, "/api/events", nil)
|
|
eventsRec := httptest.NewRecorder()
|
|
router.ServeHTTP(eventsRec, eventsReq)
|
|
if eventsRec.Code != http.StatusOK {
|
|
t.Fatalf("events status: got %d", eventsRec.Code)
|
|
}
|
|
if bytes.Contains(eventsRec.Body.Bytes(), []byte("top-secret")) ||
|
|
bytes.Contains(eventsRec.Body.Bytes(), []byte(`"token"`)) {
|
|
t.Fatalf("event list leaked raw payload: %s", eventsRec.Body.String())
|
|
}
|
|
var listed map[string][]map[string]any
|
|
if err := json.NewDecoder(eventsRec.Body).Decode(&listed); err != nil {
|
|
t.Fatalf("decode events response: %v", err)
|
|
}
|
|
if len(listed["events"]) != 1 {
|
|
t.Fatalf("events: %#v", listed)
|
|
}
|
|
if listed["events"][0]["type"] != "provider.webhook.received" {
|
|
t.Fatalf("unexpected listed event: %#v", listed["events"][0])
|
|
}
|
|
}
|
|
|
|
func TestWebhookSubscriptionRegistersFilterWithoutLeakingSecretRef(t *testing.T) {
|
|
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/webhook-subscriptions", bytes.NewBufferString(`{
|
|
"name": "nomadcode-develop-wakeup",
|
|
"target_url": "https://consumer.example.invalid/gito/events",
|
|
"events": ["branch.updated"],
|
|
"repo_id": "nomadcode",
|
|
"branch": "develop",
|
|
"secret_ref": "secret://gito/webhooks/nomadcode"
|
|
}`))
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if bytes.Contains(rec.Body.Bytes(), []byte("secret://gito/webhooks/nomadcode")) ||
|
|
bytes.Contains(rec.Body.Bytes(), []byte("secret_ref")) ||
|
|
bytes.Contains(rec.Body.Bytes(), []byte("consumer.example.invalid")) {
|
|
t.Fatalf("subscription response leaked private subscription input: %s", rec.Body.String())
|
|
}
|
|
var created map[string]map[string]any
|
|
if err := json.NewDecoder(rec.Body).Decode(&created); err != nil {
|
|
t.Fatalf("decode subscription response: %v", err)
|
|
}
|
|
subscription := created["subscription"]
|
|
if subscription["id"] == "" {
|
|
t.Fatalf("missing subscription id: %#v", subscription)
|
|
}
|
|
if subscription["name"] != "nomadcode-develop-wakeup" ||
|
|
subscription["repo_id"] != "nomadcode" ||
|
|
subscription["branch"] != "develop" {
|
|
t.Fatalf("unexpected subscription payload: %#v", subscription)
|
|
}
|
|
events, ok := subscription["events"].([]any)
|
|
if !ok || len(events) != 1 || events[0] != "branch.updated" {
|
|
t.Fatalf("events: %#v", subscription["events"])
|
|
}
|
|
|
|
listReq := httptest.NewRequest(http.MethodGet, "/api/webhook-subscriptions", nil)
|
|
listRec := httptest.NewRecorder()
|
|
router.ServeHTTP(listRec, listReq)
|
|
if listRec.Code != http.StatusOK {
|
|
t.Fatalf("list status: got %d body=%s", listRec.Code, listRec.Body.String())
|
|
}
|
|
if bytes.Contains(listRec.Body.Bytes(), []byte("secret://gito/webhooks/nomadcode")) ||
|
|
bytes.Contains(listRec.Body.Bytes(), []byte("secret_ref")) ||
|
|
bytes.Contains(listRec.Body.Bytes(), []byte("consumer.example.invalid")) {
|
|
t.Fatalf("subscription list leaked private subscription input: %s", listRec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestWebhookSubscriptionEnvSecretRefSignsDelivery(t *testing.T) {
|
|
const secret = "router-secret"
|
|
t.Setenv("GITO_WEBHOOK_SECRET", secret)
|
|
|
|
consumer := &fakeWebhookConsumer{}
|
|
server := fakeWebhookServer(t, consumer)
|
|
defer server.Close()
|
|
|
|
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
|
registerBranchListener(t, router, `{"repo_id":"nomadcode","branch":"develop","provider":"forgejo"}`)
|
|
|
|
subReq := httptest.NewRequest(http.MethodPost, "/api/webhook-subscriptions", bytes.NewBufferString(`{
|
|
"name": "nomadcode-develop-wakeup",
|
|
"target_url": "`+server.URL+`",
|
|
"events": ["branch.updated"],
|
|
"repo_id": "nomadcode",
|
|
"branch": "develop",
|
|
"secret_ref": "env:GITO_WEBHOOK_SECRET"
|
|
}`))
|
|
subRec := httptest.NewRecorder()
|
|
router.ServeHTTP(subRec, subReq)
|
|
if subRec.Code != http.StatusCreated {
|
|
t.Fatalf("subscription status: got %d body=%s", subRec.Code, subRec.Body.String())
|
|
}
|
|
|
|
pushReq := httptest.NewRequest(http.MethodPost, "/callbacks/forgejo/push?repo_id=nomadcode", bytes.NewBufferString(`{
|
|
"ref": "refs/heads/develop",
|
|
"before": "111",
|
|
"after": "222",
|
|
"repository": {"name": "nomadcode", "full_name": "toki/nomadcode"},
|
|
"commits": [{"id": "222", "modified": ["agent-roadmap/phase/a/milestones/b.md"]}]
|
|
}`))
|
|
pushReq.Header.Set("X-Forgejo-Event", "push")
|
|
pushReq.Header.Set("X-Forgejo-Delivery", "delivery-1")
|
|
pushRec := httptest.NewRecorder()
|
|
router.ServeHTTP(pushRec, pushReq)
|
|
if pushRec.Code != http.StatusAccepted {
|
|
t.Fatalf("push status: got %d body=%s", pushRec.Code, pushRec.Body.String())
|
|
}
|
|
|
|
consumer.mu.Lock()
|
|
received := append([]webhookReception(nil), consumer.received...)
|
|
consumer.mu.Unlock()
|
|
if len(received) != 1 {
|
|
t.Fatalf("expected one webhook delivery, got %d", len(received))
|
|
}
|
|
rec := received[0]
|
|
if rec.eventType != "branch.updated" || rec.deliveryID == "" {
|
|
t.Fatalf("unexpected delivery headers: event=%q delivery=%q", rec.eventType, rec.deliveryID)
|
|
}
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
_, _ = mac.Write(rec.bodyRaw)
|
|
wantSignature := "sha256=" + hex.EncodeToString(mac.Sum(nil))
|
|
if rec.signature != wantSignature {
|
|
t.Fatalf("signature: got %q want %q", rec.signature, wantSignature)
|
|
}
|
|
}
|
|
|
|
func TestWebhookSubscriptionValidatesTargetAndEvents(t *testing.T) {
|
|
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/webhook-subscriptions", bytes.NewBufferString(`{
|
|
"name": "invalid",
|
|
"target_url": "file:///tmp/event",
|
|
"events": ["branch.updated"]
|
|
}`))
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
req = httptest.NewRequest(http.MethodPost, "/api/webhook-subscriptions", bytes.NewBufferString(`{
|
|
"name": "missing-events",
|
|
"target_url": "https://consumer.example.invalid/gito/events"
|
|
}`))
|
|
rec = httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("missing events status: got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestEventSubscribeRegistersConnectionFilter(t *testing.T) {
|
|
dispatcher := protosocket.NewDispatcher()
|
|
runtime := NewRuntime(nil)
|
|
subscriber := &recordingSubscriber{}
|
|
registerProtoSocketHandlers(dispatcher, runtime, subscriber)
|
|
|
|
response := dispatcher.Dispatch(t.Context(), protosocket.Envelope{
|
|
ID: "msg-1",
|
|
Type: "request",
|
|
Channel: "event",
|
|
Action: "event.subscribe",
|
|
Payload: map[string]any{
|
|
"events": []any{"branch.updated"},
|
|
"repo_id": "nomadcode",
|
|
"branch": "develop",
|
|
},
|
|
Meta: map[string]any{
|
|
"connection_id": "conn-1",
|
|
},
|
|
})
|
|
|
|
if response.Type != "response" {
|
|
t.Fatalf("response type: got %q error=%+v", response.Type, response.Error)
|
|
}
|
|
if subscriber.connectionID != "conn-1" {
|
|
t.Fatalf("connection id: got %q", subscriber.connectionID)
|
|
}
|
|
if len(subscriber.subscription.Events) != 1 || subscriber.subscription.Events[0] != "branch.updated" {
|
|
t.Fatalf("events: %#v", subscriber.subscription.Events)
|
|
}
|
|
if subscriber.subscription.RepoID != "nomadcode" || subscriber.subscription.Branch != "develop" {
|
|
t.Fatalf("subscription: %+v", subscriber.subscription)
|
|
}
|
|
}
|
|
|
|
func TestForgejoPushTreatsMissingEventHeaderAsPush(t *testing.T) {
|
|
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
|
|
|
registerBranchListener(t, router, `{"repo_id":"nomadcode","branch":"develop","provider":"forgejo"}`)
|
|
|
|
// X-Forgejo-Event header missing but valid push payload should still be processed.
|
|
req := httptest.NewRequest(http.MethodPost, "/callbacks/forgejo/push?repo_id=nomadcode", bytes.NewBufferString(`{
|
|
"ref": "refs/heads/develop",
|
|
"before": "111",
|
|
"after": "222",
|
|
"repository": {"name": "nomadcode", "full_name": "toki/nomadcode"},
|
|
"commits": [{"id": "222", "modified": ["README.md"]}]
|
|
}`))
|
|
req.Header.Set("X-Forgejo-Delivery", "delivery-missing-event")
|
|
// Note: X-Forgejo-Event header is intentionally absent.
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusAccepted {
|
|
t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var pushed map[string]any
|
|
if err := json.NewDecoder(rec.Body).Decode(&pushed); err != nil {
|
|
t.Fatalf("decode push response: %v", err)
|
|
}
|
|
if pushed["matched"] != true {
|
|
t.Fatalf("expected push to match listener with missing event header: %#v", pushed)
|
|
}
|
|
}
|
|
|
|
func TestForgejoPushRejectsMalformedPayload(t *testing.T) {
|
|
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/callbacks/forgejo/push?repo_id=nomadcode", bytes.NewBufferString(`{malformed json`))
|
|
req.Header.Set("X-Forgejo-Event", "push")
|
|
req.Header.Set("X-Forgejo-Delivery", "delivery-malformed")
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestForgejoPushReturns400WhenNoRepoIDSource(t *testing.T) {
|
|
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
|
|
|
// Valid payload but repository has no name or full_name.
|
|
req := httptest.NewRequest(http.MethodPost, "/callbacks/forgejo/push", bytes.NewBufferString(`{
|
|
"ref": "refs/heads/develop",
|
|
"before": "111",
|
|
"after": "222",
|
|
"repository": {},
|
|
"commits": [{"id": "222"}]
|
|
}`))
|
|
req.Header.Set("X-Forgejo-Event", "push")
|
|
req.Header.Set("X-Forgejo-Delivery", "delivery-no-repo")
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status: got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestEventSubscribeRequiresConnectionID(t *testing.T) {
|
|
dispatcher := protosocket.NewDispatcher()
|
|
runtime := NewRuntime(nil)
|
|
registerProtoSocketHandlers(dispatcher, runtime, &recordingSubscriber{})
|
|
|
|
response := dispatcher.Dispatch(t.Context(), protosocket.Envelope{
|
|
ID: "msg-1",
|
|
Type: "request",
|
|
Channel: "event",
|
|
Action: "event.subscribe",
|
|
})
|
|
|
|
if response.Type != "error" || response.Error == nil || response.Error.Code != "event.missing_connection" {
|
|
t.Fatalf("expected missing connection error, got %+v", response)
|
|
}
|
|
}
|
|
|
|
func registerBranchListener(t *testing.T, router http.Handler, body string) {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/listeners/branches", bytes.NewBufferString(body))
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("listener status: got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
type recordingSubscriber struct {
|
|
connectionID string
|
|
subscription protosocket.EventSubscription
|
|
}
|
|
|
|
func (r *recordingSubscriber) Subscribe(connectionID string, subscription protosocket.EventSubscription) protosocket.EventSubscription {
|
|
r.connectionID = connectionID
|
|
r.subscription = subscription.Normalized()
|
|
return r.subscription
|
|
}
|