0632f512e6
Build / build (push) Successful in 10m25s
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)
99 lines
2.5 KiB
Go
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")
|
|
}
|
|
}
|