package webhook import ( "net/http" "strings" "testing" ) func headerWith(k, v string) http.Header { h := http.Header{} h.Set(k, v) return h } func TestParseGiteaPackageEvent_Container(t *testing.T) { body := []byte(`{ "action": "created", "package": { "name": "my-app", "type": "container", "version": "v1.2.3", "owner": {"login": "alexei"}, "repository": {"full_name": "alexei/my-app", "html_url": "https://git.example.com/alexei/my-app"} } }`) res := parseGiteaPackageEvent(body, headerWith(headerGiteaEvent, "package")) if !res.ok { t.Fatalf("expected ok=true") } if res.err != nil { t.Fatalf("unexpected err: %v", res.err) } if res.event.Kind != "image-push" || res.event.Image == nil { t.Fatalf("kind=%q image=%+v", res.event.Kind, res.event.Image) } if res.event.Image.Repo != "alexei/my-app" { t.Errorf("Repo=%q want alexei/my-app", res.event.Image.Repo) } if res.event.Image.Tag != "v1.2.3" { t.Errorf("Tag=%q want v1.2.3", res.event.Image.Tag) } if res.event.Image.Registry != "git.example.com" { t.Errorf("Registry=%q want git.example.com", res.event.Image.Registry) } } func TestParseGiteaPackageEvent_NonContainerRejected(t *testing.T) { body := []byte(`{"action": "created", "package": {"name": "lib", "type": "npm", "version": "1.0.0"}}`) res := parseGiteaPackageEvent(body, headerWith(headerGiteaEvent, "package")) if !res.ok { t.Fatalf("expected ok=true (claimed)") } if res.err == nil || !strings.Contains(res.err.Error(), "container") { t.Errorf("expected container-type error, got %v", res.err) } } func TestParseGiteaPackageEvent_NoHeaderSkips(t *testing.T) { res := parseGiteaPackageEvent([]byte(`{}`), http.Header{}) if res.ok { t.Errorf("expected ok=false when header missing") } } func TestParseGitHubPackageEvent_RegistryPackage(t *testing.T) { body := []byte(`{ "action": "published", "registry_package": { "name": "my-app", "namespace": "owner", "package_type": "CONTAINER", "package_version": { "name": "sha256:deadbeef", "container_metadata": {"tag": {"name": "v2.0.0"}} }, "registry": {"url": "https://ghcr.io/owner/my-app"} } }`) res := parseGitHubPackageEvent(body, headerWith(headerGitHubEvent, "registry_package")) if !res.ok || res.err != nil { t.Fatalf("ok=%v err=%v", res.ok, res.err) } if res.event.Image == nil || res.event.Image.Tag != "v2.0.0" { t.Errorf("Tag mismatch: %+v", res.event.Image) } if res.event.Image.Digest != "sha256:deadbeef" { t.Errorf("Digest=%q want sha256:deadbeef", res.event.Image.Digest) } if res.event.Image.Repo != "owner/my-app" { t.Errorf("Repo=%q want owner/my-app", res.event.Image.Repo) } if res.event.Image.Registry != "ghcr.io" { t.Errorf("Registry=%q want ghcr.io", res.event.Image.Registry) } } func TestParseGitHubPackageEvent_PackageAlias(t *testing.T) { // Older webhooks deliver under "package" with event name "package". body := []byte(`{ "action": "published", "package": { "name": "img", "namespace": "org", "package_type": "container", "package_version": {"container_metadata": {"tag": {"name": "latest"}}}, "registry": {"url": "https://ghcr.io/"} } }`) res := parseGitHubPackageEvent(body, headerWith(headerGitHubEvent, "package")) if !res.ok || res.err != nil { t.Fatalf("ok=%v err=%v", res.ok, res.err) } if res.event.Image.Repo != "org/img" { t.Errorf("Repo=%q want org/img", res.event.Image.Repo) } if res.event.Image.Tag != "latest" { t.Errorf("Tag=%q want latest", res.event.Image.Tag) } } func TestParseGitHubPushEvent_StampsVendor(t *testing.T) { body := []byte(`{ "ref": "refs/heads/main", "after": "abc", "repository": {"full_name": "owner/repo"}, "pusher": {"name": "alice"} }`) res := parseGitHubPushEvent(body, headerWith(headerGitHubEvent, "push")) if !res.ok || res.err != nil { t.Fatalf("ok=%v err=%v", res.ok, res.err) } if res.event.Git == nil || res.event.Git.Vendor != "github" { t.Errorf("Vendor=%q want github (git=%+v)", "", res.event.Git) } if res.event.Git.Branch != "main" { t.Errorf("Branch=%q want main", res.event.Git.Branch) } } func TestParseGiteaPushEvent_StampsVendor(t *testing.T) { body := []byte(`{ "ref": "refs/tags/v1.0", "after": "deadbeef", "repository": {"full_name": "alexei/app"}, "pusher": {"username": "alexei"} }`) res := parseGiteaPushEvent(body, headerWith(headerGiteaEvent, "push")) if !res.ok || res.err != nil { t.Fatalf("ok=%v err=%v", res.ok, res.err) } if res.event.Kind != "git-tag" { t.Errorf("Kind=%q want git-tag", res.event.Kind) } if res.event.Git.Vendor != "gitea" { t.Errorf("Vendor=%q want gitea", res.event.Git.Vendor) } if res.event.Git.Tag != "v1.0" { t.Errorf("Tag=%q want v1.0", res.event.Git.Tag) } if res.event.Git.Pusher != "alexei" { t.Errorf("Pusher=%q want alexei", res.event.Git.Pusher) } } func TestParseGitLabPushEvent(t *testing.T) { body := []byte(`{ "ref": "refs/heads/develop", "after": "feedface", "user_username": "bob", "project": {"path_with_namespace": "group/proj", "git_http_url": "https://gitlab.example.com/group/proj.git"} }`) res := parseGitLabPushEvent(body, headerWith(headerGitLabEvent, "Push Hook")) if !res.ok || res.err != nil { t.Fatalf("ok=%v err=%v", res.ok, res.err) } if res.event.Git.Vendor != "gitlab" { t.Errorf("Vendor=%q want gitlab", res.event.Git.Vendor) } if res.event.Git.Repo != "group/proj" { t.Errorf("Repo=%q want group/proj", res.event.Git.Repo) } if res.event.Git.Branch != "develop" { t.Errorf("Branch=%q want develop", res.event.Git.Branch) } if res.event.Git.Pusher != "bob" { t.Errorf("Pusher=%q want bob", res.event.Git.Pusher) } } func TestParseGitLabTagPushEvent(t *testing.T) { body := []byte(`{ "ref": "refs/tags/v3", "after": "abc", "user_name": "carol", "project": {"path_with_namespace": "g/p"} }`) res := parseGitLabPushEvent(body, headerWith(headerGitLabEvent, "Tag Push Hook")) if !res.ok || res.err != nil { t.Fatalf("ok=%v err=%v", res.ok, res.err) } if res.event.Kind != "git-tag" || res.event.Git.Tag != "v3" { t.Errorf("Tag mismatch: kind=%q git=%+v", res.event.Kind, res.event.Git) } if res.event.Git.Pusher != "carol" { t.Errorf("Pusher=%q want carol (user_name fallback)", res.event.Git.Pusher) } } func TestBuildInboundEvent_GiteaPackageRouted(t *testing.T) { body := []byte(`{ "action": "created", "package": { "name": "svc", "type": "container", "version": "v9", "owner": {"login": "alexei"}, "repository": {"html_url": "https://git.example.com/alexei/svc"} } }`) evt, err := buildInboundEvent(body, headerWith(headerGiteaEvent, "package")) if err != nil { t.Fatalf("unexpected err: %v", err) } if evt.Image == nil || evt.Image.Tag != "v9" { t.Errorf("expected Gitea-routed image-push, got %+v", evt) } // RawBody and Headers should round-trip even via the vendor branch. if len(evt.RawBody) == 0 { t.Errorf("RawBody should be attached") } if http.Header(evt.Headers).Get(headerGiteaEvent) != "package" { t.Errorf("Headers not attached") } } func TestBuildInboundEvent_FallbackToGeneric(t *testing.T) { // No vendor header — generic simple-body parser still works. body := []byte(`{"image":"reg.example.com/x/y:tag"}`) evt, err := buildInboundEvent(body, http.Header{}) if err != nil { t.Fatalf("unexpected err: %v", err) } if evt.Image == nil || evt.Image.Tag != "tag" { t.Errorf("generic fallback failed: %+v", evt) } } func TestBuildInboundEvent_GenericRefStillSupported(t *testing.T) { body := []byte(`{"ref":"refs/heads/main","repository":{"full_name":"a/b"},"after":"sha"}`) evt, err := buildInboundEvent(body, http.Header{}) if err != nil { t.Fatalf("unexpected err: %v", err) } if evt.Kind != "git-push" || evt.Git == nil || evt.Git.Branch != "main" { t.Errorf("generic ref parse failed: %+v", evt) } // Generic path does NOT stamp a vendor — only the vendor-header // paths do, so trigger plugins can tell them apart. if evt.Git.Vendor != "" { t.Errorf("generic parser should leave Vendor empty, got %q", evt.Git.Vendor) } } func TestBuildInboundEvent_VendorErrorDoesNotFallThrough(t *testing.T) { // A request with the Gitea header but a non-container package should // error out cleanly rather than fall through to the generic parser // (which would silently accept the body as JSON-with-no-fields). body := []byte(`{"package":{"name":"x","type":"npm","version":"1.0"}}`) _, err := buildInboundEvent(body, headerWith(headerGiteaEvent, "package")) if err == nil { t.Fatal("expected error for non-container Gitea package") } if !strings.Contains(err.Error(), "container") { t.Errorf("error should mention container, got %v", err) } } func TestRegistryHostFromGiteaRepoURL(t *testing.T) { cases := []struct{ in, out string }{ {"https://git.example.com/owner/repo", "git.example.com"}, {"http://localhost:3000/o/r", "localhost:3000"}, {"git.example.com/owner/repo", "git.example.com"}, {"", ""}, } for _, c := range cases { got := registryHostFromGiteaRepoURL(c.in) if got != c.out { t.Errorf("registryHostFromGiteaRepoURL(%q) = %q, want %q", c.in, got, c.out) } } }