mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-04-20 16:40:29 +00:00
403 lines
12 KiB
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")
|
|
}
|
|
}
|