Files
narratio/internal/artifacts/prepared_input_test.go

223 lines
7.1 KiB
Go

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
}