04f17f8616
ci / vet, staticcheck, test, build (push) Successful in 2m25s
- Added comprehensive table-driven test suites for all packages: cmd/glint, cicontext, fetcher, graph, linter, model, resolver. Coverage reaches 98%+ statement coverage across the codebase. - Replaced os.Exit calls in cmd/glint with an `exit` variable so tests can capture exit codes without terminating the test process. - Removed unreachable code found during coverage analysis: dead guard in cicontext.parseRegexLiteral; dead len(jobs)==0 branch in graph.Pipeline; skipWin struct field and dead continue in graph.convertToPNG; pipelineSVG return type simplified to string. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
623 lines
22 KiB
Go
623 lines
22 KiB
Go
package graph
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.k3nny.fr/glint/internal/fetcher"
|
|
"git.k3nny.fr/glint/internal/model"
|
|
)
|
|
|
|
// ── mermaidLabel ──────────────────────────────────────────────────────────────
|
|
|
|
func TestMermaidLabel(t *testing.T) {
|
|
cases := []struct{ in, want string }{
|
|
{"plain", "plain"},
|
|
{`has "quotes"`, "has 'quotes'"},
|
|
{"has#hash", "has#hash"},
|
|
{`"quoted"#both`, "'quoted'#both"},
|
|
}
|
|
for _, tc := range cases {
|
|
got := mermaidLabel(tc.in)
|
|
if got != tc.want {
|
|
t.Errorf("mermaidLabel(%q)=%q want %q", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── includeFileList ───────────────────────────────────────────────────────────
|
|
|
|
func TestIncludeFileList(t *testing.T) {
|
|
if got := includeFileList("file.yml"); len(got) != 1 || got[0] != "file.yml" {
|
|
t.Error("string form")
|
|
}
|
|
if got := includeFileList([]any{"a.yml", "b.yml"}); len(got) != 2 {
|
|
t.Error("slice form")
|
|
}
|
|
if got := includeFileList([]any{"ok.yml", 42}); len(got) != 1 {
|
|
t.Error("mixed slice drops non-strings")
|
|
}
|
|
if got := includeFileList(nil); got != nil {
|
|
t.Error("nil should return nil")
|
|
}
|
|
if got := includeFileList(123); got != nil {
|
|
t.Error("unknown type returns nil")
|
|
}
|
|
}
|
|
|
|
// ── parseComponentRef ─────────────────────────────────────────────────────────
|
|
|
|
func TestParseComponentRef(t *testing.T) {
|
|
cases := []struct {
|
|
ref string
|
|
wantHost string
|
|
wantProj string
|
|
wantComp string
|
|
wantVer string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
ref: "gitlab.com/group/project/component@v1.0",
|
|
wantHost: "gitlab.com", wantProj: "group/project", wantComp: "component", wantVer: "v1.0",
|
|
},
|
|
{
|
|
ref: "gitlab.com/a/b/c/d@main",
|
|
wantHost: "gitlab.com", wantProj: "a/b/c", wantComp: "d", wantVer: "main",
|
|
},
|
|
{ref: "no-at-sign", wantErr: true},
|
|
{ref: "no-at-sign@", wantErr: true},
|
|
{ref: "host/comp@v1", wantErr: true}, // only 2 parts — need at least 3
|
|
}
|
|
for _, tc := range cases {
|
|
host, proj, comp, ver, err := parseComponentRef(tc.ref)
|
|
if tc.wantErr {
|
|
if err == nil { t.Errorf("parseComponentRef(%q): expected error", tc.ref) }
|
|
continue
|
|
}
|
|
if err != nil { t.Errorf("parseComponentRef(%q): %v", tc.ref, err) }
|
|
if host != tc.wantHost { t.Errorf("host: got %q want %q", host, tc.wantHost) }
|
|
if proj != tc.wantProj { t.Errorf("project: got %q want %q", proj, tc.wantProj) }
|
|
if comp != tc.wantComp { t.Errorf("component: got %q want %q", comp, tc.wantComp) }
|
|
if ver != tc.wantVer { t.Errorf("version: got %q want %q", ver, tc.wantVer) }
|
|
}
|
|
}
|
|
|
|
// ── jobNames ──────────────────────────────────────────────────────────────────
|
|
|
|
func TestJobNames_Empty(t *testing.T) {
|
|
p := &model.Pipeline{Jobs: map[string]model.Job{}}
|
|
if got := jobNames(p); got != nil {
|
|
t.Errorf("empty pipeline: expected nil, got %v", got)
|
|
}
|
|
}
|
|
|
|
// ── treeBuilder helpers ───────────────────────────────────────────────────────
|
|
|
|
func TestNextID(t *testing.T) {
|
|
b := &treeBuilder{visited: map[string]bool{}}
|
|
if b.nextID() != "inc1" { t.Error("first ID") }
|
|
if b.nextID() != "inc2" { t.Error("second ID") }
|
|
}
|
|
|
|
func TestParseEntry_String(t *testing.T) {
|
|
b := &treeBuilder{visited: map[string]bool{}, baseDir: "/tmp"}
|
|
nodes := b.parseEntry("some/local.yml")
|
|
if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) }
|
|
if !strings.Contains(nodes[0].label, "local:") { t.Error("expected local: label") }
|
|
}
|
|
|
|
func TestParseEntry_Unknown(t *testing.T) {
|
|
b := &treeBuilder{visited: map[string]bool{}}
|
|
nodes := b.parseEntry(42) // unknown type
|
|
if len(nodes) != 0 { t.Error("expected no nodes for unknown type") }
|
|
}
|
|
|
|
func TestParseMap_Template(t *testing.T) {
|
|
b := &treeBuilder{visited: map[string]bool{}}
|
|
nodes := b.parseMap(map[string]any{"template": "Auto-DevOps"})
|
|
if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) }
|
|
if !strings.Contains(nodes[0].label, "template:") { t.Error("expected template: label") }
|
|
}
|
|
|
|
func TestParseMap_Remote(t *testing.T) {
|
|
b := &treeBuilder{visited: map[string]bool{}, cfg: fetcher.GitLabConfig{}}
|
|
nodes := b.parseMap(map[string]any{"remote": "https://example.com/ci.yml"})
|
|
if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) }
|
|
if !strings.Contains(nodes[0].label, "remote:") { t.Error("expected remote: label") }
|
|
}
|
|
|
|
func TestParseMap_Project_NoFiles(t *testing.T) {
|
|
b := &treeBuilder{visited: map[string]bool{}, cfg: fetcher.GitLabConfig{}}
|
|
nodes := b.parseMap(map[string]any{"project": "g/p"})
|
|
if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) }
|
|
if !strings.Contains(nodes[0].label, "project:") { t.Error("expected project: label") }
|
|
}
|
|
|
|
func TestParseMap_Project_WithRef(t *testing.T) {
|
|
b := &treeBuilder{visited: map[string]bool{}, cfg: fetcher.GitLabConfig{}}
|
|
nodes := b.parseMap(map[string]any{
|
|
"project": "g/p",
|
|
"ref": "main",
|
|
"file": "templates/ci.yml",
|
|
})
|
|
if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) }
|
|
}
|
|
|
|
func TestParseMap_Project_MultipleFiles(t *testing.T) {
|
|
b := &treeBuilder{visited: map[string]bool{}, cfg: fetcher.GitLabConfig{}}
|
|
nodes := b.parseMap(map[string]any{
|
|
"project": "g/p",
|
|
"file": []any{"a.yml", "b.yml"},
|
|
})
|
|
if len(nodes) != 2 { t.Fatalf("expected 2 nodes, got %d", len(nodes)) }
|
|
}
|
|
|
|
func TestParseMap_Component_WithDollar(t *testing.T) {
|
|
b := &treeBuilder{visited: map[string]bool{}}
|
|
// Component ref with $ — skipped (dynamic, can't resolve)
|
|
nodes := b.parseMap(map[string]any{"component": "${CI_SERVER_HOST}/group/project/comp@v1"})
|
|
if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) }
|
|
if nodes[0].jobs != nil { t.Error("jobs should be nil for unresolvable component") }
|
|
}
|
|
|
|
func TestParseMap_Component_BadRef(t *testing.T) {
|
|
b := &treeBuilder{visited: map[string]bool{}, cfg: fetcher.GitLabConfig{}}
|
|
nodes := b.parseMap(map[string]any{"component": "no-at-sign"})
|
|
if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) }
|
|
}
|
|
|
|
func TestParseMap_Unknown(t *testing.T) {
|
|
b := &treeBuilder{visited: map[string]bool{}}
|
|
nodes := b.parseMap(map[string]any{"unknown_key": "value"})
|
|
if len(nodes) != 0 { t.Error("expected no nodes for unknown map keys") }
|
|
}
|
|
|
|
// ── recurseLocal ──────────────────────────────────────────────────────────────
|
|
|
|
func TestRecurseLocal_FileNotFound(t *testing.T) {
|
|
b := &treeBuilder{visited: map[string]bool{}, baseDir: "/nonexistent"}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseLocal(node, "missing.yml")
|
|
if node.jobs != nil { t.Error("jobs should remain nil on missing file") }
|
|
}
|
|
|
|
func TestRecurseLocal_ValidFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ciPath := filepath.Join(dir, "ci.yml")
|
|
if err := os.WriteFile(ciPath, []byte("job1:\n script: echo\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := &treeBuilder{visited: map[string]bool{}, baseDir: dir}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseLocal(node, "ci.yml")
|
|
if len(node.jobs) == 0 { t.Error("expected jobs to be populated") }
|
|
}
|
|
|
|
func TestRecurseLocal_VisitedPrevents_Loop(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ciPath := filepath.Join(dir, "ci.yml")
|
|
if err := os.WriteFile(ciPath, []byte("job1:\n script: echo\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{"local:" + ciPath: true},
|
|
baseDir: dir,
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseLocal(node, "ci.yml")
|
|
if len(node.jobs) != 0 { t.Error("visited node should not be parsed again") }
|
|
}
|
|
|
|
func TestRecurseLocal_WithNestedIncludes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// parent.yml includes child.yml
|
|
childPath := filepath.Join(dir, "child.yml")
|
|
if err := os.WriteFile(childPath, []byte("child-job:\n script: echo\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parentPath := filepath.Join(dir, "parent.yml")
|
|
parentContent := "include:\n - local: child.yml\nparent-job:\n script: echo\n"
|
|
if err := os.WriteFile(parentPath, []byte(parentContent), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := &treeBuilder{visited: map[string]bool{}, baseDir: dir}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseLocal(node, "parent.yml")
|
|
if len(node.children) == 0 { t.Error("expected children from nested include") }
|
|
}
|
|
|
|
func TestRecurseLocal_InvalidYAML(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ciPath := filepath.Join(dir, "bad.yml")
|
|
if err := os.WriteFile(ciPath, []byte(":\tbad\tyaml\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := &treeBuilder{visited: map[string]bool{}, baseDir: dir}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseLocal(node, "bad.yml")
|
|
if node.jobs != nil { t.Error("invalid YAML should not set jobs") }
|
|
}
|
|
|
|
// ── directJobs ────────────────────────────────────────────────────────────────
|
|
|
|
func TestDirectJobs_MissingFile(t *testing.T) {
|
|
if jobs := directJobs("/nonexistent/ci.yml"); jobs != nil {
|
|
t.Error("expected nil for missing file")
|
|
}
|
|
}
|
|
|
|
func TestDirectJobs_ValidFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ciPath := filepath.Join(dir, "ci.yml")
|
|
if err := os.WriteFile(ciPath, []byte("build:\n script: make\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
jobs := directJobs(ciPath)
|
|
if len(jobs) == 0 { t.Error("expected jobs from valid file") }
|
|
}
|
|
|
|
// ── renderTree ────────────────────────────────────────────────────────────────
|
|
|
|
func TestRenderTree_Basic(t *testing.T) {
|
|
root := &treeNode{
|
|
id: "root",
|
|
label: "main.yml",
|
|
class: "main",
|
|
jobs: []string{"job-a", "job-b"},
|
|
children: []*treeNode{
|
|
{id: "inc1", label: "local: child.yml", class: "local"},
|
|
},
|
|
}
|
|
out := renderTree(root)
|
|
if !strings.Contains(out, "main.yml") { t.Error("expected root label") }
|
|
if !strings.Contains(out, "job-a") { t.Error("expected job-a") }
|
|
if !strings.Contains(out, "child.yml") { t.Error("expected child label") }
|
|
if !strings.Contains(out, "root --> inc1") { t.Error("expected edge root→inc1") }
|
|
}
|
|
|
|
// ── Includes (integration) ────────────────────────────────────────────────────
|
|
|
|
func TestIncludes_LocalFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
childPath := filepath.Join(dir, "child.yml")
|
|
if err := os.WriteFile(childPath, []byte("child-job:\n script: echo\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sourcePath := filepath.Join(dir, "pipeline.yml")
|
|
rawIncludes := []any{map[string]any{"local": "child.yml"}}
|
|
|
|
out := Includes(sourcePath, rawIncludes, fetcher.GitLabConfig{})
|
|
if !strings.Contains(out, "local:") { t.Error("expected local: node") }
|
|
}
|
|
|
|
func TestIncludes_NoIncludes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
sourcePath := filepath.Join(dir, "pipeline.yml")
|
|
out := Includes(sourcePath, nil, fetcher.GitLabConfig{})
|
|
if !strings.Contains(out, "flowchart") { t.Error("expected flowchart") }
|
|
}
|
|
|
|
// ── fetchComponentFile (graph package) ────────────────────────────────────────
|
|
|
|
func TestGraphFetchComponentFile_PrimarySuccess(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
fmt.Fprintln(w, "comp-job:\n script: echo")
|
|
}))
|
|
defer srv.Close()
|
|
cfg := fetcher.GitLabConfig{BaseURL: srv.URL}
|
|
data, err := fetchComponentFile(cfg, "g/p", "mycomp", "v1")
|
|
if err != nil {
|
|
t.Fatalf("primary success: unexpected error: %v", err)
|
|
}
|
|
if len(data) == 0 {
|
|
t.Error("expected non-empty data")
|
|
}
|
|
}
|
|
|
|
// ── recurseRemote ─────────────────────────────────────────────────────────────
|
|
|
|
func TestRecurseRemote_AlreadyVisited(t *testing.T) {
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{"remote:http://example.com/ci.yml": true},
|
|
cfg: fetcher.GitLabConfig{},
|
|
baseDir: "/tmp",
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseRemote(node, "http://example.com/ci.yml")
|
|
// Visited: nothing should be fetched or added.
|
|
}
|
|
|
|
func TestRecurseRemote_ValidWithJobs(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
fmt.Fprintln(w, "remote-job:\n script: echo")
|
|
}))
|
|
defer srv.Close()
|
|
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{},
|
|
cfg: fetcher.GitLabConfig{},
|
|
baseDir: "/tmp",
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseRemote(node, srv.URL+"/ci.yml")
|
|
if len(node.jobs) == 0 {
|
|
t.Error("expected job names from remote YAML")
|
|
}
|
|
}
|
|
|
|
func TestRecurseRemote_InvalidYAML(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
fmt.Fprintln(w, ":\tbad\tyaml")
|
|
}))
|
|
defer srv.Close()
|
|
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{},
|
|
cfg: fetcher.GitLabConfig{},
|
|
baseDir: "/tmp",
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseRemote(node, srv.URL+"/bad.yml")
|
|
// ParseBytes fails → early return; no panic.
|
|
}
|
|
|
|
func TestRecurseRemote_WithSubIncludes(t *testing.T) {
|
|
// The remote YAML includes a second URL (sub-includes path).
|
|
var callCount int
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
callCount++
|
|
w.WriteHeader(200)
|
|
if callCount == 1 {
|
|
// First call: returns YAML with a remote sub-include.
|
|
fmt.Fprintln(w, "include:\n - remote: http://127.0.0.1:0/sub.yml\nparent-job:\n script: echo")
|
|
} else {
|
|
// Sub-include fetch will fail (connection refused) — that's OK; we
|
|
// just need the sub-include routing path to be executed.
|
|
w.WriteHeader(500)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{},
|
|
cfg: fetcher.GitLabConfig{},
|
|
baseDir: "/tmp",
|
|
counter: 0,
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseRemote(node, srv.URL+"/ci.yml")
|
|
// parent-job should be in node.jobs even if sub-include fails.
|
|
if len(node.jobs) == 0 {
|
|
t.Error("expected job names from remote YAML")
|
|
}
|
|
}
|
|
|
|
// ── recurseComponent ─────────────────────────────────────────────────────────
|
|
|
|
// tlsHost returns a helper that creates a TLS test server, swaps http.DefaultTransport
|
|
// so the test client trusts the self-signed cert, and returns (close, host).
|
|
func tlsComponent(t *testing.T, handler http.HandlerFunc) (host string) {
|
|
t.Helper()
|
|
srv := httptest.NewTLSServer(handler)
|
|
t.Cleanup(srv.Close)
|
|
orig := http.DefaultTransport
|
|
http.DefaultTransport = srv.Client().Transport
|
|
t.Cleanup(func() { http.DefaultTransport = orig })
|
|
return srv.URL[len("https://"):]
|
|
}
|
|
|
|
func TestRecurseComponent_AlreadyVisited(t *testing.T) {
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{"component:gitlab.com/g/p/comp@v1": true},
|
|
cfg: fetcher.GitLabConfig{},
|
|
baseDir: "/tmp",
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseComponent(node, "gitlab.com/g/p/comp@v1")
|
|
// Already visited: nothing happens.
|
|
}
|
|
|
|
func TestRecurseComponent_DollarRef(t *testing.T) {
|
|
// ref containing $ → early return (line 196-197)
|
|
b := &treeBuilder{visited: map[string]bool{}, cfg: fetcher.GitLabConfig{}}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseComponent(node, "$CI_SERVER/g/p/comp@v1")
|
|
// no panic, node unchanged
|
|
}
|
|
|
|
func TestRecurseComponent_BadRef(t *testing.T) {
|
|
// ref with no @ → parseComponentRef fails (line 206-208)
|
|
b := &treeBuilder{visited: map[string]bool{}, cfg: fetcher.GitLabConfig{}}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseComponent(node, "gitlab.com/g/p/mycomp-no-version")
|
|
// no panic
|
|
}
|
|
|
|
func TestRecurseComponent_FetchError(t *testing.T) {
|
|
host := tlsComponent(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(404)
|
|
})
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{},
|
|
cfg: fetcher.GitLabConfig{},
|
|
baseDir: "/tmp",
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseComponent(node, host+"/g/p/mycomp@v1")
|
|
// 404 → fetch error → early return; no panic.
|
|
}
|
|
|
|
func TestRecurseComponent_InvalidYAML(t *testing.T) {
|
|
host := tlsComponent(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
fmt.Fprintln(w, ":\tbad\tyaml")
|
|
})
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{},
|
|
cfg: fetcher.GitLabConfig{},
|
|
baseDir: "/tmp",
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseComponent(node, host+"/g/p/mycomp@v1")
|
|
// fetch succeeds but ParseBytes fails → early return; no panic.
|
|
}
|
|
|
|
func TestRecurseComponent_ValidYAML(t *testing.T) {
|
|
host := tlsComponent(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
fmt.Fprintln(w, "comp-job:\n script: echo component")
|
|
})
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{},
|
|
cfg: fetcher.GitLabConfig{},
|
|
baseDir: "/tmp",
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseComponent(node, host+"/g/p/mycomp@v1")
|
|
if len(node.jobs) == 0 {
|
|
t.Error("expected job names from component YAML")
|
|
}
|
|
}
|
|
|
|
// ── recurseProject ────────────────────────────────────────────────────────────
|
|
|
|
func TestRecurseProject_AlreadyVisited(t *testing.T) {
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{"project:g/p:ci.yml@main": true},
|
|
cfg: fetcher.GitLabConfig{Token: "tok"},
|
|
baseDir: "/tmp",
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseProject(node, "g/p", "ci.yml", "main")
|
|
// Already visited: nothing happens.
|
|
}
|
|
|
|
func TestRecurseProject_WithToken(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
fmt.Fprintln(w, "proj-job:\n script: echo project")
|
|
}))
|
|
defer srv.Close()
|
|
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{},
|
|
cfg: fetcher.GitLabConfig{BaseURL: srv.URL, Token: "tok"},
|
|
baseDir: "/tmp",
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseProject(node, "g/p", "ci.yml", "main")
|
|
if len(node.jobs) == 0 {
|
|
t.Error("expected job names from project YAML")
|
|
}
|
|
}
|
|
|
|
func TestRecurseProject_FetchError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(404)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{},
|
|
cfg: fetcher.GitLabConfig{BaseURL: srv.URL, Token: "tok"},
|
|
baseDir: "/tmp",
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseProject(node, "g/p", "ci.yml", "main")
|
|
// fetch fails → early return; no panic.
|
|
}
|
|
|
|
func TestRecurseProject_InvalidYAML(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
fmt.Fprintln(w, ":\tbad\tyaml")
|
|
}))
|
|
defer srv.Close()
|
|
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{},
|
|
cfg: fetcher.GitLabConfig{BaseURL: srv.URL, Token: "tok"},
|
|
baseDir: "/tmp",
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseProject(node, "g/p", "ci.yml", "main")
|
|
// ParseBytes fails → early return; no panic.
|
|
}
|
|
|
|
func TestRecurseProject_WithSubIncludes(t *testing.T) {
|
|
// Project YAML includes sub-includes → covers recurseProject line 170 (buildChildren).
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
fmt.Fprintln(w, "include:\n - remote: http://127.0.0.1:0/sub.yml\nproj-job:\n script: echo")
|
|
}))
|
|
defer srv.Close()
|
|
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{},
|
|
cfg: fetcher.GitLabConfig{BaseURL: srv.URL, Token: "tok"},
|
|
baseDir: "/tmp",
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseProject(node, "g/p", "ci.yml", "main")
|
|
// proj-job should be populated; sub-include fetch fails (port 0) — that's fine.
|
|
if len(node.jobs) == 0 {
|
|
t.Error("expected job names from project YAML")
|
|
}
|
|
}
|
|
|
|
func TestRecurseComponent_WithSubIncludes(t *testing.T) {
|
|
// Component YAML includes sub-includes → covers recurseComponent line 221 (buildChildren).
|
|
host := tlsComponent(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
fmt.Fprintln(w, "include:\n - remote: http://127.0.0.1:0/sub.yml\ncomp-job:\n script: echo")
|
|
})
|
|
b := &treeBuilder{
|
|
visited: map[string]bool{},
|
|
cfg: fetcher.GitLabConfig{},
|
|
baseDir: "/tmp",
|
|
}
|
|
node := &treeNode{id: "n1"}
|
|
b.recurseComponent(node, host+"/g/p/mycomp@v1")
|
|
if len(node.jobs) == 0 {
|
|
t.Error("expected job names from component YAML")
|
|
}
|
|
}
|
|
|
|
// ── fetchComponentFile (fallback path) ───────────────────────────────────────
|
|
|
|
func TestGraphFetchComponentFile_FallbackSuccess(t *testing.T) {
|
|
// Primary path (templates/comp.yml) returns 404; fallback (templates/comp/template.yml) returns 200.
|
|
var reqCount int
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
reqCount++
|
|
if reqCount == 1 {
|
|
w.WriteHeader(404)
|
|
} else {
|
|
w.WriteHeader(200)
|
|
fmt.Fprintln(w, "fallback-job:\n script: echo")
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
cfg := fetcher.GitLabConfig{BaseURL: srv.URL}
|
|
data, err := fetchComponentFile(cfg, "g/p", "mycomp", "v1")
|
|
if err != nil {
|
|
t.Fatalf("fallback success: unexpected error: %v", err)
|
|
}
|
|
if len(data) == 0 {
|
|
t.Error("expected non-empty data from fallback path")
|
|
}
|
|
}
|
|
|
|
// Ensure model import is used.
|
|
var _ = model.Job{}
|