199 lines
7.0 KiB
Go
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
|
|
}
|