Files
at-container-registry/pkg/appview/handlers/diff_test.go

403 lines
12 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)
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,
)
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,
)
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")
}
}