Files
osmedeus/internal/executor/cmd_fallback_test.go
T
j3ssie baac7a016a feat: add worker management, hooks support, and db cleanup enhancements
- Add worker eval command for distributed function execution with Redis hooks registration
- Add worker set command to update worker fields (alias, public-ip, ssh-enabled, ssh-keys-path)
- Enhance worker status with JSON output, search filtering, and column selection (--columns, --exclude-columns, --search)
- Add --keep-setting flag to install base/validate commands to preserve osm-settings.yaml after base installation
- Fix binary installation in Nix: replace CopyInstalledBinaryToFolder with SymlinkInstalledBinaryToFolder
- Add --clean-ws flag to db clean command for removing workspace data
- Add HooksEnabled field to Run records when creating runs from CLI and API
- Add comprehensive test coverage for hook execution (pre/post hooks, execution order, failure handling)
- Add test coverage for worker commands (eval, set, status with JSON) and db clean operations
- Improve usage documentation for worker subcommands and db operations
2026-02-15 10:47:44 +07:00

513 lines
14 KiB
Go

package executor
import (
"context"
"errors"
"testing"
"time"
"github.com/j3ssie/osmedeus/v5/internal/core"
"github.com/j3ssie/osmedeus/v5/internal/runner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- stripTimeoutPrefix tests ---
func TestStripTimeoutPrefix_BasicTimeout(t *testing.T) {
r := stripTimeoutPrefix("timeout 30 nuclei -t templates")
assert.True(t, r.stripped)
assert.Equal(t, "nuclei -t templates", r.command)
assert.Equal(t, 30*time.Second, r.duration)
}
func TestStripTimeoutPrefix_WithKillAfterFlag(t *testing.T) {
r := stripTimeoutPrefix("timeout -k 10 30 nuclei -t templates")
assert.True(t, r.stripped)
assert.Equal(t, "nuclei -t templates", r.command)
assert.Equal(t, 30*time.Second, r.duration)
}
func TestStripTimeoutPrefix_WithKillAfterEquals(t *testing.T) {
r := stripTimeoutPrefix("timeout --kill-after=10s 30 nuclei -t templates")
assert.True(t, r.stripped)
assert.Equal(t, "nuclei -t templates", r.command)
}
func TestStripTimeoutPrefix_WithSignalFlag(t *testing.T) {
r := stripTimeoutPrefix("timeout -s SIGKILL 30 nuclei -t templates")
assert.True(t, r.stripped)
assert.Equal(t, "nuclei -t templates", r.command)
}
func TestStripTimeoutPrefix_WithSignalEquals(t *testing.T) {
r := stripTimeoutPrefix("timeout --signal=TERM 30 nuclei -t templates")
assert.True(t, r.stripped)
assert.Equal(t, "nuclei -t templates", r.command)
}
func TestStripTimeoutPrefix_WithForeground(t *testing.T) {
r := stripTimeoutPrefix("timeout --foreground 30 nuclei -t templates")
assert.True(t, r.stripped)
assert.Equal(t, "nuclei -t templates", r.command)
}
func TestStripTimeoutPrefix_WithPreserveStatus(t *testing.T) {
r := stripTimeoutPrefix("timeout --preserve-status 30 nuclei -t templates")
assert.True(t, r.stripped)
assert.Equal(t, "nuclei -t templates", r.command)
}
func TestStripTimeoutPrefix_WithVerbose(t *testing.T) {
r := stripTimeoutPrefix("timeout -v 30 nuclei -t templates")
assert.True(t, r.stripped)
assert.Equal(t, "nuclei -t templates", r.command)
}
func TestStripTimeoutPrefix_MultipleFlags(t *testing.T) {
r := stripTimeoutPrefix("timeout -k 10 -s SIGKILL --foreground 30 nuclei -t templates")
assert.True(t, r.stripped)
assert.Equal(t, "nuclei -t templates", r.command)
}
func TestStripTimeoutPrefix_ShortKillAttached(t *testing.T) {
r := stripTimeoutPrefix("timeout -k5s 30 nuclei -t templates")
assert.True(t, r.stripped)
assert.Equal(t, "nuclei -t templates", r.command)
}
func TestStripTimeoutPrefix_NoCommandAfterDuration(t *testing.T) {
r := stripTimeoutPrefix("timeout 30")
assert.True(t, r.stripped)
assert.Equal(t, "", r.command)
assert.Equal(t, 30*time.Second, r.duration)
}
func TestStripTimeoutPrefix_NotTimeout(t *testing.T) {
r := stripTimeoutPrefix("nuclei -t templates")
assert.False(t, r.stripped)
assert.Equal(t, "nuclei -t templates", r.command)
}
func TestStripTimeoutPrefix_EmptyString(t *testing.T) {
r := stripTimeoutPrefix("")
assert.False(t, r.stripped)
assert.Equal(t, "", r.command)
}
func TestStripTimeoutPrefix_FullPathTimeout(t *testing.T) {
r := stripTimeoutPrefix("/usr/bin/timeout 30 nuclei -t templates")
assert.True(t, r.stripped)
assert.Equal(t, "nuclei -t templates", r.command)
assert.Equal(t, 30*time.Second, r.duration)
}
func TestStripTimeoutPrefix_DurationWithSuffix(t *testing.T) {
r := stripTimeoutPrefix("timeout 2h nuclei -t templates")
assert.True(t, r.stripped)
assert.Equal(t, "nuclei -t templates", r.command)
assert.Equal(t, 2*time.Hour, r.duration)
}
// --- parseTimeoutDuration tests ---
func TestParseTimeoutDuration_Seconds(t *testing.T) {
assert.Equal(t, 30*time.Second, parseTimeoutDuration("30"))
}
func TestParseTimeoutDuration_SecondsWithSuffix(t *testing.T) {
assert.Equal(t, 30*time.Second, parseTimeoutDuration("30s"))
}
func TestParseTimeoutDuration_Minutes(t *testing.T) {
assert.Equal(t, 5*time.Minute, parseTimeoutDuration("5m"))
}
func TestParseTimeoutDuration_Hours(t *testing.T) {
assert.Equal(t, 2*time.Hour, parseTimeoutDuration("2h"))
}
func TestParseTimeoutDuration_Days(t *testing.T) {
assert.Equal(t, 24*time.Hour, parseTimeoutDuration("1d"))
}
func TestParseTimeoutDuration_Float(t *testing.T) {
assert.Equal(t, time.Duration(1.5*float64(time.Second)), parseTimeoutDuration("1.5"))
}
func TestParseTimeoutDuration_FloatWithSuffix(t *testing.T) {
assert.Equal(t, time.Duration(2.5*float64(time.Minute)), parseTimeoutDuration("2.5m"))
}
func TestParseTimeoutDuration_Empty(t *testing.T) {
assert.Equal(t, time.Duration(0), parseTimeoutDuration(""))
}
func TestParseTimeoutDuration_Invalid(t *testing.T) {
assert.Equal(t, time.Duration(0), parseTimeoutDuration("abc"))
}
// --- prependBinariesPath tests ---
func TestPrependBinariesPath_Normal(t *testing.T) {
cmd, ok := prependBinariesPath("nuclei -t templates", "/opt/tools")
assert.True(t, ok)
assert.Equal(t, "/opt/tools/nuclei -t templates", cmd)
}
func TestPrependBinariesPath_AlreadyHasPath(t *testing.T) {
cmd, ok := prependBinariesPath("/usr/bin/nuclei -t templates", "/opt/tools")
assert.False(t, ok)
assert.Equal(t, "/usr/bin/nuclei -t templates", cmd)
}
func TestPrependBinariesPath_RelativePath(t *testing.T) {
cmd, ok := prependBinariesPath("./nuclei -t templates", "/opt/tools")
assert.False(t, ok)
assert.Equal(t, "./nuclei -t templates", cmd)
}
func TestPrependBinariesPath_EmptyBinariesPath(t *testing.T) {
cmd, ok := prependBinariesPath("nuclei -t templates", "")
assert.False(t, ok)
assert.Equal(t, "nuclei -t templates", cmd)
}
func TestPrependBinariesPath_EmptyCommand(t *testing.T) {
cmd, ok := prependBinariesPath("", "/opt/tools")
assert.False(t, ok)
assert.Equal(t, "", cmd)
}
func TestPrependBinariesPath_TrailingSlash(t *testing.T) {
cmd, ok := prependBinariesPath("nuclei -t templates", "/opt/tools/")
assert.True(t, ok)
assert.Equal(t, "/opt/tools/nuclei -t templates", cmd)
}
func TestPrependBinariesPath_SingleWord(t *testing.T) {
cmd, ok := prependBinariesPath("nuclei", "/opt/tools")
assert.True(t, ok)
assert.Equal(t, "/opt/tools/nuclei", cmd)
}
// --- exitCodeError tests ---
func TestExitCodeError_ErrorMessage(t *testing.T) {
err := newExitCodeErrorf(127, "command not found: %s", "nuclei")
assert.Equal(t, "command not found: nuclei", err.Error())
assert.Equal(t, 127, err.code)
}
func TestExitCodeError_ErrorsAs(t *testing.T) {
err := newExitCodeErrorf(127, "command not found")
var ecErr *exitCodeError
require.True(t, errors.As(err, &ecErr))
assert.Equal(t, 127, ecErr.code)
}
func TestExitCodeError_NotExitCodeError(t *testing.T) {
err := errors.New("some other error")
var ecErr *exitCodeError
assert.False(t, errors.As(err, &ecErr))
}
func TestNewExitCodeError(t *testing.T) {
err := newExitCodeError(1, "failed")
assert.Equal(t, 1, err.code)
assert.Equal(t, "failed", err.Error())
}
// --- mockRunner for integration tests ---
type mockFallbackRunner struct {
// responses maps command strings to their mock results
responses map[string]*mockResponse
}
type mockResponse struct {
output string
exitCode int
}
func newMockFallbackRunner() *mockFallbackRunner {
return &mockFallbackRunner{
responses: make(map[string]*mockResponse),
}
}
func (m *mockFallbackRunner) addResponse(command string, output string, exitCode int) {
m.responses[command] = &mockResponse{output: output, exitCode: exitCode}
}
func (m *mockFallbackRunner) Execute(_ context.Context, command string) (*runner.CommandResult, error) {
if resp, ok := m.responses[command]; ok {
return &runner.CommandResult{
Output: resp.output,
ExitCode: resp.exitCode,
}, nil
}
// Default: command not found
return &runner.CommandResult{
Output: "sh: command not found",
ExitCode: 127,
}, nil
}
func (m *mockFallbackRunner) Setup(_ context.Context) error { return nil }
func (m *mockFallbackRunner) Cleanup(_ context.Context) error { return nil }
func (m *mockFallbackRunner) Type() core.RunnerType { return core.RunnerTypeHost }
func (m *mockFallbackRunner) IsRemote() bool { return false }
func (m *mockFallbackRunner) SetPIDCallbacks(_, _ runner.PIDCallback) {}
func (m *mockFallbackRunner) CopyFromRemote(_ context.Context, _, _ string) error {
return nil
}
// --- Integration tests: BashExecutor fallback chain ---
func TestBashExecutor_Fallback_StripTimeout(t *testing.T) {
mock := newMockFallbackRunner()
// Original command with timeout fails (127)
// Stripped command succeeds
mock.addResponse("nuclei -t templates", "found 5 results", 0)
exec := NewBashExecutor(nil)
exec.SetRunner(mock)
output, err := exec.executeCommandWithFallback(
context.Background(),
"timeout 30 nuclei -t templates",
0,
"",
)
assert.NoError(t, err)
assert.Equal(t, "found 5 results", output)
}
func TestBashExecutor_Fallback_PrependBinariesPath(t *testing.T) {
mock := newMockFallbackRunner()
// Original command fails (127)
// Prepended command succeeds
mock.addResponse("/opt/tools/nuclei -t templates", "found 3 results", 0)
exec := NewBashExecutor(nil)
exec.SetRunner(mock)
output, err := exec.executeCommandWithFallback(
context.Background(),
"nuclei -t templates",
0,
"/opt/tools",
)
assert.NoError(t, err)
assert.Equal(t, "found 3 results", output)
}
func TestBashExecutor_Fallback_StripTimeoutThenPrependBinaries(t *testing.T) {
mock := newMockFallbackRunner()
// Original "timeout 30 nuclei -t templates" → 127
// Stripped "nuclei -t templates" → still 127
// Prepended "/opt/tools/nuclei -t templates" → success
mock.addResponse("/opt/tools/nuclei -t templates", "found 7 results", 0)
exec := NewBashExecutor(nil)
exec.SetRunner(mock)
output, err := exec.executeCommandWithFallback(
context.Background(),
"timeout 30 nuclei -t templates",
0,
"/opt/tools",
)
assert.NoError(t, err)
assert.Equal(t, "found 7 results", output)
}
func TestBashExecutor_Fallback_NoFallbackOnNon127(t *testing.T) {
mock := newMockFallbackRunner()
// Command fails with exit code 1 (not 127)
mock.addResponse("nuclei -t templates", "error output", 1)
exec := NewBashExecutor(nil)
exec.SetRunner(mock)
_, err := exec.executeCommandWithFallback(
context.Background(),
"nuclei -t templates",
0,
"/opt/tools",
)
require.Error(t, err)
var ecErr *exitCodeError
require.True(t, errors.As(err, &ecErr))
assert.Equal(t, 1, ecErr.code)
}
func TestBashExecutor_Fallback_SuccessNoFallback(t *testing.T) {
mock := newMockFallbackRunner()
// Command succeeds on first try
mock.addResponse("nuclei -t templates", "all good", 0)
exec := NewBashExecutor(nil)
exec.SetRunner(mock)
output, err := exec.executeCommandWithFallback(
context.Background(),
"nuclei -t templates",
0,
"/opt/tools",
)
assert.NoError(t, err)
assert.Equal(t, "all good", output)
}
func TestBashExecutor_Fallback_AllFallbacksFail(t *testing.T) {
mock := newMockFallbackRunner()
// All commands return 127 (everything is command not found)
exec := NewBashExecutor(nil)
exec.SetRunner(mock)
_, err := exec.executeCommandWithFallback(
context.Background(),
"timeout 30 nuclei -t templates",
0,
"/opt/tools",
)
require.Error(t, err)
var ecErr *exitCodeError
require.True(t, errors.As(err, &ecErr))
assert.Equal(t, 127, ecErr.code)
}
func TestBashExecutor_Fallback_CancelledContextNoRetry(t *testing.T) {
mock := newMockFallbackRunner()
// All responses return 127
// But we expect no fallback because context is cancelled
exec := NewBashExecutor(nil)
exec.SetRunner(mock)
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately
_, err := exec.executeCommandWithFallback(
ctx,
"timeout 30 nuclei -t templates",
0,
"/opt/tools",
)
// Should fail due to context cancellation, not 127 fallback
require.Error(t, err)
}
func TestBashExecutor_Fallback_TimeoutFlagsWithEquals(t *testing.T) {
mock := newMockFallbackRunner()
// timeout with --kill-after= and --signal= flags
mock.addResponse("nuclei -t templates", "found results", 0)
exec := NewBashExecutor(nil)
exec.SetRunner(mock)
output, err := exec.executeCommandWithFallback(
context.Background(),
"timeout --kill-after=10s --signal=TERM 30 nuclei -t templates",
0,
"",
)
assert.NoError(t, err)
assert.Equal(t, "found results", output)
}
func TestBashExecutor_Fallback_BinaryWithPathNoFallback2(t *testing.T) {
mock := newMockFallbackRunner()
// Binary already has a path prefix — fallback 2 (prepend) should not apply
// Fallback 1 (strip timeout) should work
mock.addResponse("/usr/local/bin/nuclei -t templates", "found", 0)
exec := NewBashExecutor(nil)
exec.SetRunner(mock)
output, err := exec.executeCommandWithFallback(
context.Background(),
"timeout 30 /usr/local/bin/nuclei -t templates",
0,
"/opt/tools",
)
assert.NoError(t, err)
assert.Equal(t, "found", output)
}
func TestBashExecutor_Fallback_SequentialUsesWithFallback(t *testing.T) {
mock := newMockFallbackRunner()
// First command needs fallback (strip timeout)
mock.addResponse("nuclei -t templates", "result1", 0)
// Second command succeeds directly
mock.addResponse("echo done", "done", 0)
exec := NewBashExecutor(nil)
exec.SetRunner(mock)
output, err := exec.executeSequential(
context.Background(),
[]string{"timeout 30 nuclei -t templates", "echo done"},
0,
"",
)
assert.NoError(t, err)
assert.Contains(t, output, "result1")
assert.Contains(t, output, "done")
}
func TestBashExecutor_Fallback_ParallelUsesWithFallback(t *testing.T) {
mock := newMockFallbackRunner()
mock.addResponse("nuclei -t templates", "result1", 0)
mock.addResponse("echo done", "done", 0)
exec := NewBashExecutor(nil)
exec.SetRunner(mock)
output, err := exec.executeParallel(
context.Background(),
[]string{"timeout 30 nuclei -t templates", "echo done"},
0,
"",
)
assert.NoError(t, err)
assert.Contains(t, output, "result1")
assert.Contains(t, output, "done")
}
func TestBashExecutor_Fallback_StripTimeoutNonZeroExitAfterStrip(t *testing.T) {
mock := newMockFallbackRunner()
// After stripping timeout, command fails with non-127 exit code
mock.addResponse("nuclei -t templates", "some error", 2)
exec := NewBashExecutor(nil)
exec.SetRunner(mock)
_, err := exec.executeCommandWithFallback(
context.Background(),
"timeout 30 nuclei -t templates",
time.Duration(0),
"/opt/tools",
)
// Should return the exit code 2 error, not try fallback 2
require.Error(t, err)
var ecErr *exitCodeError
require.True(t, errors.As(err, &ecErr))
assert.Equal(t, 2, ecErr.code)
}