222 lines
8.2 KiB
Go
222 lines
8.2 KiB
Go
package configkit_test
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
)
|
|
|
|
type observedResourceFS struct {
|
|
files fstest.MapFS
|
|
opens int
|
|
closed bool
|
|
}
|
|
|
|
func (f *observedResourceFS) Open(name string) (fs.File, error) {
|
|
f.opens++
|
|
if f.closed {
|
|
return nil, fs.ErrClosed
|
|
}
|
|
return f.files.Open(name)
|
|
}
|
|
|
|
type resourceFacadeConfig struct {
|
|
Native string `yaml:"native"`
|
|
Shared string `yaml:"shared"`
|
|
Host string `yaml:"host"`
|
|
Selected string `yaml:"selected"`
|
|
}
|
|
|
|
func TestFacadeSourceContextsFollowOriginsWithoutQueryIO(t *testing.T) {
|
|
host := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(host, "main.yml"), []byte("host: host.txt\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(host, "host.txt"), []byte("host-v1"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
borrowed := &observedResourceFS{files: fstest.MapFS{
|
|
"bundle/config/defaults.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: ../shared.yml}]}\nnative: icon.txt\n")},
|
|
"bundle/shared.yml": &fstest.MapFile{Data: []byte("shared: shared.txt\n")},
|
|
"bundle/overlays/test.yml": &fstest.MapFile{Data: []byte("selected: overlay.txt\n")},
|
|
"bundle/config/icon.txt": &fstest.MapFile{Data: []byte("icon-v1")},
|
|
"bundle/shared.txt": &fstest.MapFile{Data: []byte("shared-v1")},
|
|
"bundle/overlays/overlay.txt": &fstest.MapFile{Data: []byte("overlay-v1")},
|
|
}}
|
|
namespace := configkit.NewFilesystem("bundle", borrowed)
|
|
var resolverContext configkit.ResourceContext
|
|
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(filepath.Join(host, "main.yml"))}, configkit.LayeredOptions[resourceFacadeConfig]{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
|
|
Source: namespace.File("bundle/config/defaults.yml"), Imports: true,
|
|
Filesystem: &configkit.FilesystemContext{Root: "bundle", FS: &namespace},
|
|
}}}},
|
|
Overlay: "test", OverlayFilesystem: &configkit.FilesystemContext{Root: "bundle", BaseDir: "bundle", FS: &namespace},
|
|
Definition: configkit.Definition[resourceFacadeConfig]{Resolve: func(config *resourceFacadeConfig, resolution *configkit.Resolution) error {
|
|
before := borrowed.opens
|
|
var ok bool
|
|
resolverContext, ok = resolution.SourceContext("source-2")
|
|
if !ok || resolverContext.BaseDir != "config" || resolverContext.FS == nil {
|
|
t.Errorf("resolver context = %#v, %v", resolverContext, ok)
|
|
}
|
|
if _, ok := resolution.SourceContext("missing"); ok || borrowed.opens != before {
|
|
t.Error("resolution context query performed I/O or guessed an unknown source")
|
|
}
|
|
return nil
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("load: %v, diagnostics %#v", err, err.(*configkit.LoadError).Diagnostics())
|
|
}
|
|
queriesBefore := borrowed.opens
|
|
for _, test := range []struct {
|
|
field, wantBase, wantData string
|
|
}{
|
|
{"native", "config", "icon-v1"},
|
|
{"shared", ".", "shared-v1"},
|
|
{"selected", "overlays", "overlay-v1"},
|
|
{"host", ".", "host-v1"},
|
|
} {
|
|
origin, ok := result.Metadata().Lookup(configkit.Keys(test.field))
|
|
if !ok || len(origin.Origin.Locations) != 1 {
|
|
t.Fatalf("%s origin = %#v, %v", test.field, origin, ok)
|
|
}
|
|
context, ok := result.SourceContext(origin.Origin.Locations[0].SourceID)
|
|
if !ok || context.FS == nil || context.BaseDir != test.wantBase {
|
|
t.Fatalf("%s context = %#v, %v", test.field, context, ok)
|
|
}
|
|
if borrowed.opens != queriesBefore {
|
|
t.Fatal("result SourceContext query performed filesystem I/O")
|
|
}
|
|
resourceName := path.Join(context.BaseDir, map[string]string{
|
|
"native": result.Config.Native, "shared": result.Config.Shared,
|
|
"selected": result.Config.Selected, "host": result.Config.Host,
|
|
}[test.field])
|
|
data, err := fs.ReadFile(context.FS, resourceName)
|
|
if err != nil || string(data) != test.wantData {
|
|
t.Fatalf("%s resource = %q, %v", test.field, data, err)
|
|
}
|
|
queriesBefore = borrowed.opens
|
|
}
|
|
if _, ok := result.SourceContext(""); ok {
|
|
t.Fatal("empty source ID returned a context")
|
|
}
|
|
var nilResult *configkit.Result[resourceFacadeConfig]
|
|
if _, ok := nilResult.SourceContext("source-2"); ok {
|
|
t.Fatal("nil result returned a context")
|
|
}
|
|
if _, err := fs.ReadFile(resolverContext.FS, "../shared.txt"); !errors.Is(err, fs.ErrInvalid) {
|
|
t.Fatalf("native root escape = %v", err)
|
|
}
|
|
borrowed.files["bundle/config/icon.txt"].Data = []byte("icon-v2")
|
|
data, err := fs.ReadFile(resolverContext.FS, "config/icon.txt")
|
|
if err != nil || string(data) != "icon-v2" {
|
|
t.Fatalf("updated native resource = %q, %v", data, err)
|
|
}
|
|
borrowed.closed = true
|
|
if result.Config.Native != "icon.txt" {
|
|
t.Fatal("closing caller filesystem altered loaded configuration")
|
|
}
|
|
if _, err := fs.ReadFile(resolverContext.FS, "config/icon.txt"); !errors.Is(err, fs.ErrClosed) {
|
|
t.Fatalf("closed caller filesystem error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFacadeHostContextSurvivesConfigurationDeletion(t *testing.T) {
|
|
root := t.TempDir()
|
|
main := filepath.Join(root, "main.yml")
|
|
resource := filepath.Join(root, "asset.txt")
|
|
if err := os.WriteFile(main, []byte("native: asset.txt\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(resource, []byte("first"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, err := configkit.Load[resourceFacadeConfig](configkit.File(main))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
context, ok := result.SourceContext("source-1")
|
|
if !ok || context.BaseDir != "." {
|
|
t.Fatalf("host context = %#v, %v", context, ok)
|
|
}
|
|
if err := os.Remove(main); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(resource, []byte("second"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, result.Config.Native))
|
|
if err != nil || string(data) != "second" || result.Config.Native != "asset.txt" {
|
|
t.Fatalf("resource after source deletion = %q, %v", data, err)
|
|
}
|
|
if _, err := fs.ReadFile(context.FS, "../asset.txt"); !errors.Is(err, fs.ErrInvalid) {
|
|
t.Fatalf("invalid host path = %v", err)
|
|
}
|
|
if err := os.Remove(resource); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := fs.ReadFile(context.FS, "asset.txt"); !errors.Is(err, fs.ErrNotExist) {
|
|
t.Fatalf("missing later resource = %v", err)
|
|
}
|
|
outside := filepath.Join(t.TempDir(), "outside.txt")
|
|
if err := os.WriteFile(outside, []byte("outside"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(outside, filepath.Join(root, "escape")); err == nil {
|
|
if _, err := fs.ReadFile(context.FS, "escape"); !errors.Is(err, fs.ErrPermission) {
|
|
t.Fatalf("host symlink escape = %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFacadeSyntheticSourceContextsAndContextlessSources(t *testing.T) {
|
|
namespace := configkit.NewFilesystem("bundle", fstest.MapFS{
|
|
"bundle/config/icon.txt": &fstest.MapFile{Data: []byte("icon")},
|
|
})
|
|
context := &configkit.FilesystemContext{Root: "bundle", BaseDir: "bundle/config", FS: &namespace}
|
|
for _, test := range []struct {
|
|
name string
|
|
source configkit.Source
|
|
}{
|
|
{"bytes", configkit.Bytes("memory", []byte("native: icon.txt\n"))},
|
|
{"reader", configkit.Reader("stream", strings.NewReader("native: icon.txt\n"))},
|
|
{"mapping", configkit.Mapping("values", map[string]any{"native": "icon.txt"})},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
result, err := configkit.Load(test.source, configkit.Options[resourceFacadeConfig]{Filesystem: context})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resource, ok := result.SourceContext("source-1")
|
|
if !ok || resource.BaseDir != "config" {
|
|
t.Fatalf("synthetic context = %#v, %v", resource, ok)
|
|
}
|
|
data, err := fs.ReadFile(resource.FS, path.Join(resource.BaseDir, result.Config.Native))
|
|
if err != nil || string(data) != "icon" {
|
|
t.Fatalf("synthetic resource = %q, %v", data, err)
|
|
}
|
|
})
|
|
}
|
|
result, err := configkit.Load[resourceFacadeConfig](configkit.Bytes("plain", []byte("native: none\n")))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := result.SourceContext("source-1"); ok {
|
|
t.Fatal("contextless byte source returned a context")
|
|
}
|
|
result, err = configkit.Load(configkit.NoSource(), configkit.Options[resourceFacadeConfig]{Overrides: []configkit.Override{{Path: configkit.Keys("native"), Value: "from-cli", Flag: "--native"}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := result.SourceContext("source-1"); ok {
|
|
t.Fatal("CLI source returned a context")
|
|
}
|
|
}
|