Skip to content

Commit

Permalink
Tests
Browse files Browse the repository at this point in the history
  • Loading branch information
firelizzard18 committed Jul 5, 2024
1 parent 1dca716 commit 8346987
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 19 deletions.
78 changes: 59 additions & 19 deletions cmd/accumulated/run/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
Expand All @@ -27,6 +28,10 @@ func (c *Config) FilePath() string { return c.file }
func (c *Config) SetFilePath(p string) { c.file = p }

func (c *Config) LoadFrom(file string) error {
return c.LoadFromFS(os.DirFS("."), file)
}

func (c *Config) LoadFromFS(fs fs.FS, file string) error {
var format func([]byte, any) error
switch s := filepath.Ext(file); s {
case ".toml", ".tml", ".ini":
Expand All @@ -39,12 +44,19 @@ func (c *Config) LoadFrom(file string) error {
return errors.BadRequest.WithFormat("unknown file type %s", s)
}

b, err := os.ReadFile(file)
f, err := fs.Open(file)
if err != nil {
return err
}
defer func() { _ = f.Close() }()

b, err := io.ReadAll(f)
if err != nil {
return err
}

c.file = file
c.fs = fs
return c.Load(b, format)
}

Expand Down Expand Up @@ -80,18 +92,54 @@ func (c *Config) applyDotEnv() error {
file = filepath.Join(dir, file)
}

env, err := godotenv.Read(file)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
var expand func(name string) string
var errs []error

f, err := c.fs.Open(file)
switch {
case err == nil:
defer func() { _ = f.Close() }()

// Parse
env, err := godotenv.Parse(f)
if err != nil {
return err
}

// And expand
expand = func(name string) string {
value, ok := env[name]
if ok {
return value
}
errs = append(errs, fmt.Errorf("%q is not defined", name))
return fmt.Sprintf("#!MISSING(%q)", name)
}

case errors.Is(err, fs.ErrNotExist):
// Only return an error if there is at least one ${ENV}
expand = func(name string) string {
if len(errs) == 0 {
errs = append(errs, err)
}
return fmt.Sprintf("#!MISSING(%q)", name)
}

default:
return err
}

return errors.Join(expandEnv(reflect.ValueOf(c), env)...)
expandEnv(reflect.ValueOf(c), expand)
return errors.Join(errs...)
}

func (c *Config) Save() error {
if c.file == "" {
return errors.BadRequest.With("not loaded from a file")
}
if c.fs != os.DirFS(".") {
return errors.BadRequest.With("loaded from an immutable filesystem")
}
return c.SaveTo(c.file)
}

Expand Down Expand Up @@ -193,42 +241,34 @@ func float2int(v reflect.Value) any {
}
}

