- Move old task files to archive - Update router.go with new adapter wiring - Update runtime.go with provider integration - Refactor provider.go with new abstraction - Update provider tests
297 lines
9 KiB
Go
297 lines
9 KiB
Go
package provider
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.toki-labs.com/toki/gito/services/core/internal/core"
|
|
)
|
|
|
|
func TestConfigNormalizedTrimsIdentityFields(t *testing.T) {
|
|
config := Config{
|
|
ID: ProviderID(" GitHub "),
|
|
Endpoint: " https://api.github.com ",
|
|
CredentialRef: " secret://gito/providers/github ",
|
|
Capabilities: CapabilityChangeRequest | CapabilityWebhook,
|
|
}
|
|
|
|
got := config.Normalized()
|
|
|
|
if got.ID != ProviderGitHub {
|
|
t.Fatalf("ID: got %q want %q", got.ID, ProviderGitHub)
|
|
}
|
|
if got.Endpoint != "https://api.github.com" {
|
|
t.Fatalf("Endpoint: got %q", got.Endpoint)
|
|
}
|
|
if got.CredentialRef != "secret://gito/providers/github" {
|
|
t.Fatalf("CredentialRef: got %q", got.CredentialRef)
|
|
}
|
|
}
|
|
|
|
func TestCapabilityFlags(t *testing.T) {
|
|
capabilities := CapabilityChangeRequest | CapabilityWebhook | CapabilityComment
|
|
|
|
if !capabilities.Has(CapabilityChangeRequest) {
|
|
t.Fatal("expected change request capability")
|
|
}
|
|
if !capabilities.Has(CapabilityWebhook) {
|
|
t.Fatal("expected webhook capability")
|
|
}
|
|
if !capabilities.Has(CapabilityComment) {
|
|
t.Fatal("expected comment capability")
|
|
}
|
|
if capabilities.Has(CapabilityChecks) {
|
|
t.Fatal("did not expect checks capability")
|
|
}
|
|
if capabilities.Has(0) {
|
|
t.Fatal("zero capability should not be supported")
|
|
}
|
|
}
|
|
|
|
func TestConfigSupportsCapability(t *testing.T) {
|
|
config := Config{
|
|
ID: ProviderForgejo,
|
|
Capabilities: CapabilityWebhook | CapabilityChecks,
|
|
}
|
|
|
|
if !config.Supports(CapabilityWebhook) {
|
|
t.Fatal("expected webhook support")
|
|
}
|
|
if !config.Supports(CapabilityChecks) {
|
|
t.Fatal("expected checks support")
|
|
}
|
|
if config.Supports(CapabilityChangeRequest) {
|
|
t.Fatal("did not expect change request support")
|
|
}
|
|
}
|
|
|
|
// --- Fake adapter for testing ---
|
|
|
|
// fakeWebhookAdapter is a test implementation of WebhookAdapter.
|
|
type fakeWebhookAdapter struct {
|
|
verified bool
|
|
verifyErr error
|
|
candidates []WebhookCandidate
|
|
normalizeErr error
|
|
|
|
// Recorded requests for verifier and normalizer.
|
|
verifyReq WebhookRequest
|
|
normalizeReq WebhookRequest
|
|
}
|
|
|
|
func (f *fakeWebhookAdapter) VerifyWebhook(ctx context.Context, req WebhookRequest) error {
|
|
if f.verifyErr != nil {
|
|
return f.verifyErr
|
|
}
|
|
f.verified = true
|
|
f.verifyReq = req
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeWebhookAdapter) NormalizeWebhook(ctx context.Context, req WebhookRequest) ([]WebhookCandidate, error) {
|
|
if f.normalizeErr != nil {
|
|
return nil, f.normalizeErr
|
|
}
|
|
f.normalizeReq = req
|
|
return f.candidates, nil
|
|
}
|
|
|
|
// verifyWebhookAdapter ensures WebhookAdapter compile-time assertion.
|
|
var _ WebhookAdapter = (*fakeWebhookAdapter)(nil)
|
|
|
|
func TestWebhookAdapterContractCompileTime(t *testing.T) {
|
|
// fakeWebhookAdapter is already assigned to var _ WebhookAdapter.
|
|
// This test passes if the file compiles.
|
|
}
|
|
|
|
func TestWebhookAdapterContractRequestDelivery(t *testing.T) {
|
|
revision := &core.RevisionEvent{
|
|
RepoID: "test-repo",
|
|
Branch: "main",
|
|
Before: "abc123",
|
|
After: "def456",
|
|
ObservedAt: time.Now().UTC(),
|
|
}
|
|
now := time.Now().UTC()
|
|
|
|
req := WebhookRequest{
|
|
Provider: ProviderForgejo,
|
|
EventType: "push",
|
|
ExternalID: "delivery-123",
|
|
Payload: []byte(`{"ref":"refs/heads/main"}`),
|
|
Headers: map[string][]string{
|
|
"X-Forgejo-Signature": {"sha256=abc"},
|
|
},
|
|
Query: map[string][]string{
|
|
"repo_id": {"test-repo"},
|
|
},
|
|
ReceivedAt: now,
|
|
}
|
|
|
|
fake := &fakeWebhookAdapter{
|
|
candidates: []WebhookCandidate{
|
|
{
|
|
Type: "branch.updated",
|
|
Provider: ProviderForgejo,
|
|
ExternalID: "delivery-123",
|
|
Revision: revision,
|
|
},
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Verify delivery
|
|
if err := fake.VerifyWebhook(ctx, req); err != nil {
|
|
t.Fatalf("VerifyWebhook: %v", err)
|
|
}
|
|
if !fake.verified {
|
|
t.Fatal("expected verified to be true")
|
|
}
|
|
|
|
// Normalize delivery
|
|
candidates, err := fake.NormalizeWebhook(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("NormalizeWebhook: %v", err)
|
|
}
|
|
if len(candidates) != 1 {
|
|
t.Fatalf("expected 1 candidate, got %d", len(candidates))
|
|
}
|
|
|
|
c := candidates[0]
|
|
if c.Type != "branch.updated" {
|
|
t.Fatalf("Type: got %q want %q", c.Type, "branch.updated")
|
|
}
|
|
if c.Provider != ProviderForgejo {
|
|
t.Fatalf("Provider: got %q want %q", c.Provider, ProviderForgejo)
|
|
}
|
|
if c.ExternalID != "delivery-123" {
|
|
t.Fatalf("ExternalID: got %q want %q", c.ExternalID, "delivery-123")
|
|
}
|
|
if c.Revision == nil {
|
|
t.Fatal("expected Revision to be set")
|
|
}
|
|
if c.Revision.RepoID != "test-repo" {
|
|
t.Fatalf("Revision.RepoID: got %q want %q", c.Revision.RepoID, "test-repo")
|
|
}
|
|
if c.Revision.Branch != "main" {
|
|
t.Fatalf("Revision.Branch: got %q want %q", c.Revision.Branch, "main")
|
|
}
|
|
|
|
// Verify that the request was actually delivered to the verifier.
|
|
if fake.verifyReq.Provider == "" {
|
|
t.Fatal("expected verifyReq.Provider to be set")
|
|
}
|
|
if fake.verifyReq.Provider != ProviderForgejo {
|
|
t.Fatalf("verifyReq.Provider: got %q want %q", fake.verifyReq.Provider, ProviderForgejo)
|
|
}
|
|
if fake.verifyReq.EventType != "push" {
|
|
t.Fatalf("verifyReq.EventType: got %q want %q", fake.verifyReq.EventType, "push")
|
|
}
|
|
if fake.verifyReq.ExternalID != "delivery-123" {
|
|
t.Fatalf("verifyReq.ExternalID: got %q want %q", fake.verifyReq.ExternalID, "delivery-123")
|
|
}
|
|
if string(fake.verifyReq.Payload) != `{"ref":"refs/heads/main"}` {
|
|
t.Fatalf("verifyReq.Payload: got %q", fake.verifyReq.Payload)
|
|
}
|
|
if len(fake.verifyReq.Headers) != 1 {
|
|
t.Fatalf("verifyReq.Headers: got %d entries, want 1", len(fake.verifyReq.Headers))
|
|
}
|
|
if sigs, ok := fake.verifyReq.Headers["X-Forgejo-Signature"]; !ok {
|
|
t.Fatal("verifyReq.Headers['X-Forgejo-Signature'] not found")
|
|
} else if len(sigs) != 1 || sigs[0] != "sha256=abc" {
|
|
t.Fatalf("verifyReq.Headers['X-Forgejo-Signature']: got %v, want [sha256=abc]", sigs)
|
|
}
|
|
if len(fake.verifyReq.Query) != 1 {
|
|
t.Fatalf("verifyReq.Query: got %d entries, want 1", len(fake.verifyReq.Query))
|
|
}
|
|
if repos, ok := fake.verifyReq.Query["repo_id"]; !ok {
|
|
t.Fatal("verifyReq.Query['repo_id'] not found")
|
|
} else if len(repos) != 1 || repos[0] != "test-repo" {
|
|
t.Fatalf("verifyReq.Query['repo_id']: got %v, want [test-repo]", repos)
|
|
}
|
|
if !fake.verifyReq.ReceivedAt.Equal(now) {
|
|
t.Fatalf("verifyReq.ReceivedAt: got %v, want %v", fake.verifyReq.ReceivedAt, now)
|
|
}
|
|
|
|
// Verify that the request was actually delivered to the normalizer.
|
|
if fake.normalizeReq.Provider != ProviderForgejo {
|
|
t.Fatalf("normalizeReq.Provider: got %q want %q", fake.normalizeReq.Provider, ProviderForgejo)
|
|
}
|
|
if fake.normalizeReq.EventType != "push" {
|
|
t.Fatalf("normalizeReq.EventType: got %q want %q", fake.normalizeReq.EventType, "push")
|
|
}
|
|
if fake.normalizeReq.ExternalID != "delivery-123" {
|
|
t.Fatalf("normalizeReq.ExternalID: got %q want %q", fake.normalizeReq.ExternalID, "delivery-123")
|
|
}
|
|
if string(fake.normalizeReq.Payload) != `{"ref":"refs/heads/main"}` {
|
|
t.Fatalf("normalizeReq.Payload: got %q", fake.normalizeReq.Payload)
|
|
}
|
|
if len(fake.normalizeReq.Headers) != 1 {
|
|
t.Fatalf("normalizeReq.Headers: got %d entries, want 1", len(fake.normalizeReq.Headers))
|
|
}
|
|
if sigs, ok := fake.normalizeReq.Headers["X-Forgejo-Signature"]; !ok {
|
|
t.Fatal("normalizeReq.Headers['X-Forgejo-Signature'] not found")
|
|
} else if len(sigs) != 1 || sigs[0] != "sha256=abc" {
|
|
t.Fatalf("normalizeReq.Headers['X-Forgejo-Signature']: got %v, want [sha256=abc]", sigs)
|
|
}
|
|
if len(fake.normalizeReq.Query) != 1 {
|
|
t.Fatalf("normalizeReq.Query: got %d entries, want 1", len(fake.normalizeReq.Query))
|
|
}
|
|
if repos, ok := fake.normalizeReq.Query["repo_id"]; !ok {
|
|
t.Fatal("normalizeReq.Query['repo_id'] not found")
|
|
} else if len(repos) != 1 || repos[0] != "test-repo" {
|
|
t.Fatalf("normalizeReq.Query['repo_id']: got %v, want [test-repo]", repos)
|
|
}
|
|
if !fake.normalizeReq.ReceivedAt.Equal(now) {
|
|
t.Fatalf("normalizeReq.ReceivedAt: got %v, want %v", fake.normalizeReq.ReceivedAt, now)
|
|
}
|
|
}
|
|
|
|
func TestWebhookRequestEmptyPayloadPreservation(t *testing.T) {
|
|
req := WebhookRequest{
|
|
Provider: ProviderGitHub,
|
|
EventType: "pull_request",
|
|
ExternalID: "",
|
|
Payload: []byte{},
|
|
Headers: nil,
|
|
Query: nil,
|
|
ReceivedAt: time.Time{},
|
|
}
|
|
|
|
if len(req.Payload) != 0 {
|
|
t.Fatalf("expected empty payload, got %d bytes", len(req.Payload))
|
|
}
|
|
if req.ExternalID != "" {
|
|
t.Fatalf("expected empty ExternalID, got %q", req.ExternalID)
|
|
}
|
|
if !req.ReceivedAt.IsZero() {
|
|
t.Fatalf("expected zero ReceivedAt")
|
|
}
|
|
}
|
|
|
|
func TestWebhookCandidateNoSecretRequirement(t *testing.T) {
|
|
// WebhookCandidate must not expose any secret-related fields.
|
|
// This is verified by the struct definition itself:
|
|
// - Type string
|
|
// - Provider ProviderID
|
|
// - ExternalID string
|
|
// - Revision *core.RevisionEvent
|
|
// No secret, signature, or token fields.
|
|
c := WebhookCandidate{
|
|
Type: "branch.updated",
|
|
Provider: ProviderForgejo,
|
|
ExternalID: "delivery-1",
|
|
}
|
|
|
|
// Verify the struct has no secret-related fields by construction.
|
|
// If any secret field were added, compilation would still pass,
|
|
// but the test ensures the current minimal metadata contract.
|
|
if c.Type == "" {
|
|
t.Fatal("Type should be settable")
|
|
}
|
|
if c.Provider == "" {
|
|
t.Fatal("Provider should be settable")
|
|
}
|
|
}
|