mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 04:06:58 +00:00
- 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
613 lines
18 KiB
Go
613 lines
18 KiB
Go
package handlers
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestComputeLayerDiff_IdenticalLayers(t *testing.T) {
|
|
layers := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:aaa", Size: 100, Command: "ADD file in /"},
|
|
{Index: 2, Digest: "sha256:bbb", Size: 200, Command: "RUN apt-get update"},
|
|
}
|
|
|
|
diff := computeLayerDiff(layers, layers)
|
|
|
|
if len(diff) != 2 {
|
|
t.Fatalf("expected 2 entries, got %d", len(diff))
|
|
}
|
|
for _, e := range diff {
|
|
if e.Status != "shared" {
|
|
t.Errorf("expected shared, got %s", e.Status)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestComputeLayerDiff_SharedPrefixThenDivergence(t *testing.T) {
|
|
from := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:base", Size: 100},
|
|
{Index: 2, Digest: "sha256:old", Size: 200},
|
|
}
|
|
to := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:base", Size: 100},
|
|
{Index: 2, Digest: "sha256:new1", Size: 300},
|
|
{Index: 3, Digest: "sha256:new2", Size: 150},
|
|
}
|
|
|
|
diff := computeLayerDiff(from, to)
|
|
|
|
// Lockstep: shared, then -/+ pair (no command match), then +1 added
|
|
if len(diff) != 4 {
|
|
t.Fatalf("expected 4 entries, got %d", len(diff))
|
|
}
|
|
|
|
expected := []struct {
|
|
status string
|
|
digest string
|
|
}{
|
|
{"shared", "sha256:base"},
|
|
{"removed", "sha256:old"}, // no command, different digest → -/+
|
|
{"added", "sha256:new1"},
|
|
{"added", "sha256:new2"}, // extra layer in to
|
|
}
|
|
|
|
for i, e := range expected {
|
|
if diff[i].Status != e.status {
|
|
t.Errorf("[%d] expected status %s, got %s", i, e.status, diff[i].Status)
|
|
}
|
|
if diff[i].Layer.Digest != e.digest {
|
|
t.Errorf("[%d] expected digest %s, got %s", i, e.digest, diff[i].Layer.Digest)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestComputeLayerDiff_SameCommandDifferentDigest(t *testing.T) {
|
|
from := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:base", Size: 100, Command: "ADD file in /"},
|
|
{Index: 2, Digest: "sha256:old", Size: 200, Command: "RUN apt-get update"},
|
|
{Index: 3, Digest: "sha256:old2", Size: 300, Command: "RUN pip install flask"},
|
|
}
|
|
to := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:base", Size: 100, Command: "ADD file in /"},
|
|
{Index: 2, Digest: "sha256:new", Size: 250, Command: "RUN apt-get update"},
|
|
{Index: 3, Digest: "sha256:new2", Size: 350, Command: "RUN pip install flask"},
|
|
}
|
|
|
|
diff := computeLayerDiff(from, to)
|
|
|
|
if len(diff) != 3 {
|
|
t.Fatalf("expected 3 entries, got %d", len(diff))
|
|
}
|
|
if diff[0].Status != "shared" {
|
|
t.Errorf("[0] expected shared, got %s", diff[0].Status)
|
|
}
|
|
if diff[1].Status != "rebuilt" {
|
|
t.Errorf("[1] expected rebuilt, got %s", diff[1].Status)
|
|
}
|
|
if diff[1].PrevLayer == nil || diff[1].PrevLayer.Size != 200 {
|
|
t.Error("[1] expected PrevLayer with size 200")
|
|
}
|
|
if diff[2].Status != "rebuilt" {
|
|
t.Errorf("[2] expected rebuilt, got %s", diff[2].Status)
|
|
}
|
|
}
|
|
|
|
func TestComputeLayerDiff_DifferentCommandDifferentDigest(t *testing.T) {
|
|
from := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:base", Size: 100, Command: "ADD file in /"},
|
|
{Index: 2, Digest: "sha256:old", Size: 200, Command: "RUN pip install requests==2.28"},
|
|
}
|
|
to := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:base", Size: 100, Command: "ADD file in /"},
|
|
{Index: 2, Digest: "sha256:new", Size: 250, Command: "RUN pip install requests==2.31"},
|
|
}
|
|
|
|
diff := computeLayerDiff(from, to)
|
|
|
|
if len(diff) != 3 {
|
|
t.Fatalf("expected 3 entries, got %d", len(diff))
|
|
}
|
|
if diff[0].Status != "shared" {
|
|
t.Errorf("[0] expected shared, got %s", diff[0].Status)
|
|
}
|
|
// Different command → -/+ pair
|
|
if diff[1].Status != "removed" {
|
|
t.Errorf("[1] expected removed, got %s", diff[1].Status)
|
|
}
|
|
if diff[2].Status != "added" {
|
|
t.Errorf("[2] expected added, got %s", diff[2].Status)
|
|
}
|
|
}
|
|
|
|
func TestComputeLayerDiff_InsertedLayer(t *testing.T) {
|
|
from := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:aaa", Size: 100, Command: "ADD file in /"},
|
|
{Index: 2, Digest: "sha256:bbb", Size: 200, Command: "RUN apt-get update"},
|
|
{Index: 3, Digest: "sha256:ccc", Size: 300, Command: "RUN pip install flask"},
|
|
}
|
|
to := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:aaa", Size: 100, Command: "ADD file in /"},
|
|
{Index: 2, Digest: "sha256:ddd", Size: 210, Command: "RUN apt-get update"},
|
|
{Index: 3, Digest: "sha256:eee", Size: 150, Command: "RUN apt-get install curl"},
|
|
{Index: 4, Digest: "sha256:fff", Size: 310, Command: "RUN pip install flask"},
|
|
}
|
|
|
|
diff := computeLayerDiff(from, to)
|
|
|
|
// Expected: shared, rebuilt, +added, rebuilt
|
|
expected := []string{"shared", "rebuilt", "added", "rebuilt"}
|
|
if len(diff) != len(expected) {
|
|
t.Fatalf("expected %d entries, got %d: %v", len(expected), len(diff), diffStatuses(diff))
|
|
}
|
|
for i, e := range expected {
|
|
if diff[i].Status != e {
|
|
t.Errorf("[%d] expected %s, got %s", i, e, diff[i].Status)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestComputeLayerDiff_RemovedLayer(t *testing.T) {
|
|
from := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:aaa", Size: 100, Command: "ADD file in /"},
|
|
{Index: 2, Digest: "sha256:bbb", Size: 200, Command: "RUN apt-get update"},
|
|
{Index: 3, Digest: "sha256:ccc", Size: 150, Command: "RUN apt-get install curl"},
|
|
{Index: 4, Digest: "sha256:ddd", Size: 300, Command: "RUN pip install flask"},
|
|
}
|
|
to := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:aaa", Size: 100, Command: "ADD file in /"},
|
|
{Index: 2, Digest: "sha256:eee", Size: 210, Command: "RUN apt-get update"},
|
|
{Index: 3, Digest: "sha256:fff", Size: 310, Command: "RUN pip install flask"},
|
|
}
|
|
|
|
diff := computeLayerDiff(from, to)
|
|
|
|
// Expected: shared, rebuilt, -removed, rebuilt
|
|
expected := []string{"shared", "rebuilt", "removed", "rebuilt"}
|
|
if len(diff) != len(expected) {
|
|
t.Fatalf("expected %d entries, got %d: %v", len(expected), len(diff), diffStatuses(diff))
|
|
}
|
|
for i, e := range expected {
|
|
if diff[i].Status != e {
|
|
t.Errorf("[%d] expected %s, got %s", i, e, diff[i].Status)
|
|
}
|
|
}
|
|
}
|
|
|
|
// helper for test error messages
|
|
func diffStatuses(diff []LayerDiffEntry) []string {
|
|
var s []string
|
|
for _, d := range diff {
|
|
s = append(s, d.Status)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestComputeLayerDiff_EmptyLayersMatchByCommand(t *testing.T) {
|
|
from := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:base", Size: 100},
|
|
{Index: 0, EmptyLayer: true, Command: "ENV FOO=bar"},
|
|
{Index: 2, Digest: "sha256:old", Size: 200},
|
|
}
|
|
to := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:base", Size: 100},
|
|
{Index: 0, EmptyLayer: true, Command: "ENV FOO=bar"},
|
|
{Index: 2, Digest: "sha256:new", Size: 300},
|
|
}
|
|
|
|
diff := computeLayerDiff(from, to)
|
|
|
|
if len(diff) != 4 {
|
|
t.Fatalf("expected 4 entries, got %d", len(diff))
|
|
}
|
|
if diff[0].Status != "shared" || diff[1].Status != "shared" {
|
|
t.Error("first two entries should be shared (base layer + empty layer)")
|
|
}
|
|
// Different digests, no command → -/+ pair
|
|
if diff[2].Status != "removed" {
|
|
t.Errorf("[2] expected removed, got %s", diff[2].Status)
|
|
}
|
|
if diff[3].Status != "added" {
|
|
t.Errorf("[3] expected added, got %s", diff[3].Status)
|
|
}
|
|
}
|
|
|
|
func TestComputeLayerDiff_CompletelyDifferent(t *testing.T) {
|
|
from := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:old1", Size: 100},
|
|
}
|
|
to := []LayerDetail{
|
|
{Index: 1, Digest: "sha256:new1", Size: 200},
|
|
{Index: 2, Digest: "sha256:new2", Size: 300},
|
|
}
|
|
|
|
diff := computeLayerDiff(from, to)
|
|
|
|
// Lockstep: -/+ pair for position 1, then +1 added
|
|
if len(diff) != 3 {
|
|
t.Fatalf("expected 3 entries, got %d", len(diff))
|
|
}
|
|
if diff[0].Status != "removed" {
|
|
t.Errorf("[0] expected removed, got %s", diff[0].Status)
|
|
}
|
|
if diff[1].Status != "added" {
|
|
t.Errorf("[1] expected added, got %s", diff[1].Status)
|
|
}
|
|
if diff[2].Status != "added" {
|
|
t.Errorf("[2] expected added, got %s", diff[2].Status)
|
|
}
|
|
}
|
|
|
|
func TestComputeLayerDiff_EmptyInputs(t *testing.T) {
|
|
diff := computeLayerDiff(nil, nil)
|
|
if len(diff) != 0 {
|
|
t.Fatalf("expected 0 entries, got %d", len(diff))
|
|
}
|
|
|
|
diff = computeLayerDiff(nil, []LayerDetail{{Index: 1, Digest: "sha256:a"}})
|
|
if len(diff) != 1 || diff[0].Status != "added" {
|
|
t.Error("expected 1 added entry")
|
|
}
|
|
|
|
diff = computeLayerDiff([]LayerDetail{{Index: 1, Digest: "sha256:a"}}, nil)
|
|
if len(diff) != 1 || diff[0].Status != "removed" {
|
|
t.Error("expected 1 removed entry")
|
|
}
|
|
}
|
|
|
|
func TestComputeVulnDiff_FixedAndNew(t *testing.T) {
|
|
from := []vulnMatch{
|
|
{CVEID: "CVE-2024-001", Severity: "Critical", Package: "openssl", Version: "1.1.0"},
|
|
{CVEID: "CVE-2024-002", Severity: "High", Package: "curl", Version: "7.85"},
|
|
{CVEID: "CVE-2024-003", Severity: "Medium", Package: "zlib", Version: "1.2.11"},
|
|
}
|
|
to := []vulnMatch{
|
|
{CVEID: "CVE-2024-002", Severity: "High", Package: "curl", Version: "7.85"},
|
|
{CVEID: "CVE-2025-001", Severity: "High", Package: "requests", Version: "2.31"},
|
|
}
|
|
|
|
diff := computeVulnDiff(from, to)
|
|
|
|
counts := map[string]int{}
|
|
for _, e := range diff {
|
|
counts[e.Status]++
|
|
}
|
|
|
|
if counts["fixed"] != 2 {
|
|
t.Errorf("expected 2 fixed, got %d", counts["fixed"])
|
|
}
|
|
if counts["new"] != 1 {
|
|
t.Errorf("expected 1 new, got %d", counts["new"])
|
|
}
|
|
if counts["unchanged"] != 1 {
|
|
t.Errorf("expected 1 unchanged, got %d", counts["unchanged"])
|
|
}
|
|
}
|
|
|
|
func TestComputeVulnDiff_AllFixed(t *testing.T) {
|
|
from := []vulnMatch{
|
|
{CVEID: "CVE-2024-001", Severity: "Critical"},
|
|
{CVEID: "CVE-2024-002", Severity: "High"},
|
|
}
|
|
|
|
diff := computeVulnDiff(from, nil)
|
|
|
|
for _, e := range diff {
|
|
if e.Status != "fixed" {
|
|
t.Errorf("expected fixed, got %s", e.Status)
|
|
}
|
|
}
|
|
if len(diff) != 2 {
|
|
t.Errorf("expected 2, got %d", len(diff))
|
|
}
|
|
}
|
|
|
|
func TestComputeVulnDiff_AllNew(t *testing.T) {
|
|
to := []vulnMatch{
|
|
{CVEID: "CVE-2025-001", Severity: "Critical"},
|
|
}
|
|
|
|
diff := computeVulnDiff(nil, to)
|
|
|
|
if len(diff) != 1 || diff[0].Status != "new" {
|
|
t.Error("expected 1 new entry")
|
|
}
|
|
}
|
|
|
|
func TestComputeVulnDiff_Empty(t *testing.T) {
|
|
diff := computeVulnDiff(nil, nil)
|
|
if len(diff) != 0 {
|
|
t.Errorf("expected 0, got %d", len(diff))
|
|
}
|
|
}
|
|
|
|
func TestComputeDiffSummary(t *testing.T) {
|
|
fromLayers := []LayerDetail{
|
|
{Index: 1, Size: 1000},
|
|
{Index: 2, Size: 2000},
|
|
}
|
|
toLayers := []LayerDetail{
|
|
{Index: 1, Size: 1000},
|
|
{Index: 2, Size: 2500},
|
|
{Index: 3, Size: 500},
|
|
}
|
|
|
|
vulnDiff := []VulnDiffEntry{
|
|
{Status: "fixed", Vuln: vulnMatch{Severity: "Critical"}},
|
|
{Status: "fixed", Vuln: vulnMatch{Severity: "High"}},
|
|
{Status: "fixed", Vuln: vulnMatch{Severity: "High"}},
|
|
{Status: "new", Vuln: vulnMatch{Severity: "Medium"}},
|
|
{Status: "unchanged", Vuln: vulnMatch{Severity: "Low"}},
|
|
}
|
|
|
|
summary := computeDiffSummary(fromLayers, toLayers, vulnDiff, true, nil, false)
|
|
|
|
if summary.SizeDelta != 1000 {
|
|
t.Errorf("expected size delta 1000, got %d", summary.SizeDelta)
|
|
}
|
|
if summary.LayerCountFrom != 2 {
|
|
t.Errorf("expected from count 2, got %d", summary.LayerCountFrom)
|
|
}
|
|
if summary.LayerCountTo != 3 {
|
|
t.Errorf("expected to count 3, got %d", summary.LayerCountTo)
|
|
}
|
|
if summary.VulnFixedCount != 3 {
|
|
t.Errorf("expected 3 fixed, got %d", summary.VulnFixedCount)
|
|
}
|
|
if summary.VulnNewCount != 1 {
|
|
t.Errorf("expected 1 new, got %d", summary.VulnNewCount)
|
|
}
|
|
if summary.VulnFixedBySev.Critical != 1 {
|
|
t.Errorf("expected 1 fixed critical, got %d", summary.VulnFixedBySev.Critical)
|
|
}
|
|
if summary.VulnFixedBySev.High != 2 {
|
|
t.Errorf("expected 2 fixed high, got %d", summary.VulnFixedBySev.High)
|
|
}
|
|
if summary.VulnNewBySev.Medium != 1 {
|
|
t.Errorf("expected 1 new medium, got %d", summary.VulnNewBySev.Medium)
|
|
}
|
|
if !summary.HasVulnData {
|
|
t.Error("expected HasVulnData to be true")
|
|
}
|
|
}
|
|
|
|
func TestComputeDiffSummary_NoVulnData(t *testing.T) {
|
|
summary := computeDiffSummary(
|
|
[]LayerDetail{{Size: 100}},
|
|
[]LayerDetail{{Size: 200}},
|
|
nil,
|
|
false,
|
|
nil,
|
|
false,
|
|
)
|
|
|
|
if summary.HasVulnData {
|
|
t.Error("expected HasVulnData to be false")
|
|
}
|
|
if summary.SizeDelta != 100 {
|
|
t.Errorf("expected size delta 100, got %d", summary.SizeDelta)
|
|
}
|
|
}
|
|
|
|
func TestComputeDiffSummary_SmallerImage(t *testing.T) {
|
|
summary := computeDiffSummary(
|
|
[]LayerDetail{{Size: 5000}, {Size: 3000}},
|
|
[]LayerDetail{{Size: 2000}},
|
|
nil,
|
|
false,
|
|
nil,
|
|
false,
|
|
)
|
|
|
|
if summary.SizeDelta != -6000 {
|
|
t.Errorf("expected size delta -6000, got %d", summary.SizeDelta)
|
|
}
|
|
if summary.LayerCountFrom != 2 || summary.LayerCountTo != 1 {
|
|
t.Error("unexpected layer counts")
|
|
}
|
|
}
|
|
|
|
// sbomStatusCounts tallies a diff result by status for compact assertions.
|
|
func sbomStatusCounts(diff []SbomDiffEntry) map[string]int {
|
|
counts := make(map[string]int)
|
|
for _, e := range diff {
|
|
counts[e.Status]++
|
|
}
|
|
return counts
|
|
}
|
|
|
|
func TestComputeSbomDiff_Identical(t *testing.T) {
|
|
pkgs := []sbomPackage{
|
|
{Name: "openssl", Version: "3.0.1", Type: "deb"},
|
|
{Name: "zlib", Version: "1.2.13", Type: "deb"},
|
|
}
|
|
diff := computeSbomDiff(pkgs, pkgs)
|
|
if len(diff) != 2 {
|
|
t.Fatalf("expected 2 entries, got %d", len(diff))
|
|
}
|
|
for _, e := range diff {
|
|
if e.Status != "unchanged" {
|
|
t.Errorf("expected unchanged, got %q for %s", e.Status, e.Package.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestComputeSbomDiff_VersionBump(t *testing.T) {
|
|
from := []sbomPackage{{Name: "openssl", Version: "3.0.1", Type: "deb"}}
|
|
to := []sbomPackage{{Name: "openssl", Version: "3.0.2", Type: "deb"}}
|
|
|
|
diff := computeSbomDiff(from, to)
|
|
if len(diff) != 1 {
|
|
t.Fatalf("expected 1 entry, got %d", len(diff))
|
|
}
|
|
if diff[0].Status != "changed" {
|
|
t.Errorf("expected changed, got %q", diff[0].Status)
|
|
}
|
|
if diff[0].PrevVersion != "3.0.1" {
|
|
t.Errorf("expected PrevVersion 3.0.1, got %q", diff[0].PrevVersion)
|
|
}
|
|
if diff[0].Package.Version != "3.0.2" {
|
|
t.Errorf("expected Package.Version 3.0.2, got %q", diff[0].Package.Version)
|
|
}
|
|
}
|
|
|
|
func TestComputeSbomDiff_AddedRemovedEmpty(t *testing.T) {
|
|
diff := computeSbomDiff(nil, nil)
|
|
if len(diff) != 0 {
|
|
t.Errorf("expected 0 entries for empty inputs, got %d", len(diff))
|
|
}
|
|
|
|
diff = computeSbomDiff(nil, []sbomPackage{{Name: "curl", Version: "8.0", Type: "deb"}})
|
|
if len(diff) != 1 || diff[0].Status != "added" {
|
|
t.Errorf("expected single added entry, got %+v", diff)
|
|
}
|
|
|
|
diff = computeSbomDiff([]sbomPackage{{Name: "curl", Version: "8.0", Type: "deb"}}, nil)
|
|
if len(diff) != 1 || diff[0].Status != "removed" {
|
|
t.Errorf("expected single removed entry, got %+v", diff)
|
|
}
|
|
}
|
|
|
|
func TestComputeSbomDiff_SameNameDifferentType(t *testing.T) {
|
|
// A deb package and a binary share a name; the deb version bump must
|
|
// not pair against the binary entry.
|
|
from := []sbomPackage{
|
|
{Name: "curl", Version: "8.0", Type: "deb"},
|
|
{Name: "curl", Version: "8.0", Type: ""},
|
|
}
|
|
to := []sbomPackage{
|
|
{Name: "curl", Version: "8.1", Type: "deb"},
|
|
{Name: "curl", Version: "8.0", Type: ""},
|
|
}
|
|
|
|
diff := computeSbomDiff(from, to)
|
|
if len(diff) != 2 {
|
|
t.Fatalf("expected 2 entries, got %d", len(diff))
|
|
}
|
|
counts := sbomStatusCounts(diff)
|
|
if counts["changed"] != 1 || counts["unchanged"] != 1 {
|
|
t.Errorf("expected 1 changed + 1 unchanged, got %v", counts)
|
|
}
|
|
for _, e := range diff {
|
|
if e.Status == "changed" {
|
|
if e.Package.Type != "deb" || e.PrevVersion != "8.0" || e.Package.Version != "8.1" {
|
|
t.Errorf("changed entry paired wrong: %+v", e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestComputeSbomDiff_Multiplicity(t *testing.T) {
|
|
// from {1.0, 1.1} vs to {1.1, 1.2}: 1.1 matches exactly, 1.0 pairs
|
|
// with 1.2 as a version change.
|
|
from := []sbomPackage{
|
|
{Name: "libssl", Version: "1.0", Type: "deb"},
|
|
{Name: "libssl", Version: "1.1", Type: "deb"},
|
|
}
|
|
to := []sbomPackage{
|
|
{Name: "libssl", Version: "1.1", Type: "deb"},
|
|
{Name: "libssl", Version: "1.2", Type: "deb"},
|
|
}
|
|
|
|
diff := computeSbomDiff(from, to)
|
|
if len(diff) != 2 {
|
|
t.Fatalf("expected 2 entries, got %d", len(diff))
|
|
}
|
|
counts := sbomStatusCounts(diff)
|
|
if counts["unchanged"] != 1 || counts["changed"] != 1 {
|
|
t.Errorf("expected 1 unchanged + 1 changed, got %v", counts)
|
|
}
|
|
for _, e := range diff {
|
|
if e.Status == "changed" && (e.PrevVersion != "1.0" || e.Package.Version != "1.2") {
|
|
t.Errorf("expected 1.0 to pair with 1.2, got %+v", e)
|
|
}
|
|
}
|
|
|
|
// Asymmetric counts: two from-side versions, one to-side version.
|
|
from = []sbomPackage{
|
|
{Name: "libssl", Version: "1.0", Type: "deb"},
|
|
{Name: "libssl", Version: "1.1", Type: "deb"},
|
|
}
|
|
to = []sbomPackage{{Name: "libssl", Version: "1.2", Type: "deb"}}
|
|
|
|
diff = computeSbomDiff(from, to)
|
|
counts = sbomStatusCounts(diff)
|
|
if counts["changed"] != 1 || counts["removed"] != 1 {
|
|
t.Errorf("expected 1 changed + 1 removed, got %v", counts)
|
|
}
|
|
}
|
|
|
|
func TestComputeSbomDiff_DuplicateEntriesOneSide(t *testing.T) {
|
|
// Two identical entries on the from side must not both match the
|
|
// single to-side entry (multiset, not set).
|
|
from := []sbomPackage{
|
|
{Name: "foo", Version: "1.0", Type: "npm"},
|
|
{Name: "foo", Version: "1.0", Type: "npm"},
|
|
}
|
|
to := []sbomPackage{{Name: "foo", Version: "1.0", Type: "npm"}}
|
|
|
|
diff := computeSbomDiff(from, to)
|
|
if len(diff) != 2 {
|
|
t.Fatalf("expected 2 entries, got %d", len(diff))
|
|
}
|
|
counts := sbomStatusCounts(diff)
|
|
if counts["unchanged"] != 1 || counts["removed"] != 1 {
|
|
t.Errorf("expected 1 unchanged + 1 removed, got %v", counts)
|
|
}
|
|
}
|
|
|
|
func TestComputeSbomDiff_DeterministicOrder(t *testing.T) {
|
|
from := []sbomPackage{
|
|
{Name: "zlib", Version: "1.2", Type: "deb"},
|
|
{Name: "curl", Version: "8.0", Type: "deb"},
|
|
{Name: "gone", Version: "0.1", Type: "npm"},
|
|
}
|
|
to := []sbomPackage{
|
|
{Name: "curl", Version: "8.1", Type: "deb"},
|
|
{Name: "zlib", Version: "1.2", Type: "deb"},
|
|
{Name: "fresh", Version: "2.0", Type: "npm"},
|
|
}
|
|
|
|
want := []SbomDiffEntry{
|
|
{Status: "changed", Package: sbomPackage{Name: "curl", Version: "8.1", Type: "deb"}, PrevVersion: "8.0"},
|
|
{Status: "added", Package: sbomPackage{Name: "fresh", Version: "2.0", Type: "npm"}},
|
|
{Status: "removed", Package: sbomPackage{Name: "gone", Version: "0.1", Type: "npm"}},
|
|
{Status: "unchanged", Package: sbomPackage{Name: "zlib", Version: "1.2", Type: "deb"}},
|
|
}
|
|
|
|
for run := range 5 {
|
|
diff := computeSbomDiff(from, to)
|
|
if len(diff) != len(want) {
|
|
t.Fatalf("run %d: expected %d entries, got %d", run, len(want), len(diff))
|
|
}
|
|
for i := range want {
|
|
if diff[i] != want[i] {
|
|
t.Errorf("run %d entry %d: expected %+v, got %+v", run, i, want[i], diff[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestComputeDiffSummary_PackageCounts(t *testing.T) {
|
|
sbomDiff := []SbomDiffEntry{
|
|
{Status: "added"},
|
|
{Status: "added"},
|
|
{Status: "removed"},
|
|
{Status: "changed"},
|
|
{Status: "unchanged"},
|
|
}
|
|
|
|
summary := computeDiffSummary(nil, nil, nil, false, sbomDiff, true)
|
|
|
|
if summary.PkgAddedCount != 2 {
|
|
t.Errorf("expected 2 added, got %d", summary.PkgAddedCount)
|
|
}
|
|
if summary.PkgRemovedCount != 1 {
|
|
t.Errorf("expected 1 removed, got %d", summary.PkgRemovedCount)
|
|
}
|
|
if summary.PkgChangedCount != 1 {
|
|
t.Errorf("expected 1 changed, got %d", summary.PkgChangedCount)
|
|
}
|
|
if !summary.HasSbomData {
|
|
t.Error("expected HasSbomData to be true")
|
|
}
|
|
}
|