package catalog import ( "bytes" "encoding/json" "os" "path/filepath" "strings" "testing" ) func moduleFixture(t *testing.T, name string) *Module { t.Helper() root := t.TempDir() if err := os.MkdirAll(filepath.Join(root, "vendorpart", "pkg"), 0700); err != nil { t.Fatal(err) } for path, text := range map[string]string{"LICENSE": "Root copyright\nPermission retained.\n", "LICENSE-3RD-PARTY.md": "Additional copyright\n", "vendorpart/LICENSE": "Nested copyright\n", "vendorpart/pkg/not-license.go": "package pkg"} { if err := os.WriteFile(filepath.Join(root, path), []byte(text), 0600); err != nil { t.Fatal(err) } } return &Module{Path: name, Version: "v1.2.3", Dir: root} } func encodePackages(t *testing.T, packages ...Package) *bytes.Buffer { t.Helper() var out bytes.Buffer for _, p := range packages { if err := json.NewEncoder(&out).Encode(p); err != nil { t.Fatal(err) } } return &out } func TestCollectIncludesNestedAndBundledNoticesOnce(t *testing.T) { m := moduleFixture(t, "example.org/component") packages := []Package{{Dir: filepath.Join(m.Dir, "vendorpart", "pkg"), Module: m}, {Dir: m.Dir, Module: m}, {Dir: filepath.Join(m.Dir, "vendorpart"), Module: m}} got, err := Collect(encodePackages(t, packages...), nil) if err != nil { t.Fatal(err) } if len(got.Components) != 1 || len(got.MissingNotices) != 0 { t.Fatal("wrong dependency inventory") } c := got.Components[0] if len(c.Notices) != 3 { t.Fatalf("notices=%d, want root, bundled and nested", len(c.Notices)) } if c.Notices[0].Name != "LICENSE" || c.Notices[0].Text != "Root copyright\nPermission retained.\n" || c.Notices[2].Name != "vendorpart/LICENSE" { t.Fatal("notice path, ordering or original bytes changed") } again, err := Collect(encodePackages(t, packages[2], packages[0], packages[1]), nil) if err != nil { t.Fatal(err) } a, _ := json.Marshal(got) b, _ := json.Marshal(again) if !bytes.Equal(a, b) { t.Fatal("inventory depends on package order") } } func TestCollectReportsMissingAndExcludesOnlyExplicitModules(t *testing.T) { m := &Module{Path: "example.org/missing", Version: "v2.0.0", Dir: t.TempDir()} owned := moduleFixture(t, "example.org/owned") main := &Module{Main: true, Dir: t.TempDir()} got, err := Collect(encodePackages(t, Package{Dir: m.Dir, Module: m}, Package{Dir: owned.Dir, Module: owned}, Package{Dir: main.Dir, Module: main}), map[string]bool{owned.Path: true}) if err != nil { t.Fatal(err) } if len(got.Components) != 1 || len(got.MissingNotices) != 1 || got.MissingNotices[0] != m.Path { t.Fatal("missing notice silently dropped") } } func TestCollectRefusesUnverifiableProvenance(t *testing.T) { for _, mode := range []string{"outside", "replacement", "missing-version", "symlink", "package-error"} { t.Run(mode, func(t *testing.T) { m := moduleFixture(t, "example.org/component") p := Package{Dir: m.Dir, Module: m} switch mode { case "outside": p.Dir = t.TempDir() case "replacement": m.Replace = &Module{Dir: t.TempDir()} case "missing-version": m.Version = "" case "symlink": if err := os.Symlink(filepath.Join(m.Dir, "LICENSE"), filepath.Join(m.Dir, "NOTICE")); err != nil { t.Fatal(err) } case "package-error": p.Error = &struct{ Err string }{Err: "failed"} } if _, err := Collect(encodePackages(t, p), nil); err == nil { t.Fatal("unverifiable package accepted") } }) } if _, err := Collect(strings.NewReader(`{"Module":`), nil); err == nil { t.Fatal("truncated package stream accepted") } } func TestCollectSkipsSourceAndDirectoriesButRetainsNotices(t *testing.T) { m := moduleFixture(t, "example.org/component") for _, name := range []string{"license.go", "NOTICE_test.go", "NOTICE.go"} { if err := os.WriteFile(filepath.Join(m.Dir, name), []byte("package example"), 0600); err != nil { t.Fatal(err) } } if err := os.Mkdir(filepath.Join(m.Dir, "license-tool"), 0700); err != nil { t.Fatal(err) } got, err := Collect(encodePackages(t, Package{Dir: m.Dir, Module: m}), nil) if err != nil { t.Fatal(err) } if len(got.Components) != 1 || len(got.Components[0].Notices) != 2 { t.Fatal("source or directory was collected as notice", got) } if err := os.WriteFile(filepath.Join(m.Dir, "NOTICE"), []byte(" \n\t"), 0600); err != nil { t.Fatal(err) } if _, err := Collect(encodePackages(t, Package{Dir: m.Dir, Module: m}), nil); err == nil { t.Fatal("blank notice accepted") } }