package registry import ( "context" "encoding/json" "testing" "github.com/alexei/tinyforge/internal/workload/plugin" ) func mustConfig(t *testing.T, c Config) json.RawMessage { t.Helper() b, err := json.Marshal(c) if err != nil { t.Fatalf("marshal config: %v", err) } return b } func TestValidate(t *testing.T) { tr := &trigger{} cases := []struct { name string cfg json.RawMessage wantErr bool }{ {"empty body rejected", nil, true}, {"missing image rejected", mustConfig(t, Config{TagPattern: "*"}), true}, {"valid wildcard", mustConfig(t, Config{Image: "owner/app", TagPattern: "*"}), false}, {"valid with glob", mustConfig(t, Config{Image: "registry.example.com/owner/app", TagPattern: "v*"}), false}, {"invalid glob", mustConfig(t, Config{Image: "owner/app", TagPattern: "v[oops"}), true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { err := tr.Validate(tc.cfg) if (err != nil) != tc.wantErr { t.Fatalf("Validate(%s) err=%v want err=%v", tc.name, err, tc.wantErr) } }) } } func TestImageMatches(t *testing.T) { cases := []struct { name string want, got string shouldMatch bool }{ {"exact qualified", "registry.example.com/owner/app", "registry.example.com/owner/app", true}, {"host case-insensitive", "REGISTRY.example.com/owner/app", "registry.example.com/owner/app", true}, {"path mismatch", "registry.example.com/owner/app", "registry.example.com/owner/other", false}, {"different registry", "a.example.com/owner/app", "b.example.com/owner/app", false}, // Single-segment images (Docker Hub officials) — recently fixed. {"both single-segment equal", "nginx", "nginx", true}, {"both single-segment unequal", "nginx", "postgres", false}, {"want single, got qualified", "nginx", "library/nginx", false}, {"want qualified, got single", "library/nginx", "nginx", false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := imageMatches(tc.want, tc.got); got != tc.shouldMatch { t.Errorf("imageMatches(%q, %q) = %v, want %v", tc.want, tc.got, got, tc.shouldMatch) } }) } } func TestMatch(t *testing.T) { tr := &trigger{} cfg := mustConfig(t, Config{Image: "registry.example.com/owner/app", TagPattern: "v*"}) wl := plugin.Workload{ ID: "wkl-1", TriggerConfig: cfg, } t.Run("wrong event kind returns nil", func(t *testing.T) { evt := plugin.InboundEvent{Kind: "git-push"} intent, err := tr.Match(context.Background(), plugin.Deps{}, wl, evt) if err != nil || intent != nil { t.Fatalf("expected nil intent, got intent=%v err=%v", intent, err) } }) t.Run("matching push produces intent", func(t *testing.T) { evt := plugin.InboundEvent{ Kind: "image-push", Image: &plugin.ImagePushEvent{ Registry: "registry.example.com", Repo: "owner/app", Tag: "v1.2.3", }, } intent, err := tr.Match(context.Background(), plugin.Deps{}, wl, evt) if err != nil { t.Fatalf("unexpected error: %v", err) } if intent == nil { t.Fatal("expected non-nil intent") } if intent.Reference != "v1.2.3" { t.Errorf("intent.Reference = %q, want v1.2.3", intent.Reference) } if intent.Reason != "registry-push" { t.Errorf("intent.Reason = %q, want registry-push", intent.Reason) } }) t.Run("tag outside glob returns nil", func(t *testing.T) { evt := plugin.InboundEvent{ Kind: "image-push", Image: &plugin.ImagePushEvent{ Registry: "registry.example.com", Repo: "owner/app", Tag: "latest", // doesn't match v* }, } intent, err := tr.Match(context.Background(), plugin.Deps{}, wl, evt) if err != nil || intent != nil { t.Fatalf("expected nil intent for tag=latest, got intent=%v err=%v", intent, err) } }) t.Run("wrong repo returns nil", func(t *testing.T) { evt := plugin.InboundEvent{ Kind: "image-push", Image: &plugin.ImagePushEvent{ Registry: "registry.example.com", Repo: "owner/other", Tag: "v1.0.0", }, } intent, err := tr.Match(context.Background(), plugin.Deps{}, wl, evt) if err != nil || intent != nil { t.Fatalf("expected nil intent for wrong repo, got intent=%v err=%v", intent, err) } }) t.Run("empty pattern matches anything", func(t *testing.T) { wlAny := plugin.Workload{ ID: "wkl-any", TriggerConfig: mustConfig(t, Config{Image: "owner/app"}), } evt := plugin.InboundEvent{ Kind: "image-push", Image: &plugin.ImagePushEvent{Repo: "owner/app", Tag: "latest"}, } intent, err := tr.Match(context.Background(), plugin.Deps{}, wlAny, evt) if err != nil { t.Fatalf("unexpected error: %v", err) } if intent == nil { t.Fatal("expected match with empty pattern") } }) }