Files
DoGaMa-serv/internal/catalog/template_test.go
T
codex 4ed3c8ae58
CI / validate (pull_request) Successful in 26m33s
refactor(catalog): make template artwork fully local
2026-08-26 21:58:30 +02:00

199 lines
7.0 KiB
Go

package catalog_test
import (
"errors"
"io/fs"
"path"
"strings"
"testing"
catalogdata "git.zaynet.fr/DoGaMa/DoGaMa-serv/catalog"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/catalog"
)
// TestBuiltInCatalogValidatesEveryDiscoveredTemplate deliberately enumerates
// the embedded filesystem instead of knowing any game IDs, versions or order.
func TestBuiltInCatalogValidatesEveryDiscoveredTemplate(t *testing.T) {
names, err := fs.Glob(catalogdata.Files, "*/template.yaml")
if err != nil {
t.Fatal(err)
}
if len(names) == 0 {
t.Fatal("embedded catalog contains no templates")
}
snapshots, err := catalog.LoadFS(catalogdata.Files, ".")
if err != nil {
t.Fatalf("load embedded catalog: %v", err)
}
if len(snapshots) != len(names) {
t.Fatalf("loaded %d templates after discovering %d paths", len(snapshots), len(names))
}
loaded := make(map[string]catalog.Snapshot, len(snapshots))
for _, snapshot := range snapshots {
key := snapshot.Template.ID + "@" + snapshot.Template.Version
if _, duplicate := loaded[key]; duplicate {
t.Fatalf("duplicate loaded template identity %q", key)
}
loaded[key] = snapshot
}
for _, name := range names {
body, readErr := catalogdata.Files.ReadFile(name)
if readErr != nil {
t.Fatalf("read embedded template %q: %v", name, readErr)
}
snapshot, validateErr := catalog.Validate(body, path.Dir(name), catalogdata.Files)
if validateErr != nil {
t.Fatalf("validate embedded template path=%q: %v", name, validateErr)
}
key := snapshot.Template.ID + "@" + snapshot.Template.Version
if _, ok := loaded[key]; !ok {
t.Fatalf("validated embedded template path=%q id=%q was not loaded", name, snapshot.Template.ID)
}
for _, asset := range snapshot.Template.Container.Assets {
assetPath := path.Join(path.Dir(name), asset.Source)
if _, statErr := fs.Stat(catalogdata.Files, assetPath); statErr != nil {
t.Fatalf("template path=%q id=%q asset=%q is missing: %v", name, snapshot.Template.ID, assetPath, statErr)
}
}
if snapshot.Template.Module != nil {
if _, ok := snapshot.ModuleFiles[snapshot.Template.Module.Path]; !ok {
t.Fatalf("template path=%q id=%q declared module=%q is missing", name, snapshot.Template.ID, snapshot.Template.Module.Path)
}
}
for _, field := range []string{"logo", "image", "poster"} {
if len(snapshot.AssetFiles[field]) == 0 {
t.Fatalf("template path=%q id=%q artwork=%q was not bundled", name, snapshot.Template.ID, field)
}
}
}
}
func TestArtworkURLsAndTraversalAreRejectedGenerically(t *testing.T) {
name, body, snapshot := embeddedTemplate(t)
for _, value := range []string{"https://example.invalid/art.jpg", "/tmp/art.jpg", "../art.jpg"} {
t.Run(value, func(t *testing.T) {
invalid := []byte(strings.Replace(string(body), snapshot.Template.Game.Artwork.Image, value, 1))
if _, err := catalog.Validate(invalid, path.Dir(name), catalogdata.Files); err == nil {
t.Fatalf("unsafe artwork path accepted: %q", value)
}
})
}
}
func TestCrossValidationRejectsMissingDeclaredModule(t *testing.T) {
name, body, snapshot := embeddedTemplate(t)
if snapshot.Template.Module == nil {
t.Skip("embedded fixture has no module")
}
body = []byte(strings.Replace(string(body), snapshot.Template.Module.Path, "module/missing.yaml", 1))
_, err := catalog.Validate(body, path.Dir(name), catalogdata.Files)
var validation *catalog.ValidationErrors
if !errors.As(err, &validation) || len(validation.Issues) == 0 || validation.Issues[0].Path != "/module/path" {
t.Fatalf("validation error = %#v", err)
}
}
func TestCrossValidationRejectsModuleTraversalAndAbsolutePath(t *testing.T) {
name, body, snapshot := embeddedTemplate(t)
if snapshot.Template.Module == nil {
t.Skip("embedded fixture has no module")
}
for _, modulePath := range []string{"../outside.wasm", "/outside.wasm"} {
t.Run(modulePath, func(t *testing.T) {
invalid := []byte(strings.Replace(string(body), snapshot.Template.Module.Path, modulePath, 1))
_, err := catalog.Validate(invalid, path.Dir(name), catalogdata.Files)
if err == nil {
t.Fatalf("unsafe module path accepted: %q", modulePath)
}
})
}
}
func TestBuiltInCatalogIsDeterministic(t *testing.T) {
first, err := catalog.LoadFS(catalogdata.Files, ".")
if err != nil {
t.Fatal(err)
}
second, err := catalog.LoadFS(catalogdata.Files, ".")
if err != nil {
t.Fatal(err)
}
if len(first) != len(second) {
t.Fatalf("catalog snapshot count differs: first=%d second=%d", len(first), len(second))
}
for index, snapshot := range first {
other := second[index]
if snapshot.Template.ID != other.Template.ID || snapshot.Template.Version != other.Template.Version || snapshot.Digest != other.Digest || snapshot.CanonicalYAML != other.CanonicalYAML {
t.Fatalf("catalog snapshot %d is not deterministic: first=%#v second=%#v", index, snapshot, other)
}
}
}
func TestCrossValidationRejectsMissingArtworkAsset(t *testing.T) {
name, body, snapshot := embeddedTemplate(t)
body = []byte(strings.Replace(string(body), "image: "+snapshot.Template.Game.Artwork.Image, "image: assets/missing", 1))
_, err := catalog.Validate(body, path.Dir(name), catalogdata.Files)
var validation *catalog.ValidationErrors
if !errors.As(err, &validation) {
t.Fatalf("validation error = %#v", err)
}
for _, issue := range validation.Issues {
if issue.Path == "/game/artwork/image" {
return
}
}
t.Fatalf("issues = %#v", validation.Issues)
}
func TestSchemaErrorsContainFieldPathAndLine(t *testing.T) {
name, body, _ := embeddedTemplate(t)
body = []byte(strings.Replace(string(body), "schema_version: 1", "schema_version: 2", 1))
_, err := catalog.Validate(body, path.Dir(name), catalogdata.Files)
var validation *catalog.ValidationErrors
if !errors.As(err, &validation) || len(validation.Issues) == 0 {
t.Fatalf("validation error = %#v", err)
}
if validation.Issues[0].Path == "" || validation.Issues[0].Line == 0 {
t.Fatalf("validation issue = %#v", validation.Issues[0])
}
}
func TestCrossValidationRejectsUnknownBackupMount(t *testing.T) {
name, body, snapshot := embeddedTemplate(t)
normalized := strings.ReplaceAll(string(body), "\r\n", "\n")
body = []byte(strings.Replace(normalized, " - "+snapshot.Template.Backup.SourceMounts[0]+"\n", " - missing\n", 1))
_, err := catalog.Validate(body, path.Dir(name), catalogdata.Files)
var validation *catalog.ValidationErrors
if !errors.As(err, &validation) {
t.Fatalf("validation error = %#v", err)
}
for _, issue := range validation.Issues {
if issue.Path == "/backup/source_mounts" {
return
}
}
t.Fatalf("issues = %#v", validation.Issues)
}
func embeddedTemplate(t *testing.T) (string, []byte, catalog.Snapshot) {
t.Helper()
names, err := fs.Glob(catalogdata.Files, "*/template.yaml")
if err != nil || len(names) == 0 {
t.Fatalf("discover embedded template: names=%v err=%v", names, err)
}
body, err := catalogdata.Files.ReadFile(names[0])
if err != nil {
t.Fatal(err)
}
snapshot, err := catalog.Validate(body, path.Dir(names[0]), catalogdata.Files)
if err != nil {
t.Fatal(err)
}
return names[0], body, snapshot
}