Files
tiny-forge/internal/webhook/matcher_test.go
T
alexei.dolgolyov 0632f512e6
Build / build (push) Successful in 10m25s
feat(webhook): per-project and per-site webhook URLs
Replace the single global webhook secret with entity-scoped secrets stored
on each project and static site. Webhook-driven project autocreate is
removed — projects must exist before their URL can trigger deploys.

Also wires static-site webhooks (sync_trigger=push|tag), turning the
previously inert "push" trigger into a functional one: POST the site's
webhook URL from a Git provider and Tinyforge re-syncs on matching refs.

- Adds webhook_secret columns + unique indexes to projects and static_sites
- Per-entity GET/regenerate endpoints under /api/projects/{id}/webhook
  and /api/sites/{id}/webhook (admin-only)
- Removes /api/settings/webhook-url and the global webhook panel
- Reusable WebhookPanel Svelte component on both detail pages, i18n in en/ru
- Tests for matcher (siteRefMatches, ParseImageRef) and handler (project
  match/mismatch/404 and site push/manual/branch-skip)
2026-04-23 15:18:19 +03:00

99 lines
2.5 KiB
Go

package webhook
import (
"testing"
"github.com/alexei/tinyforge/internal/store"
)
func TestSiteRefMatches_Push(t *testing.T) {
t.Parallel()
site := store.StaticSite{SyncTrigger: "push", Branch: "main"}
cases := []struct {
ref string
want bool
}{
{"refs/heads/main", true},
{"refs/heads/develop", false},
{"refs/tags/v1.0.0", false},
{"", false},
{"main", false},
}
for _, tc := range cases {
if got := siteRefMatches(site, tc.ref); got != tc.want {
t.Errorf("siteRefMatches(push, %q) = %v; want %v", tc.ref, got, tc.want)
}
}
}
func TestSiteRefMatches_PushEmptyBranchAcceptsAny(t *testing.T) {
t.Parallel()
// When Branch is unset, any heads ref should match — tolerates the sites
// table having blank Branch values from legacy rows.
site := store.StaticSite{SyncTrigger: "push"}
if !siteRefMatches(site, "refs/heads/whatever") {
t.Error("expected empty Branch to accept any heads ref")
}
if siteRefMatches(site, "refs/tags/v1") {
t.Error("empty Branch must still reject tag refs")
}
}
func TestSiteRefMatches_Tag(t *testing.T) {
t.Parallel()
site := store.StaticSite{SyncTrigger: "tag", TagPattern: "v*"}
cases := []struct {
ref string
want bool
}{
{"refs/tags/v1.0.0", true},
{"refs/tags/v2", true},
{"refs/tags/hotfix", false},
{"refs/heads/main", false},
}
for _, tc := range cases {
if got := siteRefMatches(site, tc.ref); got != tc.want {
t.Errorf("siteRefMatches(tag, %q) = %v; want %v", tc.ref, got, tc.want)
}
}
}
func TestSiteRefMatches_ManualIsIgnored(t *testing.T) {
t.Parallel()
site := store.StaticSite{SyncTrigger: "manual", Branch: "main"}
if siteRefMatches(site, "refs/heads/main") {
t.Error("manual trigger must never match any ref — caller short-circuits")
}
}
func TestParseImageRef(t *testing.T) {
t.Parallel()
cases := []struct {
in string
wantFull string
wantTag string
}{
{"registry.example.com/alexei/app:v1", "alexei/app", "v1"},
{"alexei/app:dev", "alexei/app", "dev"},
{"app", "app", ""},
}
for _, tc := range cases {
got, err := ParseImageRef(tc.in)
if err != nil {
t.Errorf("ParseImageRef(%q) unexpected error: %v", tc.in, err)
continue
}
if got.FullName() != tc.wantFull || got.Tag != tc.wantTag {
t.Errorf("ParseImageRef(%q) = %q:%q; want %q:%q",
tc.in, got.FullName(), got.Tag, tc.wantFull, tc.wantTag)
}
}
}
func TestParseImageRef_Empty(t *testing.T) {
t.Parallel()
if _, err := ParseImageRef(""); err == nil {
t.Error("expected error for empty image ref")
}
}