package apps import ( "path/filepath" "strings" "testing" "codeberg.org/VARASYS/ZDDC/zddc/internal/zddc" ) // ── ParseSpec ──────────────────────────────────────────────────────────── func TestParseSpec_Channels(t *testing.T) { cases := []struct { spec, wantChan string }{ {"stable", "stable"}, {"beta", "beta"}, {"alpha", "alpha"}, {":stable", "stable"}, {":beta", "beta"}, {":alpha", "alpha"}, } for _, tc := range cases { t.Run(tc.spec, func(t *testing.T) { c, err := ParseSpec(tc.spec, "/root", "/root") if err != nil { t.Fatalf("ParseSpec error: %v", err) } if c.Channel != tc.wantChan { t.Errorf("got Channel=%q, want %q", c.Channel, tc.wantChan) } if c.URLPrefix != "" || c.Path != "" || c.FullURL != "" { t.Errorf("expected channel-only, got %+v", c) } }) } } func TestParseSpec_Versions(t *testing.T) { cases := []struct { spec, wantChan string }{ {"v0.0.4", "v0.0.4"}, {"0.0.4", "v0.0.4"}, {"v0.0", "v0.0"}, {"0.0", "v0.0"}, {"v0", "v0"}, {"0", "v0"}, {":v0.0.4", "v0.0.4"}, {":0.0.4", "v0.0.4"}, {":v0", "v0"}, } for _, tc := range cases { t.Run(tc.spec, func(t *testing.T) { c, err := ParseSpec(tc.spec, "/root", "/root") if err != nil { t.Fatalf("ParseSpec error: %v", err) } if c.Channel != tc.wantChan { t.Errorf("got Channel=%q, want %q", c.Channel, tc.wantChan) } }) } } func TestParseSpec_URLPrefix(t *testing.T) { cases := []struct { spec, wantPrefix, wantChan string }{ {"https://my-mirror.example/releases", "https://my-mirror.example/releases", ""}, {"https://my-mirror.example/releases/", "https://my-mirror.example/releases", ""}, // trailing slash stripped {"https://my-mirror.example/releases:stable", "https://my-mirror.example/releases", "stable"}, {"https://my-mirror.example/releases:beta", "https://my-mirror.example/releases", "beta"}, {"https://my-mirror.example/releases:v0.0.4", "https://my-mirror.example/releases", "v0.0.4"}, // Port colon must NOT be confused with channel separator. {"https://my-mirror.example:8080/releases", "https://my-mirror.example:8080/releases", ""}, {"https://my-mirror.example:8080/releases:stable", "https://my-mirror.example:8080/releases", "stable"}, // Colon embedded in path before final slash — treated as part of path. {"https://host/some:thing/releases", "https://host/some:thing/releases", ""}, {"https://host/some:thing/releases:beta", "https://host/some:thing/releases", "beta"}, } for _, tc := range cases { t.Run(tc.spec, func(t *testing.T) { c, err := ParseSpec(tc.spec, "/root", "/root") if err != nil { t.Fatalf("ParseSpec error: %v", err) } if c.URLPrefix != tc.wantPrefix { t.Errorf("got URLPrefix=%q, want %q", c.URLPrefix, tc.wantPrefix) } if c.Channel != tc.wantChan { t.Errorf("got Channel=%q, want %q", c.Channel, tc.wantChan) } if c.Path != "" || c.FullURL != "" { t.Errorf("expected non-terminal, got %+v", c) } }) } } func TestParseSpec_FullURL(t *testing.T) { c, err := ParseSpec("https://my-fork.example/archive.html", "/root", "/root") if err != nil { t.Fatalf("ParseSpec error: %v", err) } if c.FullURL != "https://my-fork.example/archive.html" { t.Errorf("got FullURL=%q", c.FullURL) } if !c.IsTerminal() { t.Errorf("expected terminal") } } func TestParseSpec_FullURLWithChannelSuffixRejected(t *testing.T) { _, err := ParseSpec("https://my-fork.example/archive.html:stable", "/root", "/root") if err == nil { t.Errorf("expected error for .html URL with :suffix") } } func TestParseSpec_Paths(t *testing.T) { root := t.TempDir() zddcDir := filepath.Join(root, "Project-X") cases := []struct { spec string wantOK bool wantPath string }{ {"./local.html", true, filepath.Join(zddcDir, "local.html")}, {"../sibling.html", true, filepath.Join(root, "sibling.html")}, {filepath.Join(root, "abs.html"), true, filepath.Join(root, "abs.html")}, {"/etc/passwd", false, ""}, {"../../../escape.html", false, ""}, } for _, tc := range cases { t.Run(tc.spec, func(t *testing.T) { c, err := ParseSpec(tc.spec, zddcDir, root) if tc.wantOK { if err != nil { t.Fatalf("want success, got error: %v", err) } if c.Path != tc.wantPath { t.Errorf("got Path=%q, want %q", c.Path, tc.wantPath) } if !c.IsTerminal() { t.Errorf("expected terminal") } } else { if err == nil { t.Errorf("want error, got %+v", c) } } }) } } func TestParseSpec_Errors(t *testing.T) { cases := []string{ "", "weird-thing", ":", ":weird", "v", "v0.0.0.0", "v0.a.0", "https://", // missing host } for _, tc := range cases { t.Run(tc, func(t *testing.T) { _, err := ParseSpec(tc, "/root", "/root") if err == nil { t.Errorf("ParseSpec(%q) = nil, want error", tc) } }) } } // ── Resolve ────────────────────────────────────────────────────────────── func TestResolve_NoEntries(t *testing.T) { chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{}}} _, has, err := Resolve(chain, "archive", t.TempDir(), t.TempDir()) if err != nil { t.Fatalf("Resolve: %v", err) } if has { t.Errorf("got override=true, want false") } } func TestResolve_PerAppChannelOnly(t *testing.T) { root := t.TempDir() chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{ Apps: map[string]string{"archive": "beta"}, }}} src, has, err := Resolve(chain, "archive", root, root) if err != nil || !has { t.Fatalf("has=%v err=%v", has, err) } want := DefaultUpstreamReleases + "/archive_beta.html" if src.URL != want { t.Errorf("got URL=%q, want %q", src.URL, want) } } func TestResolve_PerAppVersionOnly(t *testing.T) { root := t.TempDir() chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{ Apps: map[string]string{"archive": "v0.0.4"}, }}} src, _, err := Resolve(chain, "archive", root, root) if err != nil { t.Fatal(err) } want := DefaultUpstreamReleases + "/archive_v0.0.4.html" if src.URL != want { t.Errorf("got URL=%q, want %q", src.URL, want) } } func TestResolve_DefaultProvidesURLAndChannel(t *testing.T) { root := t.TempDir() chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{ Apps: map[string]string{ "default": "https://mirror.example/releases:beta", }, }}} src, has, err := Resolve(chain, "archive", root, root) if err != nil || !has { t.Fatalf("has=%v err=%v", has, err) } if src.URL != "https://mirror.example/releases/archive_beta.html" { t.Errorf("got URL=%q", src.URL) } } func TestResolve_DefaultPlusPerAppChannelOverride(t *testing.T) { // User's example: default=https://zddc.varasys.io/releases:stable, // classifier=:beta → mirror URL with classifier_beta.html. root := t.TempDir() chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{ Apps: map[string]string{ "default": "https://zddc.varasys.io/releases:stable", "classifier": ":beta", }, }}} src, _, err := Resolve(chain, "classifier", root, root) if err != nil { t.Fatal(err) } if src.URL != "https://zddc.varasys.io/releases/classifier_beta.html" { t.Errorf("got URL=%q", src.URL) } } func TestResolve_DefaultPlusPerAppURLPrefixOverride(t *testing.T) { // User's example: default=...:stable, archive=https://my.local.stuff/releases // → custom URL + default channel (stable). root := t.TempDir() chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{ Apps: map[string]string{ "default": "https://zddc.varasys.io/releases:stable", "archive": "https://my.local.stuff/releases", }, }}} src, _, err := Resolve(chain, "archive", root, root) if err != nil { t.Fatal(err) } if src.URL != "https://my.local.stuff/releases/archive_stable.html" { t.Errorf("got URL=%q", src.URL) } } func TestResolve_DeeperLevelOverridesParentChannel(t *testing.T) { root := t.TempDir() requestDir := filepath.Join(root, "Project-A") chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{ {Apps: map[string]string{"default": ":stable"}}, {Apps: map[string]string{"default": ":beta"}}, }} src, _, err := Resolve(chain, "archive", root, requestDir) if err != nil { t.Fatal(err) } want := DefaultUpstreamReleases + "/archive_beta.html" if src.URL != want { t.Errorf("got URL=%q, want %q", src.URL, want) } } func TestResolve_DeeperLevelOverridesParentURL(t *testing.T) { root := t.TempDir() requestDir := filepath.Join(root, "Project-A") chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{ {Apps: map[string]string{"default": "https://a.example/releases:stable"}}, {Apps: map[string]string{"default": "https://b.example/releases"}}, }} src, _, err := Resolve(chain, "archive", root, requestDir) if err != nil { t.Fatal(err) } // b.example URL prefix wins; channel inherited (stable). want := "https://b.example/releases/archive_stable.html" if src.URL != want { t.Errorf("got URL=%q, want %q", src.URL, want) } } func TestResolve_TerminalAtLeafBeatsParentDefault(t *testing.T) { root := t.TempDir() requestDir := filepath.Join(root, "Project-A") chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{ {Apps: map[string]string{"default": "https://a.example/releases:stable"}}, {Apps: map[string]string{"archive": "https://my-fork.example/archive.html"}}, }} src, _, err := Resolve(chain, "archive", root, requestDir) if err != nil { t.Fatal(err) } if src.URL != "https://my-fork.example/archive.html" { t.Errorf("got URL=%q (want terminal full URL)", src.URL) } } func TestResolve_DeeperNonTerminalOverridesParentTerminal(t *testing.T) { root := t.TempDir() requestDir := filepath.Join(root, "Project-A") chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{ {Apps: map[string]string{"archive": "https://a.example/archive.html"}}, // terminal {Apps: map[string]string{"archive": "alpha"}}, // non-terminal }} src, _, err := Resolve(chain, "archive", root, requestDir) if err != nil { t.Fatal(err) } want := DefaultUpstreamReleases + "/archive_alpha.html" if src.URL != want { t.Errorf("got URL=%q, want %q", src.URL, want) } } func TestResolve_PathSourceTerminal(t *testing.T) { root := t.TempDir() projDir := filepath.Join(root, "Project-X") chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{ {}, {Apps: map[string]string{"archive": "./our-archive.html"}}, }} src, _, err := Resolve(chain, "archive", root, projDir) if err != nil { t.Fatal(err) } if src.URL != "" { t.Errorf("got URL=%q, want empty", src.URL) } want := filepath.Join(projDir, "our-archive.html") if src.Path != want { t.Errorf("got Path=%q, want %q", src.Path, want) } } func TestResolve_PerAppOverridesDefaultAtSameLevel(t *testing.T) { root := t.TempDir() chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{ Apps: map[string]string{ "default": "https://a.example/releases:stable", "archive": "https://b.example/archive.html", // terminal — wins for archive only }, }}} src, _, err := Resolve(chain, "archive", root, root) if err != nil { t.Fatal(err) } if src.URL != "https://b.example/archive.html" { t.Errorf("got URL=%q (want b.example terminal)", src.URL) } // Other apps still use the default. src2, _, err := Resolve(chain, "classifier", root, root) if err != nil { t.Fatal(err) } if src2.URL != "https://a.example/releases/classifier_stable.html" { t.Errorf("got classifier URL=%q (want a.example default)", src2.URL) } } func TestResolve_BadSpecBubblesError(t *testing.T) { root := t.TempDir() chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{ Apps: map[string]string{"archive": "this is garbage"}, }}} _, _, err := Resolve(chain, "archive", root, root) if err == nil { t.Errorf("expected error") } } func TestResolve_UnknownAppRejected(t *testing.T) { root := t.TempDir() _, _, err := Resolve(zddc.PolicyChain{}, "unknown", root, root) if err == nil { t.Errorf("expected error") } } // ── PreviewLine ────────────────────────────────────────────────────────── func TestPreviewLine(t *testing.T) { root := t.TempDir() t.Run("no entries → embedded", func(t *testing.T) { got := PreviewLine(zddc.PolicyChain{Levels: []zddc.ZddcFile{{}}}, "archive", root, root) if !strings.Contains(got, "embedded") { t.Errorf("got %q", got) } }) t.Run("default channel → URL", func(t *testing.T) { chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{Apps: map[string]string{"default": ":beta"}}}} got := PreviewLine(chain, "archive", root, root) if !strings.Contains(got, "archive_beta.html") { t.Errorf("got %q", got) } }) }