203 lines
7.4 KiB
Go
203 lines
7.4 KiB
Go
package checkpoint
|
|
|
|
import (
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
|
)
|
|
|
|
func TestNewIdentityNormalizesOrderAndEmptyValues(t *testing.T) {
|
|
base := representativeIdentityInput()
|
|
identity, err := NewIdentity(base)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, tt := range []struct {
|
|
name string
|
|
mutate func(*IdentityInput)
|
|
}{
|
|
{"selected lanes", func(v *IdentityInput) { v.SelectedLanes = []string{"lane-a", "lane-b"} }},
|
|
{"runtime fingerprints", func(v *IdentityInput) {
|
|
v.RuntimeOverrides = []Fingerprint{{Name: "model", Value: "large"}, {Name: "timeout", Value: "30s"}}
|
|
}},
|
|
{"references", func(v *IdentityInput) {
|
|
v.References = []artifacts.ReferenceProvenance{v.References[1], v.References[0]}
|
|
}},
|
|
{"provenance fingerprints", func(v *IdentityInput) {
|
|
v.ProvenanceFingerprints = []Fingerprint{{Name: "source", Value: "v2"}, {Name: "runner", Value: "v1"}}
|
|
}},
|
|
{"resolved lanes", func(v *IdentityInput) {
|
|
v.Pipeline.ArtifactLanes = []pipeline.ResolvedArtifactLane{v.Pipeline.ArtifactLanes[1], v.Pipeline.ArtifactLanes[0]}
|
|
}},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
changed := cloneIdentityInput(base)
|
|
tt.mutate(&changed)
|
|
got, err := NewIdentity(changed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(identity, got) {
|
|
t.Fatalf("reordered identity differs:\nbase=%#v\ngot=%#v", identity, got)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("duplicates and blanks are ignored", func(t *testing.T) {
|
|
changed := base
|
|
changed.SelectedLanes = []string{" ", "lane-b", "lane-a", "lane-a", ""}
|
|
changed.RuntimeOverrides = append(changed.RuntimeOverrides, Fingerprint{}, Fingerprint{Name: " ", Value: "ignored"}, Fingerprint{Name: "timeout", Value: "30s"})
|
|
changed.ProvenanceFingerprints = append(changed.ProvenanceFingerprints, Fingerprint{}, Fingerprint{Name: "", Value: "ignored"})
|
|
|
|
got, err := NewIdentity(changed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(identity, got) {
|
|
t.Fatalf("empty or duplicate values changed identity:\nbase=%#v\ngot=%#v", identity, got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNewIdentityChangesForMeaningfulInputs(t *testing.T) {
|
|
base := representativeIdentityInput()
|
|
original, err := NewIdentity(base)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cases := map[string]func(*IdentityInput){
|
|
"pipeline id": func(v *IdentityInput) { v.Pipeline.ID = "another-pipeline" },
|
|
"pipeline digest": func(v *IdentityInput) { v.Pipeline.Digest = "sha256:pipeline-digest-2" },
|
|
"input key": func(v *IdentityInput) { v.InputKey = "another-input" },
|
|
"raw input digest": func(v *IdentityInput) { v.RawInputDigest = "sha256:raw-input-2" },
|
|
"source digest": func(v *IdentityInput) { v.SourceDigest = "sha256:source-2" },
|
|
"selected lanes": func(v *IdentityInput) { v.SelectedLanes = []string{"lane-a"} },
|
|
"runtime override": func(v *IdentityInput) { v.RuntimeOverrides[0].Value = "60s" },
|
|
"reference digest": func(v *IdentityInput) { v.References[0].Digest = "sha256:reference-2" },
|
|
"reference identity": func(v *IdentityInput) { v.References[0].OriginURI = "file:///other-reference" },
|
|
"provenance": func(v *IdentityInput) { v.ProvenanceFingerprints[0].Value = "v3" },
|
|
}
|
|
|
|
for name, mutate := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
changed := cloneIdentityInput(base)
|
|
mutate(&changed)
|
|
got, err := NewIdentity(changed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Digest == original.Digest {
|
|
t.Fatalf("meaningful %s input did not change digest %q", name, got.Digest)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewIdentityUsesResolvedInputWhenKeyIsOmitted(t *testing.T) {
|
|
input := representativeIdentityInput()
|
|
input.InputKey = ""
|
|
|
|
identity, err := NewIdentity(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if identity.InputKey != input.Pipeline.Input.Module {
|
|
t.Fatalf("input key = %q, want resolved module %q", identity.InputKey, input.Pipeline.Input.Module)
|
|
}
|
|
}
|
|
|
|
func TestNewIdentityRejectsMissingRequiredInputs(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
mutate func(*IdentityInput)
|
|
want string
|
|
}{
|
|
{"pipeline id", func(v *IdentityInput) { v.Pipeline.ID = "" }, "pipeline id"},
|
|
{"pipeline digest", func(v *IdentityInput) { v.Pipeline.Digest = "" }, "pipeline digest"},
|
|
{"input key", func(v *IdentityInput) { v.InputKey = ""; v.Pipeline.Input = pipeline.Binding("") }, "input key"},
|
|
{"input digests", func(v *IdentityInput) { v.RawInputDigest = ""; v.SourceDigest = "" }, "raw input digest or source digest"},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
input := representativeIdentityInput()
|
|
tt.mutate(&input)
|
|
_, err := NewIdentity(input)
|
|
if err == nil || !strings.Contains(err.Error(), tt.want) {
|
|
t.Fatalf("error = %v, want category containing %q", err, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIdentityRelativePathIsDeterministicAndConfined(t *testing.T) {
|
|
identity, err := NewIdentity(representativeIdentityInput())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
first, err := identity.RelativePath()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := identity.RelativePath()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first != second {
|
|
t.Fatalf("relative path is not deterministic: %q != %q", first, second)
|
|
}
|
|
if filepath.IsAbs(first) || filepath.Clean(first) != first || filepath.ToSlash(first) != first {
|
|
t.Fatalf("path is not a clean relative slash-separated path: %q", first)
|
|
}
|
|
if strings.Contains(first, "../") || strings.HasPrefix(first, "../") || strings.Contains(first, `\\`) {
|
|
t.Fatalf("path escapes its root: %q", first)
|
|
}
|
|
parts := strings.Split(first, "/")
|
|
if len(parts) != 4 || parts[0] != "pipeline" || !strings.HasPrefix(parts[1], "input-") {
|
|
t.Fatalf("path does not contain the documented identity hierarchy: %q", first)
|
|
}
|
|
if !strings.Contains(parts[1], "source-digest") || !strings.Contains(parts[2], "pipeline-digest") || parts[3] == "" {
|
|
t.Fatalf("path omits digest-derived hierarchy: %q", first)
|
|
}
|
|
}
|
|
|
|
func representativeIdentityInput() IdentityInput {
|
|
return IdentityInput{
|
|
Pipeline: pipeline.ResolvedPipeline{
|
|
ID: "pipeline",
|
|
Digest: "sha256:pipeline-digest-000000000000",
|
|
Input: pipeline.Binding("input"),
|
|
ArtifactLanes: []pipeline.ResolvedArtifactLane{
|
|
{ID: "lane-b"},
|
|
{ID: "lane-a"},
|
|
},
|
|
},
|
|
InputKey: "input",
|
|
RawInputDigest: "sha256:raw-input-000000000000",
|
|
SourceDigest: "sha256:source-digest-000000000000",
|
|
SelectedLanes: []string{"lane-b", "lane-a"},
|
|
RuntimeOverrides: []Fingerprint{{Name: "timeout", Value: "30s"}, {Name: "model", Value: "large"}},
|
|
References: []artifacts.ReferenceProvenance{
|
|
{Stage: "chunk", SlotName: "glossary", OriginURI: "file:///glossary", Digest: "sha256:reference-1"},
|
|
{Stage: "extract", LaneID: "lane-a", SlotName: "party", OriginURI: "file:///party", Digest: "sha256:reference-2"},
|
|
},
|
|
ProvenanceFingerprints: []Fingerprint{{Name: "runner", Value: "v1"}, {Name: "source", Value: "v2"}},
|
|
}
|
|
}
|
|
|
|
func cloneIdentityInput(input IdentityInput) IdentityInput {
|
|
input.SelectedLanes = append([]string(nil), input.SelectedLanes...)
|
|
input.RuntimeOverrides = append([]Fingerprint(nil), input.RuntimeOverrides...)
|
|
input.References = append([]artifacts.ReferenceProvenance(nil), input.References...)
|
|
input.ProvenanceFingerprints = append([]Fingerprint(nil), input.ProvenanceFingerprints...)
|
|
input.Pipeline.ArtifactLanes = append([]pipeline.ResolvedArtifactLane(nil), input.Pipeline.ArtifactLanes...)
|
|
return input
|
|
}
|