feat: inline tests + Fs zero-value fix + coverage 76.9% → 82.3%
Move all tests from tests/ to package root for proper coverage.
Fix Fs zero-value: path() and validatePath() default empty root
to "/" so &Fs{} works without New().
New tests: PathGlob, PathIsAbs, CleanPath, Cli.SetOutput,
ServiceShutdown, Core.Context, Fs zero-value, Fs protected
delete, Command lifecycle with implementation, error formatting
branches, PerformAsync completion/no-handler/after-shutdown,
Extract with templates, Embed path traversal.
Coverage: 76.9% → 82.3% (23 test files).
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
a06b779e3c
commit
e0c190ca8f
26 changed files with 547 additions and 9 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
package core_test
|
package core_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "dappco.re/go/core"
|
. "dappco.re/go/core"
|
||||||
|
|
@ -74,3 +75,11 @@ func TestCli_PrintHelp_Good(t *testing.T) {
|
||||||
c.Command("serve", Command{Action: func(_ Options) Result { return Result{OK: true} }})
|
c.Command("serve", Command{Action: func(_ Options) Result { return Result{OK: true} }})
|
||||||
c.Cli().PrintHelp()
|
c.Cli().PrintHelp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCli_SetOutput_Good(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
c.Cli().SetOutput(&buf)
|
||||||
|
c.Cli().Print("hello %s", "world")
|
||||||
|
assert.Contains(t, buf.String(), "hello world")
|
||||||
|
}
|
||||||
|
|
@ -121,6 +121,93 @@ func TestCommand_Lifecycle_NoImpl_Good(t *testing.T) {
|
||||||
assert.False(t, cmd.Signal("HUP").OK)
|
assert.False(t, cmd.Signal("HUP").OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Lifecycle with Implementation ---
|
||||||
|
|
||||||
|
type testLifecycle struct {
|
||||||
|
started bool
|
||||||
|
stopped bool
|
||||||
|
restarted bool
|
||||||
|
reloaded bool
|
||||||
|
signalled string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *testLifecycle) Start(opts Options) Result {
|
||||||
|
l.started = true
|
||||||
|
return Result{Value: "started", OK: true}
|
||||||
|
}
|
||||||
|
func (l *testLifecycle) Stop() Result {
|
||||||
|
l.stopped = true
|
||||||
|
return Result{OK: true}
|
||||||
|
}
|
||||||
|
func (l *testLifecycle) Restart() Result {
|
||||||
|
l.restarted = true
|
||||||
|
return Result{OK: true}
|
||||||
|
}
|
||||||
|
func (l *testLifecycle) Reload() Result {
|
||||||
|
l.reloaded = true
|
||||||
|
return Result{OK: true}
|
||||||
|
}
|
||||||
|
func (l *testLifecycle) Signal(sig string) Result {
|
||||||
|
l.signalled = sig
|
||||||
|
return Result{Value: sig, OK: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommand_Lifecycle_WithImpl_Good(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
lc := &testLifecycle{}
|
||||||
|
c.Command("daemon", Command{Lifecycle: lc})
|
||||||
|
cmd := c.Command("daemon").Value.(*Command)
|
||||||
|
|
||||||
|
r := cmd.Start(Options{})
|
||||||
|
assert.True(t, r.OK)
|
||||||
|
assert.True(t, lc.started)
|
||||||
|
|
||||||
|
assert.True(t, cmd.Stop().OK)
|
||||||
|
assert.True(t, lc.stopped)
|
||||||
|
|
||||||
|
assert.True(t, cmd.Restart().OK)
|
||||||
|
assert.True(t, lc.restarted)
|
||||||
|
|
||||||
|
assert.True(t, cmd.Reload().OK)
|
||||||
|
assert.True(t, lc.reloaded)
|
||||||
|
|
||||||
|
r = cmd.Signal("HUP")
|
||||||
|
assert.True(t, r.OK)
|
||||||
|
assert.Equal(t, "HUP", lc.signalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommand_Duplicate_Bad(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
|
||||||
|
r := c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
|
||||||
|
assert.False(t, r.OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommand_InvalidPath_Bad(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
assert.False(t, c.Command("/leading", Command{}).OK)
|
||||||
|
assert.False(t, c.Command("trailing/", Command{}).OK)
|
||||||
|
assert.False(t, c.Command("double//slash", Command{}).OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cli Run with Lifecycle ---
|
||||||
|
|
||||||
|
func TestCli_Run_Lifecycle_Good(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
lc := &testLifecycle{}
|
||||||
|
c.Command("serve", Command{Lifecycle: lc})
|
||||||
|
r := c.Cli().Run("serve")
|
||||||
|
assert.True(t, r.OK)
|
||||||
|
assert.True(t, lc.started)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCli_Run_NoActionNoLifecycle_Bad(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
c.Command("empty", Command{})
|
||||||
|
r := c.Cli().Run("empty")
|
||||||
|
assert.False(t, r.OK)
|
||||||
|
}
|
||||||
|
|
||||||
// --- Empty path ---
|
// --- Empty path ---
|
||||||
|
|
||||||
func TestCommand_EmptyPath_Bad(t *testing.T) {
|
func TestCommand_EmptyPath_Bad(t *testing.T) {
|
||||||
|
|
@ -157,6 +157,94 @@ func TestGeneratePack_WithFiles_Good(t *testing.T) {
|
||||||
assert.Contains(t, r.Value.(string), "core.AddAsset")
|
assert.Contains(t, r.Value.(string), "core.AddAsset")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Extract (template + nested) ---
|
||||||
|
|
||||||
|
func TestExtract_WithTemplate_Good(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
// Create an in-memory FS with a template file and a plain file
|
||||||
|
tmplDir := os.DirFS(t.TempDir())
|
||||||
|
|
||||||
|
// Use a real temp dir with files
|
||||||
|
srcDir := t.TempDir()
|
||||||
|
os.WriteFile(srcDir+"/plain.txt", []byte("static content"), 0644)
|
||||||
|
os.WriteFile(srcDir+"/greeting.tmpl", []byte("Hello {{.Name}}!"), 0644)
|
||||||
|
os.MkdirAll(srcDir+"/sub", 0755)
|
||||||
|
os.WriteFile(srcDir+"/sub/nested.txt", []byte("nested"), 0644)
|
||||||
|
|
||||||
|
_ = tmplDir
|
||||||
|
fsys := os.DirFS(srcDir)
|
||||||
|
data := map[string]string{"Name": "World"}
|
||||||
|
|
||||||
|
r := Extract(fsys, dir, data)
|
||||||
|
assert.True(t, r.OK)
|
||||||
|
|
||||||
|
// Plain file copied
|
||||||
|
content, err := os.ReadFile(dir + "/plain.txt")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "static content", string(content))
|
||||||
|
|
||||||
|
// Template processed and .tmpl stripped
|
||||||
|
greeting, err := os.ReadFile(dir + "/greeting")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Hello World!", string(greeting))
|
||||||
|
|
||||||
|
// Nested directory preserved
|
||||||
|
nested, err := os.ReadFile(dir + "/sub/nested.txt")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "nested", string(nested))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtract_BadTargetDir_Ugly(t *testing.T) {
|
||||||
|
srcDir := t.TempDir()
|
||||||
|
os.WriteFile(srcDir+"/f.txt", []byte("x"), 0644)
|
||||||
|
r := Extract(os.DirFS(srcDir), "/nonexistent/deeply/nested/impossible", nil)
|
||||||
|
// Should fail gracefully, not panic
|
||||||
|
_ = r
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmbed_PathTraversal_Ugly(t *testing.T) {
|
||||||
|
emb := Mount(testFS, "testdata").Value.(*Embed)
|
||||||
|
r := emb.ReadFile("../../etc/passwd")
|
||||||
|
assert.False(t, r.OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmbed_Sub_BaseDir_Good(t *testing.T) {
|
||||||
|
emb := Mount(testFS, "testdata").Value.(*Embed)
|
||||||
|
r := emb.Sub("scantest")
|
||||||
|
assert.True(t, r.OK)
|
||||||
|
sub := r.Value.(*Embed)
|
||||||
|
assert.Equal(t, ".", sub.BaseDirectory())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmbed_Open_Bad(t *testing.T) {
|
||||||
|
emb := Mount(testFS, "testdata").Value.(*Embed)
|
||||||
|
r := emb.Open("nonexistent.txt")
|
||||||
|
assert.False(t, r.OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmbed_ReadDir_Bad(t *testing.T) {
|
||||||
|
emb := Mount(testFS, "testdata").Value.(*Embed)
|
||||||
|
r := emb.ReadDir("nonexistent")
|
||||||
|
assert.False(t, r.OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmbed_EmbedFS_Original_Good(t *testing.T) {
|
||||||
|
emb := Mount(testFS, "testdata").Value.(*Embed)
|
||||||
|
efs := emb.EmbedFS()
|
||||||
|
_, err := efs.ReadFile("testdata/test.txt")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtract_NilData_Good(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
srcDir := t.TempDir()
|
||||||
|
os.WriteFile(srcDir+"/file.txt", []byte("no template"), 0644)
|
||||||
|
|
||||||
|
r := Extract(os.DirFS(srcDir), dir, nil)
|
||||||
|
assert.True(t, r.OK)
|
||||||
|
}
|
||||||
|
|
||||||
func mustCompress(input string) string {
|
func mustCompress(input string) string {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
b64 := base64.NewEncoder(base64.StdEncoding, &buf)
|
b64 := base64.NewEncoder(base64.StdEncoding, &buf)
|
||||||
|
|
@ -227,3 +227,46 @@ func TestErrorPanic_CrashFile_Good(t *testing.T) {
|
||||||
assert.Nil(t, r.Value)
|
assert.Nil(t, r.Value)
|
||||||
_ = path
|
_ = path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Error formatting branches ---
|
||||||
|
|
||||||
|
func TestErr_Error_WithCode_Good(t *testing.T) {
|
||||||
|
err := WrapCode(errors.New("bad"), "INVALID", "validate", "input failed")
|
||||||
|
assert.Contains(t, err.Error(), "[INVALID]")
|
||||||
|
assert.Contains(t, err.Error(), "validate")
|
||||||
|
assert.Contains(t, err.Error(), "bad")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErr_Error_CodeNoCause_Good(t *testing.T) {
|
||||||
|
err := NewCode("NOT_FOUND", "resource missing")
|
||||||
|
assert.Contains(t, err.Error(), "[NOT_FOUND]")
|
||||||
|
assert.Contains(t, err.Error(), "resource missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErr_Error_NoOp_Good(t *testing.T) {
|
||||||
|
err := &Err{Message: "bare error"}
|
||||||
|
assert.Equal(t, "bare error", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapCode_NilErr_EmptyCode_Good(t *testing.T) {
|
||||||
|
err := WrapCode(nil, "", "op", "msg")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrap_PreservesCode_Good(t *testing.T) {
|
||||||
|
inner := WrapCode(errors.New("root"), "AUTH_FAIL", "auth", "denied")
|
||||||
|
outer := Wrap(inner, "handler", "request failed")
|
||||||
|
assert.Equal(t, "AUTH_FAIL", ErrorCode(outer))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorLog_Warn_Nil_Good(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
r := c.LogWarn(nil, "op", "msg")
|
||||||
|
assert.True(t, r.OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorLog_Error_Nil_Good(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
r := c.LogError(nil, "op", "msg")
|
||||||
|
assert.True(t, r.OK)
|
||||||
|
}
|
||||||
25
fs.go
25
fs.go
|
|
@ -15,15 +15,20 @@ type Fs struct {
|
||||||
|
|
||||||
// path sanitises and returns the full path.
|
// path sanitises and returns the full path.
|
||||||
// Absolute paths are sandboxed under root (unless root is "/").
|
// Absolute paths are sandboxed under root (unless root is "/").
|
||||||
|
// Empty root defaults to "/" — the zero value of Fs is usable.
|
||||||
func (m *Fs) path(p string) string {
|
func (m *Fs) path(p string) string {
|
||||||
|
root := m.root
|
||||||
|
if root == "" {
|
||||||
|
root = "/"
|
||||||
|
}
|
||||||
if p == "" {
|
if p == "" {
|
||||||
return m.root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the path is relative and the medium is rooted at "/",
|
// If the path is relative and the medium is rooted at "/",
|
||||||
// treat it as relative to the current working directory.
|
// treat it as relative to the current working directory.
|
||||||
// This makes io.Local behave more like the standard 'os' package.
|
// This makes io.Local behave more like the standard 'os' package.
|
||||||
if m.root == "/" && !filepath.IsAbs(p) {
|
if root == "/" && !filepath.IsAbs(p) {
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
return filepath.Join(cwd, p)
|
return filepath.Join(cwd, p)
|
||||||
}
|
}
|
||||||
|
|
@ -33,23 +38,27 @@ func (m *Fs) path(p string) string {
|
||||||
clean := filepath.Clean("/" + p)
|
clean := filepath.Clean("/" + p)
|
||||||
|
|
||||||
// If root is "/", allow absolute paths through
|
// If root is "/", allow absolute paths through
|
||||||
if m.root == "/" {
|
if root == "/" {
|
||||||
return clean
|
return clean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip leading "/" so Join works correctly with root
|
// Strip leading "/" so Join works correctly with root
|
||||||
return filepath.Join(m.root, clean[1:])
|
return filepath.Join(root, clean[1:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// validatePath ensures the path is within the sandbox, following symlinks if they exist.
|
// validatePath ensures the path is within the sandbox, following symlinks if they exist.
|
||||||
func (m *Fs) validatePath(p string) Result {
|
func (m *Fs) validatePath(p string) Result {
|
||||||
if m.root == "/" {
|
root := m.root
|
||||||
|
if root == "" {
|
||||||
|
root = "/"
|
||||||
|
}
|
||||||
|
if root == "/" {
|
||||||
return Result{m.path(p), true}
|
return Result{m.path(p), true}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split the cleaned path into components
|
// Split the cleaned path into components
|
||||||
parts := Split(filepath.Clean("/"+p), string(os.PathSeparator))
|
parts := Split(filepath.Clean("/"+p), string(os.PathSeparator))
|
||||||
current := m.root
|
current := root
|
||||||
|
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
if part == "" {
|
if part == "" {
|
||||||
|
|
@ -70,7 +79,7 @@ func (m *Fs) validatePath(p string) Result {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the resolved part is still within the root
|
// Verify the resolved part is still within the root
|
||||||
rel, err := filepath.Rel(m.root, realNext)
|
rel, err := filepath.Rel(root, realNext)
|
||||||
if err != nil || HasPrefix(rel, "..") {
|
if err != nil || HasPrefix(rel, "..") {
|
||||||
// Security event: sandbox escape attempt
|
// Security event: sandbox escape attempt
|
||||||
username := "unknown"
|
username := "unknown"
|
||||||
|
|
@ -78,7 +87,7 @@ func (m *Fs) validatePath(p string) Result {
|
||||||
username = u.Username
|
username = u.Username
|
||||||
}
|
}
|
||||||
Print(os.Stderr, "[%s] SECURITY sandbox escape detected root=%s path=%s attempted=%s user=%s",
|
Print(os.Stderr, "[%s] SECURITY sandbox escape detected root=%s path=%s attempted=%s user=%s",
|
||||||
time.Now().Format(time.RFC3339), m.root, p, realNext, username)
|
time.Now().Format(time.RFC3339), root, p, realNext, username)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = E("fs.validatePath", Concat("sandbox escape: ", p, " resolves outside ", m.root), nil)
|
err = E("fs.validatePath", Concat("sandbox escape: ", p, " resolves outside ", m.root), nil)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -184,3 +184,74 @@ func TestFs_WriteMode_Good(t *testing.T) {
|
||||||
assert.True(t, r.OK)
|
assert.True(t, r.OK)
|
||||||
assert.Equal(t, "secret.txt", r.Value.(os.FileInfo).Name())
|
assert.Equal(t, "secret.txt", r.Value.(os.FileInfo).Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Zero Value ---
|
||||||
|
|
||||||
|
func TestFs_ZeroValue_Good(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
zeroFs := &Fs{}
|
||||||
|
|
||||||
|
path := filepath.Join(dir, "zero.txt")
|
||||||
|
assert.True(t, zeroFs.Write(path, "zero value works").OK)
|
||||||
|
r := zeroFs.Read(path)
|
||||||
|
assert.True(t, r.OK)
|
||||||
|
assert.Equal(t, "zero value works", r.Value.(string))
|
||||||
|
assert.True(t, zeroFs.IsFile(path))
|
||||||
|
assert.True(t, zeroFs.Exists(path))
|
||||||
|
assert.True(t, zeroFs.IsDir(dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFs_ZeroValue_List_Good(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
zeroFs := &Fs{}
|
||||||
|
|
||||||
|
os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a"), 0644)
|
||||||
|
r := zeroFs.List(dir)
|
||||||
|
assert.True(t, r.OK)
|
||||||
|
entries := r.Value.([]fs.DirEntry)
|
||||||
|
assert.Len(t, entries, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFs_Exists_NotFound_Bad(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
assert.False(t, c.Fs().Exists("/nonexistent/path/xyz"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fs path/validatePath edge cases ---
|
||||||
|
|
||||||
|
func TestFs_Read_EmptyPath_Ugly(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
r := c.Fs().Read("")
|
||||||
|
assert.False(t, r.OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFs_Write_EmptyPath_Ugly(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
r := c.Fs().Write("", "data")
|
||||||
|
assert.False(t, r.OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFs_Delete_Protected_Ugly(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
r := c.Fs().Delete("/")
|
||||||
|
assert.False(t, r.OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFs_DeleteAll_Protected_Ugly(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
r := c.Fs().DeleteAll("/")
|
||||||
|
assert.False(t, r.OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFs_ReadStream_WriteStream_Good(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
c := New()
|
||||||
|
path := filepath.Join(dir, "stream.txt")
|
||||||
|
c.Fs().Write(path, "streamed")
|
||||||
|
|
||||||
|
r := c.Fs().ReadStream(path)
|
||||||
|
assert.True(t, r.OK)
|
||||||
|
|
||||||
|
w := c.Fs().WriteStream(path)
|
||||||
|
assert.True(t, w.OK)
|
||||||
|
}
|
||||||
|
|
@ -142,6 +142,24 @@ func TestLogPanic_Recover_Good(t *testing.T) {
|
||||||
func TestLog_SetOutput_Good(t *testing.T) {
|
func TestLog_SetOutput_Good(t *testing.T) {
|
||||||
l := NewLog(LogOptions{Level: LevelInfo})
|
l := NewLog(LogOptions{Level: LevelInfo})
|
||||||
l.SetOutput(os.Stderr)
|
l.SetOutput(os.Stderr)
|
||||||
// Should not panic — just changes where logs go
|
|
||||||
l.Info("redirected")
|
l.Info("redirected")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Log suppression by level ---
|
||||||
|
|
||||||
|
func TestLog_Quiet_Suppresses_Ugly(t *testing.T) {
|
||||||
|
l := NewLog(LogOptions{Level: LevelQuiet})
|
||||||
|
// These should not panic even though nothing is logged
|
||||||
|
l.Debug("suppressed")
|
||||||
|
l.Info("suppressed")
|
||||||
|
l.Warn("suppressed")
|
||||||
|
l.Error("suppressed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLog_ErrorLevel_Suppresses_Ugly(t *testing.T) {
|
||||||
|
l := NewLog(LogOptions{Level: LevelError})
|
||||||
|
l.Debug("suppressed") // below threshold
|
||||||
|
l.Info("suppressed") // below threshold
|
||||||
|
l.Warn("suppressed") // below threshold
|
||||||
|
l.Error("visible") // at threshold
|
||||||
|
}
|
||||||
112
path_test.go
Normal file
112
path_test.go
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
package core_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
core "dappco.re/go/core"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPath_Relative(t *testing.T) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
require.NoError(t, err)
|
||||||
|
ds := core.Env("DS")
|
||||||
|
assert.Equal(t, home+ds+"Code"+ds+".core", core.Path("Code", ".core"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPath_Absolute(t *testing.T) {
|
||||||
|
ds := core.Env("DS")
|
||||||
|
assert.Equal(t, "/tmp"+ds+"workspace", core.Path("/tmp", "workspace"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPath_Empty(t *testing.T) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, home, core.Path())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPath_Cleans(t *testing.T) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, home+core.Env("DS")+"Code", core.Path("Code", "sub", ".."))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPath_CleanDoubleSlash(t *testing.T) {
|
||||||
|
ds := core.Env("DS")
|
||||||
|
assert.Equal(t, ds+"tmp"+ds+"file", core.Path("/tmp//file"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathBase(t *testing.T) {
|
||||||
|
assert.Equal(t, "core", core.PathBase("/Users/snider/Code/core"))
|
||||||
|
assert.Equal(t, "homelab", core.PathBase("deploy/to/homelab"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathBase_Root(t *testing.T) {
|
||||||
|
assert.Equal(t, "/", core.PathBase("/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathBase_Empty(t *testing.T) {
|
||||||
|
assert.Equal(t, ".", core.PathBase(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathDir(t *testing.T) {
|
||||||
|
assert.Equal(t, "/Users/snider/Code", core.PathDir("/Users/snider/Code/core"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathDir_Root(t *testing.T) {
|
||||||
|
assert.Equal(t, "/", core.PathDir("/file"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathDir_NoDir(t *testing.T) {
|
||||||
|
assert.Equal(t, ".", core.PathDir("file.go"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathExt(t *testing.T) {
|
||||||
|
assert.Equal(t, ".go", core.PathExt("main.go"))
|
||||||
|
assert.Equal(t, "", core.PathExt("Makefile"))
|
||||||
|
assert.Equal(t, ".gz", core.PathExt("archive.tar.gz"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPath_EnvConsistency(t *testing.T) {
|
||||||
|
assert.Equal(t, core.Env("DIR_HOME"), core.Path())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathGlob_Good(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(dir, "b.txt"), []byte("b"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(dir, "c.log"), []byte("c"), 0644)
|
||||||
|
|
||||||
|
matches := core.PathGlob(filepath.Join(dir, "*.txt"))
|
||||||
|
assert.Len(t, matches, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathGlob_NoMatch(t *testing.T) {
|
||||||
|
matches := core.PathGlob("/nonexistent/pattern-*.xyz")
|
||||||
|
assert.Empty(t, matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathIsAbs_Good(t *testing.T) {
|
||||||
|
assert.True(t, core.PathIsAbs("/tmp"))
|
||||||
|
assert.True(t, core.PathIsAbs("/"))
|
||||||
|
assert.False(t, core.PathIsAbs("relative"))
|
||||||
|
assert.False(t, core.PathIsAbs(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanPath_Good(t *testing.T) {
|
||||||
|
assert.Equal(t, "/a/b", core.CleanPath("/a//b", "/"))
|
||||||
|
assert.Equal(t, "/a/c", core.CleanPath("/a/b/../c", "/"))
|
||||||
|
assert.Equal(t, "/", core.CleanPath("/", "/"))
|
||||||
|
assert.Equal(t, ".", core.CleanPath("", "/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathDir_TrailingSlash(t *testing.T) {
|
||||||
|
// Trailing slash is stripped, then dir of /Users/snider/Code = /Users/snider
|
||||||
|
result := core.PathDir("/Users/snider/Code/")
|
||||||
|
assert.Equal(t, "/Users/snider/Code", result)
|
||||||
|
}
|
||||||
|
|
@ -74,3 +74,48 @@ func TestRuntime_Lifecycle_Good(t *testing.T) {
|
||||||
assert.True(t, result.OK)
|
assert.True(t, result.OK)
|
||||||
assert.True(t, started)
|
assert.True(t, started)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRuntime_ServiceShutdown_Good(t *testing.T) {
|
||||||
|
stopped := false
|
||||||
|
r := NewWithFactories(nil, map[string]ServiceFactory{
|
||||||
|
"test": func() Result {
|
||||||
|
return Result{Value: Service{
|
||||||
|
OnStart: func() Result { return Result{OK: true} },
|
||||||
|
OnStop: func() Result { stopped = true; return Result{OK: true} },
|
||||||
|
}, OK: true}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.True(t, r.OK)
|
||||||
|
rt := r.Value.(*Runtime)
|
||||||
|
|
||||||
|
rt.ServiceStartup(context.Background(), nil)
|
||||||
|
result := rt.ServiceShutdown(context.Background())
|
||||||
|
assert.True(t, result.OK)
|
||||||
|
assert.True(t, stopped)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuntime_ServiceShutdown_NilCore_Good(t *testing.T) {
|
||||||
|
rt := &Runtime{}
|
||||||
|
result := rt.ServiceShutdown(context.Background())
|
||||||
|
assert.True(t, result.OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCore_ServiceShutdown_Good(t *testing.T) {
|
||||||
|
stopped := false
|
||||||
|
c := New()
|
||||||
|
c.Service("test", Service{
|
||||||
|
OnStart: func() Result { return Result{OK: true} },
|
||||||
|
OnStop: func() Result { stopped = true; return Result{OK: true} },
|
||||||
|
})
|
||||||
|
c.ServiceStartup(context.Background(), nil)
|
||||||
|
result := c.ServiceShutdown(context.Background())
|
||||||
|
assert.True(t, result.OK)
|
||||||
|
assert.True(t, stopped)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCore_Context_Good(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
c.ServiceStartup(context.Background(), nil)
|
||||||
|
assert.NotNil(t, c.Context())
|
||||||
|
c.ServiceShutdown(context.Background())
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package core_test
|
package core_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -46,6 +47,61 @@ func TestPerformAsync_Progress_Good(t *testing.T) {
|
||||||
c.Progress(taskID, 0.5, "halfway", "work")
|
c.Progress(taskID, 0.5, "halfway", "work")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPerformAsync_Completion_Good(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
completed := make(chan ActionTaskCompleted, 1)
|
||||||
|
|
||||||
|
c.RegisterTask(func(_ *Core, task Task) Result {
|
||||||
|
return Result{Value: "result", OK: true}
|
||||||
|
})
|
||||||
|
c.RegisterAction(func(_ *Core, msg Message) Result {
|
||||||
|
if evt, ok := msg.(ActionTaskCompleted); ok {
|
||||||
|
completed <- evt
|
||||||
|
}
|
||||||
|
return Result{OK: true}
|
||||||
|
})
|
||||||
|
|
||||||
|
c.PerformAsync("work")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case evt := <-completed:
|
||||||
|
assert.Nil(t, evt.Error)
|
||||||
|
assert.Equal(t, "result", evt.Result)
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for completion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPerformAsync_NoHandler_Good(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
completed := make(chan ActionTaskCompleted, 1)
|
||||||
|
|
||||||
|
c.RegisterAction(func(_ *Core, msg Message) Result {
|
||||||
|
if evt, ok := msg.(ActionTaskCompleted); ok {
|
||||||
|
completed <- evt
|
||||||
|
}
|
||||||
|
return Result{OK: true}
|
||||||
|
})
|
||||||
|
|
||||||
|
c.PerformAsync("unhandled")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case evt := <-completed:
|
||||||
|
assert.NotNil(t, evt.Error)
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPerformAsync_AfterShutdown_Bad(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
c.ServiceStartup(context.Background(), nil)
|
||||||
|
c.ServiceShutdown(context.Background())
|
||||||
|
|
||||||
|
r := c.PerformAsync("should not run")
|
||||||
|
assert.False(t, r.OK)
|
||||||
|
}
|
||||||
|
|
||||||
// --- RegisterAction + RegisterActions ---
|
// --- RegisterAction + RegisterActions ---
|
||||||
|
|
||||||
func TestRegisterAction_Good(t *testing.T) {
|
func TestRegisterAction_Good(t *testing.T) {
|
||||||
0
tests/testdata/test.txt → testdata/test.txt
vendored
0
tests/testdata/test.txt → testdata/test.txt
vendored
Loading…
Add table
Reference in a new issue