Verify prepared inputs from manifest evidence
This commit is contained in:
222
internal/artifacts/prepared_input_test.go
Normal file
222
internal/artifacts/prepared_input_test.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package artifacts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
|
||||
func TestResolvePreparedInputReturnsVerifiedIdentity(t *testing.T) {
|
||||
paths, m, canonicalPath, checksum := preparedInputFixture(t, artifactpolicy.SourceInputSpellCatalog, []byte("{\"spells\":[]}\n"))
|
||||
m.Inputs[0].Path = filepath.ToSlash(filepath.Join("inputs", "spell_catalog.json"))
|
||||
|
||||
identity, err := ResolvePreparedInput(paths, m, artifactpolicy.SourceInputSpellCatalog)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolvePreparedInput() error = %v", err)
|
||||
}
|
||||
wantAbsolute, err := filepath.Abs(canonicalPath)
|
||||
if err != nil {
|
||||
t.Fatalf("filepath.Abs() error = %v", err)
|
||||
}
|
||||
if identity.SourceID != artifactpolicy.SourceInputSpellCatalog ||
|
||||
identity.ManifestKind != "spell_catalog" ||
|
||||
identity.Path != wantAbsolute ||
|
||||
identity.RelativePath != "inputs/spell_catalog.json" ||
|
||||
identity.Checksum != checksum ||
|
||||
identity.Size != int64(len("{\"spells\":[]}\n")) {
|
||||
t.Fatalf("ResolvePreparedInput() = %#v", identity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePreparedInputRequiresCurrentManifestRecord(t *testing.T) {
|
||||
paths, _, _, _ := preparedInputFixture(t, artifactpolicy.SourceInputPlayers, []byte("- Alice\n"))
|
||||
|
||||
for _, m := range []*manifest.Manifest{nil, manifest.New("session", time.Now().UTC())} {
|
||||
_, err := ResolvePreparedInput(paths, m, artifactpolicy.SourceInputPlayers)
|
||||
if !errors.Is(err, ErrPreparedInputAbsent) {
|
||||
t.Fatalf("ResolvePreparedInput() error = %v, want ErrPreparedInputAbsent", err)
|
||||
}
|
||||
var absent *PreparedInputAbsentError
|
||||
if !errors.As(err, &absent) || absent.SourceID != artifactpolicy.SourceInputPlayers {
|
||||
t.Fatalf("ResolvePreparedInput() error = %#v, want typed players absence", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePreparedInputRejectsInvalidManifestEvidence(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*testing.T, SessionPaths, *manifest.Manifest, string)
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "duplicate record",
|
||||
mutate: func(_ *testing.T, _ SessionPaths, m *manifest.Manifest, _ string) {
|
||||
m.Inputs = append(m.Inputs, m.Inputs[0])
|
||||
},
|
||||
wantErr: "2 manifest records",
|
||||
},
|
||||
{
|
||||
name: "wrong kind",
|
||||
mutate: func(_ *testing.T, _ SessionPaths, m *manifest.Manifest, _ string) {
|
||||
m.Inputs[0].Kind = "players"
|
||||
},
|
||||
wantErr: "recorded with manifest kind",
|
||||
},
|
||||
{
|
||||
name: "wrong canonical path",
|
||||
mutate: func(t *testing.T, paths SessionPaths, m *manifest.Manifest, _ string) {
|
||||
wrong := filepath.Join(paths.InputsDir, "other.json")
|
||||
if err := os.WriteFile(wrong, []byte("other\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(wrong) error = %v", err)
|
||||
}
|
||||
m.Inputs[0].Path = wrong
|
||||
},
|
||||
wantErr: "does not match canonical path",
|
||||
},
|
||||
{
|
||||
name: "traversal path",
|
||||
mutate: func(_ *testing.T, _ SessionPaths, m *manifest.Manifest, _ string) {
|
||||
m.Inputs[0].Path = filepath.Join("..", "..", "outside.json")
|
||||
},
|
||||
wantErr: "outside session root",
|
||||
},
|
||||
{
|
||||
name: "missing checksum",
|
||||
mutate: func(_ *testing.T, _ SessionPaths, m *manifest.Manifest, _ string) {
|
||||
m.Inputs[0].Checksum = " "
|
||||
},
|
||||
wantErr: "manifest checksum is required",
|
||||
},
|
||||
{
|
||||
name: "checksum mismatch",
|
||||
mutate: func(_ *testing.T, _ SessionPaths, m *manifest.Manifest, _ string) {
|
||||
m.Inputs[0].Checksum = strings.Repeat("0", 64)
|
||||
},
|
||||
wantErr: "checksum mismatch",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
paths, m, canonicalPath, _ := preparedInputFixture(t, artifactpolicy.SourceInputSpellCatalog, []byte("{\"spells\":[]}\n"))
|
||||
tt.mutate(t, paths, m, canonicalPath)
|
||||
_, err := ResolvePreparedInput(paths, m, artifactpolicy.SourceInputSpellCatalog)
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("ResolvePreparedInput() error = %v, want containing %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePreparedInputRejectsInvalidCanonicalFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*testing.T, string)
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "missing",
|
||||
mutate: func(t *testing.T, path string) {
|
||||
if err := os.Remove(path); err != nil {
|
||||
t.Fatalf("Remove() error = %v", err)
|
||||
}
|
||||
},
|
||||
wantErr: "open prepared input source",
|
||||
},
|
||||
{
|
||||
name: "symlink",
|
||||
mutate: func(t *testing.T, path string) {
|
||||
outside := filepath.Join(t.TempDir(), "outside.json")
|
||||
if err := os.WriteFile(outside, []byte("outside\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(outside) error = %v", err)
|
||||
}
|
||||
if err := os.Remove(path); err != nil {
|
||||
t.Fatalf("Remove() error = %v", err)
|
||||
}
|
||||
if err := os.Symlink(outside, path); err != nil {
|
||||
t.Fatalf("Symlink() error = %v", err)
|
||||
}
|
||||
},
|
||||
wantErr: "not a regular file",
|
||||
},
|
||||
{
|
||||
name: "directory",
|
||||
mutate: func(t *testing.T, path string) {
|
||||
if err := os.Remove(path); err != nil {
|
||||
t.Fatalf("Remove() error = %v", err)
|
||||
}
|
||||
if err := os.Mkdir(path, 0o755); err != nil {
|
||||
t.Fatalf("Mkdir() error = %v", err)
|
||||
}
|
||||
},
|
||||
wantErr: "not a regular file",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
mutate: func(t *testing.T, path string) {
|
||||
if err := os.WriteFile(path, nil, 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(empty) error = %v", err)
|
||||
}
|
||||
},
|
||||
wantErr: "is empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
paths, m, canonicalPath, _ := preparedInputFixture(t, artifactpolicy.SourceInputSpellCatalog, []byte("{\"spells\":[]}\n"))
|
||||
tt.mutate(t, canonicalPath)
|
||||
_, err := ResolvePreparedInput(paths, m, artifactpolicy.SourceInputSpellCatalog)
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("ResolvePreparedInput() error = %v, want containing %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePreparedInputRejectsUnsupportedSource(t *testing.T) {
|
||||
paths := buildSessionPaths(t.TempDir(), "campaign", "session")
|
||||
_, err := ResolvePreparedInput(paths, nil, "narratio.input.unknown")
|
||||
if err == nil || errors.Is(err, ErrPreparedInputAbsent) || !strings.Contains(err.Error(), "unsupported prepared input source") {
|
||||
t.Fatalf("ResolvePreparedInput() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func preparedInputFixture(
|
||||
t *testing.T,
|
||||
sourceID string,
|
||||
payload []byte,
|
||||
) (SessionPaths, *manifest.Manifest, string, string) {
|
||||
t.Helper()
|
||||
paths := buildSessionPaths(t.TempDir(), "campaign", "session")
|
||||
descriptor, ok := artifactpolicy.DescribePreparedInputSource(sourceID)
|
||||
if !ok {
|
||||
t.Fatalf("DescribePreparedInputSource(%q) ok = false", sourceID)
|
||||
}
|
||||
canonicalPath := filepath.Join(paths.InputsDir, descriptor.Filename)
|
||||
if err := os.MkdirAll(paths.InputsDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(inputs) error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(canonicalPath, payload, 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(canonical) error = %v", err)
|
||||
}
|
||||
checksum, err := SHA256File(canonicalPath)
|
||||
if err != nil {
|
||||
t.Fatalf("SHA256File() error = %v", err)
|
||||
}
|
||||
m := manifest.New("session", time.Now().UTC())
|
||||
m.Inputs = []manifest.InputRecord{{
|
||||
Kind: descriptor.ManifestKind,
|
||||
Path: canonicalPath,
|
||||
Checksum: checksum,
|
||||
Source: "campaign_config",
|
||||
}}
|
||||
return paths, m, canonicalPath, checksum
|
||||
}
|
||||
Reference in New Issue
Block a user