c8e71a0c34
Three packages (api, reconciler, webhook) each carried a private 30-line toPluginWorkload() copy that had drifted — only the api version logged malformed public_faces JSON; the others swallowed it. Hoist the single implementation to plugin.WorkloadFromStore() (convert.go); store is already a plugin dependency so no new import edge or cycle forms. Likewise the dockerfile and static sources each had a private removeContainerByName() that disagreed (remove-all vs stop-at-first). Docker enforces unique container names, so the two were equivalent for every reachable state; converge on plugin.RemoveContainerByName() (container.go, stop-at-first) with a note on why remove-all was moot. Callers migrated; old copies removed. Adds convert_test.go pinning the field-by-field contract and JSON edge cases.
130 lines
4.8 KiB
Go
130 lines
4.8 KiB
Go
package plugin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/alexei/tinyforge/internal/store"
|
|
)
|
|
|
|
// TestWorkloadFromStore_MapsEveryField pins the full field-for-field contract
|
|
// of the consolidated converter. The chief risk of extracting the three former
|
|
// per-package copies into one shared function is a silently dropped field —
|
|
// especially the three secrets (json:"-", so serialization-based tests can
|
|
// never catch a regression here) and the GroupID<-AppID rename.
|
|
func TestWorkloadFromStore_MapsEveryField(t *testing.T) {
|
|
faces := []PublicFace{
|
|
{Subdomain: "app", Domain: "example.com", TargetService: "web", TargetPort: 8080, AccessListID: 3, EnableSSL: true},
|
|
{Subdomain: "api", Domain: "example.com", TargetPort: 9090},
|
|
}
|
|
facesJSON, err := json.Marshal(faces)
|
|
if err != nil {
|
|
t.Fatalf("marshal faces: %v", err)
|
|
}
|
|
|
|
src := store.Workload{
|
|
ID: "wl-1",
|
|
Name: "my-workload",
|
|
AppID: "grp-7",
|
|
SourceKind: "dockerfile",
|
|
SourceConfig: `{"repo":"x"}`,
|
|
TriggerKind: "git",
|
|
TriggerConfig: `{"branch":"main"}`,
|
|
PublicFaces: string(facesJSON),
|
|
ParentWorkloadID: "parent-2",
|
|
NotificationURL: "https://hooks.example.com/notify",
|
|
NotificationSecret: "notif-secret",
|
|
WebhookSecret: "wh-secret",
|
|
WebhookSigningSecret: "wh-signing-secret",
|
|
WebhookRequireSignature: true,
|
|
CreatedAt: "2026-01-01T00:00:00Z",
|
|
UpdatedAt: "2026-01-02T00:00:00Z",
|
|
}
|
|
|
|
got := WorkloadFromStore(src)
|
|
|
|
if got.ID != src.ID {
|
|
t.Errorf("ID = %q, want %q", got.ID, src.ID)
|
|
}
|
|
if got.Name != src.Name {
|
|
t.Errorf("Name = %q, want %q", got.Name, src.Name)
|
|
}
|
|
if got.GroupID != src.AppID {
|
|
t.Errorf("GroupID = %q, want AppID %q", got.GroupID, src.AppID)
|
|
}
|
|
if got.ParentWorkloadID != src.ParentWorkloadID {
|
|
t.Errorf("ParentWorkloadID = %q, want %q", got.ParentWorkloadID, src.ParentWorkloadID)
|
|
}
|
|
if got.SourceKind != src.SourceKind {
|
|
t.Errorf("SourceKind = %q, want %q", got.SourceKind, src.SourceKind)
|
|
}
|
|
if string(got.SourceConfig) != src.SourceConfig {
|
|
t.Errorf("SourceConfig = %q, want %q", string(got.SourceConfig), src.SourceConfig)
|
|
}
|
|
if got.TriggerKind != src.TriggerKind {
|
|
t.Errorf("TriggerKind = %q, want %q", got.TriggerKind, src.TriggerKind)
|
|
}
|
|
if string(got.TriggerConfig) != src.TriggerConfig {
|
|
t.Errorf("TriggerConfig = %q, want %q", string(got.TriggerConfig), src.TriggerConfig)
|
|
}
|
|
if got.NotificationURL != src.NotificationURL {
|
|
t.Errorf("NotificationURL = %q, want %q", got.NotificationURL, src.NotificationURL)
|
|
}
|
|
if got.NotificationSecret != src.NotificationSecret {
|
|
t.Errorf("NotificationSecret not carried through")
|
|
}
|
|
if got.WebhookSecret != src.WebhookSecret {
|
|
t.Errorf("WebhookSecret not carried through")
|
|
}
|
|
if got.WebhookSigningSecret != src.WebhookSigningSecret {
|
|
t.Errorf("WebhookSigningSecret not carried through")
|
|
}
|
|
if got.WebhookRequireSignature != src.WebhookRequireSignature {
|
|
t.Errorf("WebhookRequireSignature = %v, want %v", got.WebhookRequireSignature, src.WebhookRequireSignature)
|
|
}
|
|
if got.CreatedAt != src.CreatedAt {
|
|
t.Errorf("CreatedAt = %q, want %q", got.CreatedAt, src.CreatedAt)
|
|
}
|
|
if got.UpdatedAt != src.UpdatedAt {
|
|
t.Errorf("UpdatedAt = %q, want %q", got.UpdatedAt, src.UpdatedAt)
|
|
}
|
|
|
|
if len(got.PublicFaces) != len(faces) {
|
|
t.Fatalf("PublicFaces len = %d, want %d", len(got.PublicFaces), len(faces))
|
|
}
|
|
if got.PublicFaces[0] != faces[0] || got.PublicFaces[1] != faces[1] {
|
|
t.Errorf("PublicFaces = %+v, want %+v", got.PublicFaces, faces)
|
|
}
|
|
}
|
|
|
|
// TestWorkloadFromStore_PublicFaces covers the PublicFaces decode branch,
|
|
// including the malformed-JSON path that the consolidation newly unified onto
|
|
// "log and treat as empty" for every caller (the old reconciler/webhook copies
|
|
// silently swallowed the error). A decode failure must never fail the
|
|
// conversion or panic — it yields nil faces.
|
|
func TestWorkloadFromStore_PublicFaces(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
raw string
|
|
wantLen int
|
|
wantNil bool
|
|
}{
|
|
{name: "empty string yields nil", raw: "", wantLen: 0, wantNil: true},
|
|
{name: "empty array yields empty", raw: "[]", wantLen: 0, wantNil: false},
|
|
{name: "malformed json yields nil", raw: "{not-json", wantLen: 0, wantNil: true},
|
|
{name: "wrong-shape json yields nil", raw: `{"a":1}`, wantLen: 0, wantNil: true},
|
|
{name: "single valid face", raw: `[{"Subdomain":"a"}]`, wantLen: 1, wantNil: false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := WorkloadFromStore(store.Workload{ID: "wl", PublicFaces: tt.raw})
|
|
if len(got.PublicFaces) != tt.wantLen {
|
|
t.Errorf("PublicFaces len = %d, want %d", len(got.PublicFaces), tt.wantLen)
|
|
}
|
|
if tt.wantNil && got.PublicFaces != nil {
|
|
t.Errorf("PublicFaces = %+v, want nil", got.PublicFaces)
|
|
}
|
|
})
|
|
}
|
|
}
|