package topdata import ( "errors" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project" ) func cdnConsumer() project.AutogenConsumerConfig { return project.AutogenConsumerConfig{ ID: "accessory_visualeffects", Producer: "accessory_visualeffects", Dataset: "visualeffects", Mode: "accessory_visualeffects", Optional: true, AccessoryVisualeffects: project.AccessoryVisualeffectsConfig{ Groups: map[string]project.AccessoryVisualeffectGroupConfig{ "head_accessories": {}, "chest_accessories": {}, "head_decorations": {}, "head_features": {}, }, }, Source: project.AutogenSourceConfig{ Kind: "cdn_channel", ChannelsPath: "releases/haks/channels.json", ManifestPath: "releases/haks/{tag}/vfxs.yml", ReleaseMarkerPath: "releases/haks/{tag}/haks.json", }, } } // cdnServer serves channels.json, vfxs.yml, and haks.json from in-memory maps. // A key absent from the map returns 404. func cdnServer(t *testing.T, files map[string]string) *httptest.Server { t.Helper() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, ok := files[r.URL.Path] if !ok { w.WriteHeader(http.StatusNotFound) return } fmt.Fprint(w, body) })) t.Cleanup(srv.Close) return srv } func TestResolveCDNChannelHappyPath(t *testing.T) { root := testProjectRoot(t) srv := cdnServer(t, map[string]string{ "/releases/haks/channels.json": `{"current":"v1.2.3"}`, "/releases/haks/v1.2.3/vfxs.yml": `assets: - path: vfxs/head_accessories/hat/hfx_bandana.mdl restype: mdl sha256: aaa - path: vfxs/chest_accessories/cape/cfx_cloak.mdl restype: mdl sha256: bbb - path: vfxs/head_accessories/hat/hfx_bandana.tga restype: tga sha256: ccc - path: vfxs/weapons/sword.mdl restype: mdl sha256: ddd `, }) t.Setenv("BUNNY_CDN_BASE", srv.URL) t.Setenv("SOW_TOPDATA_ASSET_CHANNEL", "current") p := testProject(root) c := cdnConsumer() c.Source.CDNBaseEnv = "BUNNY_CDN_BASE" c.Source.ChannelEnv = "SOW_TOPDATA_ASSET_CHANNEL" m, err := resolveCDNChannelManifest(p, c, c.Source, nil) if err != nil { t.Fatalf("resolve failed: %v", err) } if len(m.Entries) != 2 { t.Fatalf("want 2 mdl entries under target groups, got %d: %#v", len(m.Entries), m.Entries) } // Sorted by source: chest_accessories/... before head_accessories/... if m.Entries[0].Source != "chest_accessories/cape/cfx_cloak.mdl" { t.Fatalf("unexpected first entry: %#v", m.Entries[0]) } if m.Entries[1].Source != "head_accessories/hat/hfx_bandana.mdl" || m.Entries[1].Group != "head_accessories" || m.Entries[1].Subgroup != "hat" || m.Entries[1].ModelStem != "hfx_bandana" { t.Fatalf("unexpected derived entry: %#v", m.Entries[1]) } if m.Ref != "v1.2.3" { t.Fatalf("want ref v1.2.3, got %q", m.Ref) } } func TestResolveCDNChannelFailOpen(t *testing.T) { root := testProjectRoot(t) c := cdnConsumer() c.Source.CDNBaseEnv = "BUNNY_CDN_BASE" c.Source.ChannelEnv = "SOW_TOPDATA_ASSET_CHANNEL" t.Setenv("SOW_TOPDATA_ASSET_CHANNEL", "current") t.Run("channels.json unreachable", func(t *testing.T) { t.Setenv("BUNNY_CDN_BASE", "http://127.0.0.1:0") // unroutable _, err := resolveCDNChannelManifest(testProject(root), c, c.Source, nil) if err == nil || !errorIsUnavailable(err) { t.Fatalf("want fail-open (unavailable), got %v", err) } }) t.Run("channel absent", func(t *testing.T) { srv := cdnServer(t, map[string]string{"/releases/haks/channels.json": `{"testing":"v9"}`}) t.Setenv("BUNNY_CDN_BASE", srv.URL) _, err := resolveCDNChannelManifest(testProject(root), c, c.Source, nil) if err == nil || !errorIsUnavailable(err) { t.Fatalf("want fail-open (unavailable), got %v", err) } }) t.Run("vfxs 404 no release marker", func(t *testing.T) { srv := cdnServer(t, map[string]string{"/releases/haks/channels.json": `{"current":"v1"}`}) t.Setenv("BUNNY_CDN_BASE", srv.URL) _, err := resolveCDNChannelManifest(testProject(root), c, c.Source, nil) if err == nil || !errorIsUnavailable(err) { t.Fatalf("want fail-open (unavailable), got %v", err) } }) } func TestResolveCDNChannelHardFail(t *testing.T) { root := testProjectRoot(t) c := cdnConsumer() c.Source.CDNBaseEnv = "BUNNY_CDN_BASE" c.Source.ChannelEnv = "SOW_TOPDATA_ASSET_CHANNEL" t.Setenv("SOW_TOPDATA_ASSET_CHANNEL", "current") t.Run("broken release: vfxs 404 + haks.json present", func(t *testing.T) { srv := cdnServer(t, map[string]string{ "/releases/haks/channels.json": `{"current":"v1"}`, "/releases/haks/v1/haks.json": `{}`, }) t.Setenv("BUNNY_CDN_BASE", srv.URL) _, err := resolveCDNChannelManifest(testProject(root), c, c.Source, nil) if err == nil || errorIsUnavailable(err) { t.Fatalf("want HARD fail, got %v", err) } }) t.Run("malformed vfxs.yml: no assets array", func(t *testing.T) { srv := cdnServer(t, map[string]string{ "/releases/haks/channels.json": `{"current":"v1"}`, "/releases/haks/v1/vfxs.yml": "generated_at: 2026-01-01\n", }) t.Setenv("BUNNY_CDN_BASE", srv.URL) _, err := resolveCDNChannelManifest(testProject(root), c, c.Source, nil) if err == nil || errorIsUnavailable(err) { t.Fatalf("want HARD fail, got %v", err) } }) t.Run("malformed vfxs.yml: syntactically broken YAML", func(t *testing.T) { srv := cdnServer(t, map[string]string{ "/releases/haks/channels.json": `{"current":"v1"}`, "/releases/haks/v1/vfxs.yml": "assets:\n - path: [unterminated", }) t.Setenv("BUNNY_CDN_BASE", srv.URL) _, err := resolveCDNChannelManifest(testProject(root), c, c.Source, nil) if err == nil || errorIsUnavailable(err) { t.Fatalf("want HARD fail on broken YAML, got %v", err) } }) } func TestResolveCDNChannelOfflineOverride(t *testing.T) { root := testProjectRoot(t) vfxs := filepath.Join(root, "vfxs.yml") writeFile(t, vfxs, `assets: - path: vfxs/head_features/scar/hfx_scar.mdl restype: mdl sha256: zzz `) t.Setenv("SOW_VFXS_MANIFEST", vfxs) c := cdnConsumer() c.Source.OfflineOverrideEnv = "SOW_VFXS_MANIFEST" m, err := resolveCDNChannelManifest(testProject(root), c, c.Source, nil) if err != nil { t.Fatalf("offline override failed: %v", err) } if len(m.Entries) != 1 || m.Entries[0].Source != "head_features/scar/hfx_scar.mdl" { t.Fatalf("unexpected offline entries: %#v", m.Entries) } } // TestResolveCDNChannelOfflineUsesManifestBasename proves the resolver derives // the offline checkout filename from manifest_path's basename (part.yml here), // not a hardcoded assets/vfxs.yml, and routes parts_rows through the parts filter. func TestResolveCDNChannelOfflineUsesManifestBasename(t *testing.T) { root := testProjectRoot(t) dir := filepath.Join(root, "manifest-checkout") if err := os.MkdirAll(filepath.Join(dir, "assets"), 0o755); err != nil { t.Fatal(err) } writeFile(t, filepath.Join(dir, "assets", "part.yml"), "assets:\n - {path: part/belt/m/pfa0_belt017.mdl, restype: mdl}\n") t.Setenv("SOW_PART_MANIFEST", dir) c := projectPartsConsumer() c.Source = project.AutogenSourceConfig{ Kind: "cdn_channel", ManifestPath: "releases/haks/{tag}/part.yml", OfflineOverrideEnv: "SOW_PART_MANIFEST", } m, err := resolveCDNChannelManifest(testProject(root), c, c.Source, nil) if err != nil { t.Fatalf("resolve: %v", err) } if len(m.Entries) != 1 || m.Entries[0].RowID != 17 || m.Entries[0].Group != "belt" { t.Fatalf("unexpected entries: %+v", m.Entries) } } func errorIsUnavailable(err error) bool { return errors.Is(err, errAutogenManifestUnavailable) }