Files
Evan Jarrett 6758996300 add SBOM package diffing, verify hold-service captain records
- diff view gains a Packages tab with added/removed/changed/unchanged
  package tables and purl-derived type/license/upstream links
- captain records verified against the DID's atcr_hold service before
  caching (processor + batch backfill), preventing forged holds
- fix empty-handle updates clobbering cached handles and colliding on
  the UNIQUE constraint
- move fillPrevCIDs into repo.go; DirectRepoOperator is now canonical,
  repomgr kept as a test oracle
- surface read-only crew status in hold selector
- reconcile docs
2026-06-13 12:49:03 -05:00

179 lines
4.6 KiB
Go

package handlers
import "testing"
func TestParsePurl(t *testing.T) {
tests := []struct {
name string
in string
want *purlInfo
}{
{
name: "deb with qualifiers",
in: "pkg:deb/ubuntu/libssl3t64@3.0.13-0ubuntu3.9?arch=amd64&distro=ubuntu-24.04&upstream=openssl",
want: &purlInfo{Type: "deb", Namespace: "ubuntu", Name: "libssl3t64", Version: "3.0.13-0ubuntu3.9"},
},
{
name: "scoped npm",
in: "pkg:npm/%40discordjs/builders@1.14.1",
want: &purlInfo{Type: "npm", Namespace: "@discordjs", Name: "builders", Version: "1.14.1"},
},
{
name: "unscoped npm",
in: "pkg:npm/lodash@4.18.1",
want: &purlInfo{Type: "npm", Namespace: "", Name: "lodash", Version: "4.18.1"},
},
{
name: "golang multi-segment namespace",
in: "pkg:golang/github.com/moby/sys/user@v0.1.0",
want: &purlInfo{Type: "golang", Namespace: "github.com/moby/sys", Name: "user", Version: "v0.1.0"},
},
{
name: "no version",
in: "pkg:gem/rails",
want: &purlInfo{Type: "gem", Namespace: "", Name: "rails", Version: ""},
},
{
name: "subpath stripped",
in: "pkg:golang/golang.org/x/sys@v0.1.0#unix",
want: &purlInfo{Type: "golang", Namespace: "golang.org/x", Name: "sys", Version: "v0.1.0"},
},
{name: "not a purl", in: "https://example.com/foo", want: nil},
{name: "missing name", in: "pkg:npm", want: nil},
{name: "empty", in: "", want: nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parsePurl(tt.in)
if tt.want == nil {
if got != nil {
t.Fatalf("expected nil, got %+v", got)
}
return
}
if got == nil {
t.Fatalf("expected %+v, got nil", tt.want)
}
if got.Type != tt.want.Type || got.Namespace != tt.want.Namespace ||
got.Name != tt.want.Name || got.Version != tt.want.Version {
t.Errorf("expected %+v, got %+v", tt.want, got)
}
})
}
}
func TestParsePurl_Qualifiers(t *testing.T) {
p := parsePurl("pkg:deb/debian/libpam0g@1.7.0-5?arch=amd64&upstream=pam%401.7.0")
if p == nil {
t.Fatal("expected parse to succeed")
}
if p.Qualifiers["upstream"] != "pam@1.7.0" {
t.Errorf("expected upstream qualifier pam@1.7.0, got %q", p.Qualifiers["upstream"])
}
if p.Qualifiers["arch"] != "amd64" {
t.Errorf("expected arch qualifier amd64, got %q", p.Qualifiers["arch"])
}
}
func TestPurlDisplayType(t *testing.T) {
tests := map[string]string{
"golang": "go",
"pypi": "python",
"cargo": "rust",
"maven": "java",
"composer": "php",
"generic": "",
"deb": "deb",
"npm": "npm",
"nuget": "nuget",
"apk": "apk",
}
for in, want := range tests {
if got := purlDisplayType(in); got != want {
t.Errorf("purlDisplayType(%q): expected %q, got %q", in, want, got)
}
}
}
func TestPurlURL(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{
name: "scoped npm",
in: "pkg:npm/%40discordjs/builders@1.14.1",
want: "https://npmx.dev/package/@discordjs/builders/v/1.14.1",
},
{
name: "ubuntu deb uses upstream source",
in: "pkg:deb/ubuntu/libssl3t64@3.0.13?upstream=openssl",
want: "https://launchpad.net/ubuntu/+source/openssl",
},
{
name: "debian deb without upstream falls back to binary name",
in: "pkg:deb/debian/bash@5.2.37-2",
want: "https://tracker.debian.org/pkg/bash",
},
{
name: "debian upstream with version suffix",
in: "pkg:deb/debian/libpam0g@1.7.0-5?upstream=pam%401.7.0",
want: "https://tracker.debian.org/pkg/pam",
},
{
name: "golang",
in: "pkg:golang/github.com/moby/sys/user@v0.1.0",
want: "https://pkg.go.dev/github.com/moby/sys/user@v0.1.0",
},
{
name: "nuget",
in: "pkg:nuget/Serilog@4.3.0",
want: "https://www.nuget.org/packages/Serilog/4.3.0",
},
{
name: "pypi",
in: "pkg:pypi/requests@2.31.0",
want: "https://pypi.org/project/requests/2.31.0/",
},
{
name: "apk",
in: "pkg:apk/alpine/musl@1.2.4-r2",
want: "https://pkgs.alpinelinux.org/packages?name=musl",
},
{
name: "deb with unknown distro namespace",
in: "pkg:deb/somedistro/foo@1.0",
want: "",
},
{
name: "unknown type",
in: "pkg:conan/openssl@3.0.0",
want: "",
},
{
name: "UNKNOWN version gets no link",
in: "pkg:npm/web-streams-ponyfill-es6@UNKNOWN",
want: "",
},
{
name: "lowercase unknown version also gets no link",
in: "pkg:pypi/somepkg@unknown",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := parsePurl(tt.in)
if p == nil {
t.Fatal("expected parse to succeed")
}
if got := purlURL(p); got != tt.want {
t.Errorf("expected %q, got %q", tt.want, got)
}
})
}
}