Configuration Hardening
Key changes:
- Added internal/project/effective.go with normalized effective config, defaults, provenance, and
JSON output.
- Added config subcommands:
- config validate
- config effective [--json|--yaml]
- config inspect [<key>]
- config explain <key>
- config sources
- Added YAML schema fields for configurable outputs, cache roots, script cache, inventory extensions,
topdata output/wiki paths, and autogen cache root.
- Routed existing hardcoded paths through effective config wrappers for module archives, HAK
manifests/archives, script cache, music cache/credits, topdata outputs, and autogen caches.
- Added validation for unsafe generated output/cache paths.
- Updated command descriptions to avoid fixed build/, .cache, sow_top, etc.
- Added regression tests for defaults, provenance, deterministic effective config, YAML-controlled
derivation, config commands, and invalid paths.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package project
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -43,6 +44,128 @@ haks:
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveConfigAppliesVisibleToolkitDefaults(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
||||
module:
|
||||
name: Test Module
|
||||
resref: testmod
|
||||
paths:
|
||||
source: src
|
||||
`)
|
||||
|
||||
proj, err := Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
|
||||
effective := proj.EffectiveConfig()
|
||||
if got, want := effective.Paths.Build, "build"; got != want {
|
||||
t.Fatalf("expected default build path %q, got %q", want, got)
|
||||
}
|
||||
if got, want := effective.Outputs.HAKManifest, "haks.json"; got != want {
|
||||
t.Fatalf("expected default HAK manifest %q, got %q", want, got)
|
||||
}
|
||||
if got, want := effective.TopData.PackageHAK, "sow_top.hak"; got != want {
|
||||
t.Fatalf("expected default topdata HAK %q, got %q", want, got)
|
||||
}
|
||||
if prov := effective.Provenance["paths.build"]; prov.Source != "toolkit default" {
|
||||
t.Fatalf("expected paths.build toolkit default provenance, got %#v", prov)
|
||||
}
|
||||
if prov := effective.Provenance["module.name"]; prov.Source != "yaml" {
|
||||
t.Fatalf("expected module.name YAML provenance, got %#v", prov)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveConfigHonorsConfiguredOutputAndCacheFields(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
||||
module:
|
||||
name: Test Module
|
||||
resref: testmod
|
||||
paths:
|
||||
build: output
|
||||
cache: cache
|
||||
outputs:
|
||||
module_archive: modules/{module.resref}.mod
|
||||
hak_manifest: manifests/haks.json
|
||||
hak_archive: haks/{hak.name}.hak
|
||||
scripts:
|
||||
cache: "{paths.cache}/compiled"
|
||||
topdata:
|
||||
source: topdata
|
||||
build: "{paths.cache}"
|
||||
compiled_2da_dir: tables
|
||||
compiled_tlk: dialog.tlk
|
||||
package_hak: custom_top.hak
|
||||
package_tlk: custom_dialog.tlk
|
||||
wiki:
|
||||
output_root: docs
|
||||
pages_dir: pages
|
||||
state_file: wiki-state.json
|
||||
autogen:
|
||||
cache:
|
||||
root: "{paths.cache}/autogen"
|
||||
`)
|
||||
|
||||
proj, err := Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
|
||||
if got, want := proj.ModuleArchivePath(), filepath.Join(root, "output", "modules", "testmod.mod"); got != want {
|
||||
t.Fatalf("expected module archive path %s, got %s", want, got)
|
||||
}
|
||||
if got, want := proj.HAKManifestPath(), filepath.Join(root, "output", "manifests", "haks.json"); got != want {
|
||||
t.Fatalf("expected HAK manifest path %s, got %s", want, got)
|
||||
}
|
||||
if got, want := proj.HAKArchivePath("core_01"), filepath.Join(root, "output", "haks", "core_01.hak"); got != want {
|
||||
t.Fatalf("expected HAK archive path %s, got %s", want, got)
|
||||
}
|
||||
if got, want := proj.ScriptCacheDir(), filepath.Join(root, "cache", "compiled"); got != want {
|
||||
t.Fatalf("expected script cache path %s, got %s", want, got)
|
||||
}
|
||||
if got, want := proj.TopDataCompiled2DADir(), filepath.Join(root, "cache", "tables"); got != want {
|
||||
t.Fatalf("expected topdata 2da path %s, got %s", want, got)
|
||||
}
|
||||
if got, want := proj.TopDataWikiStatePath(), filepath.Join(root, "cache", "docs", "wiki-state.json"); got != want {
|
||||
t.Fatalf("expected wiki state path %s, got %s", want, got)
|
||||
}
|
||||
if got, want := proj.AutogenCachePath("manifest.json"), filepath.Join(root, "cache", "autogen", "manifest.json"); got != want {
|
||||
t.Fatalf("expected autogen cache path %s, got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveConfigJSONIsDeterministic(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
||||
module:
|
||||
name: Test Module
|
||||
resref: testmod
|
||||
paths:
|
||||
source: src
|
||||
`)
|
||||
|
||||
proj, err := Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
first, err := proj.EffectiveConfigJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("EffectiveConfigJSON returned error: %v", err)
|
||||
}
|
||||
second, err := proj.EffectiveConfigJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("EffectiveConfigJSON returned error: %v", err)
|
||||
}
|
||||
if !bytes.Equal(first, second) {
|
||||
t.Fatalf("effective config JSON is not deterministic")
|
||||
}
|
||||
if !bytes.Contains(first, []byte(`"hak_manifest": "haks.json"`)) {
|
||||
t.Fatalf("effective config JSON missing HAK manifest default: %s", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadYMLConfig(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeProjectFile(t, filepath.Join(root, ConfigFileYML), `
|
||||
@@ -346,6 +469,38 @@ func TestValidateLayoutRejectsInvalidTopDataPackageOutputNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLayoutRejectsUnsafeGeneratedOutputPaths(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mkdirAll(t, filepath.Join(root, "src"))
|
||||
mkdirAll(t, filepath.Join(root, "build"))
|
||||
|
||||
proj := &Project{
|
||||
Root: root,
|
||||
Config: Config{
|
||||
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
||||
Paths: PathConfig{Source: "src", Build: "build"},
|
||||
Outputs: OutputConfig{
|
||||
HAKManifest: "../haks.json",
|
||||
},
|
||||
TopData: TopDataConfig{
|
||||
Source: "topdata",
|
||||
Compiled2DADir: "../2da",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := proj.ValidateLayout()
|
||||
if err == nil {
|
||||
t.Fatal("expected unsafe output path validation error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "outputs.hak_manifest") {
|
||||
t.Fatalf("expected HAK manifest path validation error, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "topdata.compiled_2da_dir") {
|
||||
t.Fatalf("expected topdata path validation error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLayoutRejectsInvalidAutogenConsumerConfig(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mkdirAll(t, filepath.Join(root, "src"))
|
||||
|
||||
Reference in New Issue
Block a user