Files
osmedeus/internal/runner/runner_test.go
T
j3ssie b6e9d12324 feat: implement path-friendly loop variables and improve asset merge logic
- Add auto-generated _<variable>_ path-friendly variables for foreach loops that sanitize unsafe filesystem characters (/, :, etc.) and deterministically truncate long values
- Refactor asset and vulnerability import/merge logic to preserve existing non-empty fields instead of full overwrite on conflict
- Add mergeAssetFields() and mergeVulnFields() helper functions for consistent field-level merge behavior across all import methods
- Add comprehensive unit tests for merge functions and path-friendly variable behavior in foreach loops
- Add E2E test module (test-foreach-path-friendly) validating sanitization, directory creation, truncation, and variable coexistence
2026-02-13 13:27:04 +07:00

303 lines
7.8 KiB
Go

package runner
import (
"context"
"net"
"testing"
"time"
"github.com/j3ssie/osmedeus/v5/internal/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHostRunner_Execute(t *testing.T) {
ctx := context.Background()
runner := NewHostRunner("")
err := runner.Setup(ctx)
require.NoError(t, err)
defer func() { _ = runner.Cleanup(ctx) }()
result, err := runner.Execute(ctx, "echo hello")
require.NoError(t, err)
assert.Equal(t, 0, result.ExitCode)
assert.Contains(t, result.Output, "hello")
}
func TestHostRunner_Type(t *testing.T) {
runner := NewHostRunner("")
assert.Equal(t, core.RunnerTypeHost, runner.Type())
assert.False(t, runner.IsRemote())
}
func TestHostRunner_ExitCode(t *testing.T) {
ctx := context.Background()
runner := NewHostRunner("")
result, err := runner.Execute(ctx, "exit 1")
require.NoError(t, err)
assert.Equal(t, 1, result.ExitCode)
}
func TestHostRunner_WithBinariesPath(t *testing.T) {
ctx := context.Background()
runner := NewHostRunner("/tmp/test-binaries")
result, err := runner.Execute(ctx, "echo $PATH")
require.NoError(t, err)
assert.Equal(t, 0, result.ExitCode)
assert.Contains(t, result.Output, "/tmp/test-binaries")
}
// Integration test - requires Docker
func TestDockerRunner_Execute_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
config := &core.RunnerConfig{
Image: "alpine:latest",
Persistent: false,
}
runner, err := NewDockerRunner(config, "")
require.NoError(t, err)
err = runner.Setup(ctx)
require.NoError(t, err)
defer func() { _ = runner.Cleanup(ctx) }()
result, err := runner.Execute(ctx, "echo hello from docker")
require.NoError(t, err)
assert.Equal(t, 0, result.ExitCode)
assert.Contains(t, result.Output, "hello from docker")
}
func TestDockerRunner_Type(t *testing.T) {
config := &core.RunnerConfig{
Image: "alpine:latest",
}
runner, err := NewDockerRunner(config, "")
require.NoError(t, err)
assert.Equal(t, core.RunnerTypeDocker, runner.Type())
assert.True(t, runner.IsRemote())
}
func TestDockerRunner_RequiresImage(t *testing.T) {
config := &core.RunnerConfig{}
_, err := NewDockerRunner(config, "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "image")
}
// Integration test - requires SSH server (linuxserver/openssh-server)
func TestSSHRunner_Execute_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
conn, err := net.DialTimeout("tcp", "localhost:2222", 500*time.Millisecond)
if err != nil {
t.Skip("skipping integration test: SSH server not available on localhost:2222")
}
_ = conn.Close()
ctx := context.Background()
config := &core.RunnerConfig{
Host: "localhost",
Port: 2222,
User: "testuser",
Password: "testpass",
}
runner, err := NewSSHRunner(config, "")
require.NoError(t, err)
err = runner.Setup(ctx)
require.NoError(t, err)
defer func() { _ = runner.Cleanup(ctx) }()
result, err := runner.Execute(ctx, "echo hello from ssh")
require.NoError(t, err)
assert.Equal(t, 0, result.ExitCode)
assert.Contains(t, result.Output, "hello from ssh")
}
func TestSSHRunner_Type(t *testing.T) {
config := &core.RunnerConfig{
Host: "localhost",
User: "test",
}
runner, err := NewSSHRunner(config, "")
require.NoError(t, err)
assert.Equal(t, core.RunnerTypeSSH, runner.Type())
assert.True(t, runner.IsRemote())
}
func TestSSHRunner_RequiresHost(t *testing.T) {
config := &core.RunnerConfig{
User: "test",
}
_, err := NewSSHRunner(config, "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "host")
}
func TestSSHRunner_RequiresUser(t *testing.T) {
config := &core.RunnerConfig{
Host: "localhost",
}
_, err := NewSSHRunner(config, "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "user")
}
func TestNewRunner_Host(t *testing.T) {
workflow := &core.Workflow{
Name: "test",
Kind: core.KindModule,
Runner: core.RunnerTypeHost,
}
runner, err := NewRunner(workflow, "")
require.NoError(t, err)
assert.Equal(t, core.RunnerTypeHost, runner.Type())
}
func TestNewRunner_DefaultsToHost(t *testing.T) {
workflow := &core.Workflow{
Name: "test",
Kind: core.KindModule,
}
runner, err := NewRunner(workflow, "")
require.NoError(t, err)
assert.Equal(t, core.RunnerTypeHost, runner.Type())
}
// ============================================================================
// LimitedBuffer tests
// ============================================================================
func TestLimitedBuffer_WritesUnderLimit(t *testing.T) {
buf := NewLimitedBuffer(100)
n, err := buf.Write([]byte("hello"))
require.NoError(t, err)
assert.Equal(t, 5, n)
assert.Equal(t, 5, buf.Len())
assert.False(t, buf.Overflow())
assert.Equal(t, "hello", string(buf.Bytes()))
}
func TestLimitedBuffer_TruncatesAtLimit(t *testing.T) {
buf := NewLimitedBuffer(10)
n, err := buf.Write([]byte("hello world!")) // 12 bytes > 10 limit
require.NoError(t, err)
assert.Equal(t, 12, n) // reports full length written
assert.Equal(t, 10, buf.Len())
assert.True(t, buf.Overflow())
assert.Equal(t, "hello worl", string(buf.Bytes()))
}
func TestLimitedBuffer_DiscardsAfterFull(t *testing.T) {
buf := NewLimitedBuffer(5)
_, _ = buf.Write([]byte("hello"))
assert.Equal(t, 5, buf.Len())
assert.False(t, buf.Overflow())
// Further writes are silently discarded
n, err := buf.Write([]byte(" world"))
require.NoError(t, err)
assert.Equal(t, 6, n) // reports full length
assert.Equal(t, 5, buf.Len())
assert.True(t, buf.Overflow())
assert.Equal(t, "hello", string(buf.Bytes()))
}
func TestLimitedBuffer_MultipleWrites(t *testing.T) {
buf := NewLimitedBuffer(10)
_, _ = buf.Write([]byte("aaa")) // 3 bytes, total 3
_, _ = buf.Write([]byte("bbb")) // 3 bytes, total 6
_, _ = buf.Write([]byte("ccc")) // 3 bytes, total 9
_, _ = buf.Write([]byte("dddd")) // 4 bytes, only 1 fits -> total 10
assert.Equal(t, 10, buf.Len())
assert.True(t, buf.Overflow())
assert.Equal(t, "aaabbbcccd", string(buf.Bytes()))
}
func TestLimitedBuffer_ZeroSize(t *testing.T) {
buf := NewLimitedBuffer(0)
n, err := buf.Write([]byte("anything"))
require.NoError(t, err)
assert.Equal(t, 8, n)
assert.Equal(t, 0, buf.Len())
assert.True(t, buf.Overflow())
}
func TestCombineOutput_Normal(t *testing.T) {
stdout := NewLimitedBuffer(100)
stderr := NewLimitedBuffer(100)
_, _ = stdout.Write([]byte("out"))
_, _ = stderr.Write([]byte("err"))
result := combineOutput(stdout, stderr)
assert.Equal(t, "outerr", result)
}
func TestCombineOutput_Empty(t *testing.T) {
stdout := NewLimitedBuffer(100)
stderr := NewLimitedBuffer(100)
result := combineOutput(stdout, stderr)
assert.Equal(t, "", result)
}
func TestCombineOutput_Truncated(t *testing.T) {
stdout := NewLimitedBuffer(5)
stderr := NewLimitedBuffer(5)
_, _ = stdout.Write([]byte("long output that overflows"))
_, _ = stderr.Write([]byte("err"))
result := combineOutput(stdout, stderr)
assert.True(t, stdout.Overflow())
assert.Contains(t, result, "[output truncated]")
}
func TestHostRunner_LargeOutput_Bounded(t *testing.T) {
ctx := context.Background()
runner := NewHostRunner("")
// Generate output larger than MaxOutputSize limit
// Use printf to generate ~20KB of output (well under limit, but proves buffer works)
result, err := runner.Execute(ctx, "printf '%0.s-' {1..20000}")
require.NoError(t, err)
assert.Equal(t, 0, result.ExitCode)
assert.True(t, len(result.Output) > 0)
assert.True(t, len(result.Output) <= MaxOutputSize+20) // +20 for "[output truncated]\n"
// Verify that very large output gets truncated
// Generate output of ~11MB (> 10MB limit)
bigResult, err := runner.Execute(ctx, "head -c 11000000 /dev/zero | tr '\\0' 'A'")
require.NoError(t, err)
assert.True(t, len(bigResult.Output) <= MaxOutputSize+20)
if len(bigResult.Output) > MaxOutputSize {
assert.Contains(t, bigResult.Output, "[output truncated]")
}
}