func expandEnv(v reflect.Value, env map[string]string) (errs []error) {
func expandEnv(v reflect.Value, expand func(string) string) {
switch v.Kind() {
case reflect.String:
s := v.String()
s = os.Expand(s, func(name string) string {
value, ok := env[name]
if ok {
return value
}
errs = append(errs, fmt.Errorf("%q is not defined", name))
return fmt.Sprintf("#!MISSING(%q)", name)
})
s = os.Expand(s, expand)
v.SetString(s)

case reflect.Pointer, reflect.Interface:
errs = append(errs, expandEnv(v.Elem(), env)...)
expandEnv(v.Elem(), expand)

case reflect.Slice, reflect.Array:
for i, n := 0, v.Len(); i < n; i++ {
errs = append(errs, expandEnv(v.Index(i), env)...)
expandEnv(v.Index(i), expand)
}

case reflect.Map:
it := v.MapRange()
for it.Next() {
errs = append(errs, expandEnv(it.Key(), env)...)
errs = append(errs, expandEnv(it.Value(), env)...)
expandEnv(it.Key(), expand)
expandEnv(it.Value(), expand)
}

case reflect.Struct:
typ := v.Type()
for i, n := 0, typ.NumField(); i < n; i++ {
if typ.Field(i).IsExported() {
errs = append(errs, expandEnv(v.Field(i), env)...)
expandEnv(v.Field(i), expand)
}
}
}
return errs
}
3 changes: 3 additions & 0 deletions cmd/accumulated/run/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Config:
- name: file
type: string
marshal-as: none
- name: fs
type: fs.FS
marshal-as: none
- name: DotEnv
type: bool
pointer: true
Expand Down
83 changes: 83 additions & 0 deletions cmd/accumulated/run/dotenv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package run

import (
"io/fs"
"os"
"strings"
"testing"

"github.com/stretchr/testify/require"
testutil "gitlab.com/accumulatenetwork/accumulate/test/util"
)

func TestDotenv(t *testing.T) {
// When dot-env is set, ${FOO} is resolved
t.Run("Set", func(t *testing.T) {
fs := mkfs(map[string]string{
".env": `
FOO=bar`,
"accumulate.toml": `
dot-env = true
network = "${FOO}"`,
})

cfg := new(Config)
require.NoError(t, cfg.LoadFromFS(fs, "accumulate.toml"))
require.Equal(t, "bar", cfg.Network)
})

// When dot-env is unset, ${FOO} is left as is
t.Run("Unset", func(t *testing.T) {
fs := mkfs(map[string]string{
".env": `
FOO=bar`,
"accumulate.toml": `
network = "${FOO}"`,
})

cfg := new(Config)
require.NoError(t, cfg.LoadFromFS(fs, "accumulate.toml"))
require.Equal(t, "${FOO}", cfg.Network)
})

// When dot-env is set, referencing an unset variable ${BAR} is an error
t.Run("Wrong var", func(t *testing.T) {
fs := mkfs(map[string]string{
".env": `
FOO=bar`,
"accumulate.toml": `
dot-env = true
network = "${BAR}"`,
})

cfg := new(Config)
err := cfg.LoadFromFS(fs, "accumulate.toml")
require.EqualError(t, err, `"BAR" is not defined`)
})

// Variable are resolved exclusively from .env, not from actual environment
// variables
t.Run("Ignore env", func(t *testing.T) {
fs := mkfs(map[string]string{
"accumulate.toml": `
dot-env = true
network = "${FOO}"`,
})
require.NoError(t, os.Setenv("FOO", "bar"))

cfg := new(Config)
err := cfg.LoadFromFS(fs, "accumulate.toml")
require.EqualError(t, err, `open .env: file does not exist`)
})
}

func mkfs(files map[string]string) fs.FS {
root := new(testutil.Dir)
for name, data := range files {
root.Files = append(root.Files, &testutil.File{
Name: name,
Data: strings.NewReader(data),
})
}
return root
}
2 changes: 2 additions & 0 deletions cmd/accumulated/run/types_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"log/slog"
"strings"
"time"
Expand Down Expand Up @@ -50,6 +51,7 @@ type CometPrivValFile struct {

type Config struct {
file string
fs fs.FS
DotEnv *bool `json:"dotEnv,omitempty" form:"dotEnv" query:"dotEnv" validate:"required"`
Network string `json:"network,omitempty" form:"network" query:"network" validate:"required"`
Logging *Logging `json:"logging,omitempty" form:"logging" query:"logging" validate:"required"`
Expand Down
127 changes: 127 additions & 0 deletions test/util/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package testutil

import (
"fmt"
"io"
"io/fs"
"strings"
"time"
)

type Dir struct {
Name string
Files []DirEntry
}

type DirEntry interface {
fs.File
stat() fsFileInfo
}

func (d *Dir) Read(b []byte) (int, error) { return 0, io.EOF }
func (d *Dir) Close() error { return nil }
func (d *Dir) Stat() (fs.FileInfo, error) { return d.stat(), nil }

func (d *Dir) stat() fsFileInfo {
return fsFileInfo{d.Name, 0, true}
}

func (d *Dir) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrInvalid,
}
}

s := strings.SplitN(name, "/", 2)
var f DirEntry
f, ok := d.open(s[0])
if !ok {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrNotExist,
}
}

if len(s) == 1 {
return f, nil
}

e, ok := f.(*Dir)
if !ok {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fmt.Errorf("%w: %q is not a directory", fs.ErrInvalid, s[0]),
}
}

return e.Open(s[1])
}

func (d *Dir) open(name string) (DirEntry, bool) {
for _, f := range d.Files {
if f.stat().name == name {
return f, true
}
}
return nil, false
}

func (d *Dir) ReadDir(n int) ([]fs.DirEntry, error) {
var entries []fs.DirEntry
for _, e := range d.Files {
entries = append(entries, fsDirEntry(e.stat()))
if n--; n == 0 {
break
}
}
return entries, nil
}

type File struct {
Name string
Data FileData
}

type FileData interface {
io.Reader
Len() int
}

func (f *File) Read(b []byte) (int, error) { return f.Data.Read(b) }
func (f *File) Close() error { return nil }
func (f *File) Stat() (fs.FileInfo, error) { return f.stat(), nil }

func (f *File) stat() fsFileInfo {
return fsFileInfo{f.Name, int64(f.Data.Len()), false}
}

type fsDirEntry fsFileInfo

func (e fsDirEntry) Name() string { return e.name }
func (e fsDirEntry) IsDir() bool { return e.isDir }
func (e fsDirEntry) Type() fs.FileMode { return fsFileInfo(e).Mode() & fs.ModeType }
func (e fsDirEntry) Info() (fs.FileInfo, error) { return fsFileInfo(e), nil }

type fsFileInfo struct {
name string
size int64
isDir bool
}

func (f fsFileInfo) Name() string { return f.name }
func (f fsFileInfo) Size() int64 { return f.size }
func (f fsFileInfo) ModTime() time.Time { return time.Now() }
func (f fsFileInfo) IsDir() bool { return f.isDir }
func (f fsFileInfo) Sys() any { return nil }

func (f fsFileInfo) Mode() fs.FileMode {
if f.isDir {
return fs.ModeDir | fs.ModePerm
}
return fs.ModePerm
}

0 comments on commit 8346987

Please sign in to comment.