mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-09 14:28:01 -05:00
- Extract InputSentinel constant to shared constants.go file - Remove duplicate inputSentinel definitions from template.go and patterns.go - Create withTestExtension helper function to reduce test code duplication - Refactor 3 test functions to use the helper (reduces ~40 lines per test) - Fix shell script to use $@ instead of $* for proper argument quoting Addresses review comments from @ksylvan and @Copilot AI
276 lines
8.3 KiB
Go
276 lines
8.3 KiB
Go
package template
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// withTestExtension creates a temporary test extension and runs the test function
|
|
func withTestExtension(t *testing.T, name string, scriptContent string, testFunc func(*ExtensionManager, string)) {
|
|
t.Helper()
|
|
|
|
// Create a temporary directory for test extension
|
|
tmpDir := t.TempDir()
|
|
configDir := filepath.Join(tmpDir, ".config", "fabric")
|
|
extensionsDir := filepath.Join(configDir, "extensions")
|
|
binDir := filepath.Join(extensionsDir, "bin")
|
|
configsDir := filepath.Join(extensionsDir, "configs")
|
|
|
|
err := os.MkdirAll(binDir, 0755)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create bin directory: %v", err)
|
|
}
|
|
err = os.MkdirAll(configsDir, 0755)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create configs directory: %v", err)
|
|
}
|
|
|
|
// Create a test script
|
|
scriptPath := filepath.Join(binDir, name+".sh")
|
|
err = os.WriteFile(scriptPath, []byte(scriptContent), 0755)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test script: %v", err)
|
|
}
|
|
|
|
// Create extension config
|
|
configPath := filepath.Join(configsDir, name+".yaml")
|
|
configContent := fmt.Sprintf(`name: %s
|
|
executable: %s
|
|
type: executable
|
|
timeout: "5s"
|
|
description: "Test extension"
|
|
version: "1.0.0"
|
|
|
|
operations:
|
|
echo:
|
|
cmd_template: "{{executable}} {{value}}"
|
|
|
|
config:
|
|
output:
|
|
method: stdout
|
|
`, name, scriptPath)
|
|
err = os.WriteFile(configPath, []byte(configContent), 0644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create extension config: %v", err)
|
|
}
|
|
|
|
// Initialize extension manager with test config directory
|
|
mgr := NewExtensionManager(configDir)
|
|
|
|
// Register the test extension
|
|
err = mgr.RegisterExtension(configPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to register extension: %v", err)
|
|
}
|
|
|
|
// Run the test
|
|
testFunc(mgr, name)
|
|
}
|
|
|
|
// TestSentinelTokenReplacement tests the fix for the {{input}} sentinel token bug
|
|
// This test verifies that when {{input}} is used inside an extension call,
|
|
// the actual input is passed to the extension, not the sentinel token.
|
|
func TestSentinelTokenReplacement(t *testing.T) {
|
|
scriptContent := `#!/bin/bash
|
|
echo "RECEIVED: $@"
|
|
`
|
|
|
|
withTestExtension(t, "echo-test", scriptContent, func(mgr *ExtensionManager, name string) {
|
|
// Save and restore global extension manager
|
|
oldManager := extensionManager
|
|
defer func() { extensionManager = oldManager }()
|
|
extensionManager = mgr
|
|
|
|
tests := []struct {
|
|
name string
|
|
template string
|
|
input string
|
|
wantContain string
|
|
wantNotContain string
|
|
}{
|
|
{
|
|
name: "sentinel token with {{input}} in extension value",
|
|
template: "{{ext:echo-test:echo:__FABRIC_INPUT_SENTINEL_TOKEN__}}",
|
|
input: "test input data",
|
|
wantContain: "RECEIVED: test input data",
|
|
wantNotContain: "__FABRIC_INPUT_SENTINEL_TOKEN__",
|
|
},
|
|
{
|
|
name: "direct input variable replacement",
|
|
template: "{{ext:echo-test:echo:{{input}}}}",
|
|
input: "Hello World",
|
|
wantContain: "RECEIVED: Hello World",
|
|
wantNotContain: "{{input}}",
|
|
},
|
|
{
|
|
name: "sentinel with complex input",
|
|
template: "Result: {{ext:echo-test:echo:__FABRIC_INPUT_SENTINEL_TOKEN__}}",
|
|
input: "What is AI?",
|
|
wantContain: "RECEIVED: What is AI?",
|
|
wantNotContain: "__FABRIC_INPUT_SENTINEL_TOKEN__",
|
|
},
|
|
{
|
|
name: "multiple words in input",
|
|
template: "{{ext:echo-test:echo:{{input}}}}",
|
|
input: "Multiple word input string",
|
|
wantContain: "RECEIVED: Multiple word input string",
|
|
wantNotContain: "{{input}}",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := ApplyTemplate(tt.template, map[string]string{}, tt.input)
|
|
if err != nil {
|
|
t.Errorf("ApplyTemplate() error = %v", err)
|
|
return
|
|
}
|
|
|
|
// Check that result contains expected string
|
|
if !strings.Contains(got, tt.wantContain) {
|
|
t.Errorf("ApplyTemplate() = %q, should contain %q", got, tt.wantContain)
|
|
}
|
|
|
|
// Check that result does NOT contain unwanted string
|
|
if strings.Contains(got, tt.wantNotContain) {
|
|
t.Errorf("ApplyTemplate() = %q, should NOT contain %q", got, tt.wantNotContain)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestSentinelInVariableProcessing tests that the sentinel token is handled
|
|
// correctly in regular variable processing (not just extensions)
|
|
// Note: The sentinel is only replaced when it appears in extension values,
|
|
// not when used as a standalone variable (which would be a user error)
|
|
func TestSentinelInVariableProcessing(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
template string
|
|
vars map[string]string
|
|
input string
|
|
want string
|
|
}{
|
|
{
|
|
name: "input variable works normally",
|
|
template: "Value: {{input}}",
|
|
input: "actual input",
|
|
want: "Value: actual input",
|
|
},
|
|
{
|
|
name: "multiple input references",
|
|
template: "First: {{input}}, Second: {{input}}",
|
|
input: "test",
|
|
want: "First: test, Second: test",
|
|
},
|
|
{
|
|
name: "input with variables",
|
|
template: "Var: {{name}}, Input: {{input}}",
|
|
vars: map[string]string{"name": "TestVar"},
|
|
input: "input value",
|
|
want: "Var: TestVar, Input: input value",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := ApplyTemplate(tt.template, tt.vars, tt.input)
|
|
if err != nil {
|
|
t.Errorf("ApplyTemplate() error = %v", err)
|
|
return
|
|
}
|
|
|
|
if got != tt.want {
|
|
t.Errorf("ApplyTemplate() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExtensionValueWithSentinel specifically tests the extension value
|
|
// sentinel replacement logic
|
|
func TestExtensionValueWithSentinel(t *testing.T) {
|
|
scriptContent := `#!/bin/bash
|
|
# Output each argument on a separate line
|
|
for arg in "$@"; do
|
|
echo "ARG: $arg"
|
|
done
|
|
`
|
|
|
|
withTestExtension(t, "arg-test", scriptContent, func(mgr *ExtensionManager, name string) {
|
|
// Save and restore global extension manager
|
|
oldManager := extensionManager
|
|
defer func() { extensionManager = oldManager }()
|
|
extensionManager = mgr
|
|
|
|
// Test that sentinel token in extension value gets replaced
|
|
template := "{{ext:arg-test:echo:prefix-__FABRIC_INPUT_SENTINEL_TOKEN__-suffix}}"
|
|
input := "MYINPUT"
|
|
|
|
got, err := ApplyTemplate(template, map[string]string{}, input)
|
|
if err != nil {
|
|
t.Fatalf("ApplyTemplate() error = %v", err)
|
|
}
|
|
|
|
// The sentinel should be replaced with actual input
|
|
expectedContain := "ARG: prefix-MYINPUT-suffix"
|
|
if !strings.Contains(got, expectedContain) {
|
|
t.Errorf("ApplyTemplate() = %q, should contain %q", got, expectedContain)
|
|
}
|
|
|
|
// The sentinel token should NOT appear in output
|
|
if strings.Contains(got, "__FABRIC_INPUT_SENTINEL_TOKEN__") {
|
|
t.Errorf("ApplyTemplate() = %q, should NOT contain sentinel token", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestNestedInputInExtension tests the original bug case:
|
|
// {{ext:name:op:{{input}}}} should pass the actual input, not the sentinel
|
|
func TestNestedInputInExtension(t *testing.T) {
|
|
scriptContent := `#!/bin/bash
|
|
echo "NESTED_TEST: $*"
|
|
`
|
|
|
|
withTestExtension(t, "nested-test", scriptContent, func(mgr *ExtensionManager, name string) {
|
|
// Save and restore global extension manager
|
|
oldManager := extensionManager
|
|
defer func() { extensionManager = oldManager }()
|
|
extensionManager = mgr
|
|
|
|
// This is the bug case: {{input}} nested inside extension call
|
|
// The template processing should:
|
|
// 1. Replace {{input}} with sentinel during variable protection
|
|
// 2. Process the extension, replacing sentinel with actual input
|
|
// 3. Execute extension with actual input, not sentinel
|
|
|
|
template := "{{ext:nested-test:echo:{{input}}}}"
|
|
input := "What is Artificial Intelligence"
|
|
|
|
got, err := ApplyTemplate(template, map[string]string{}, input)
|
|
if err != nil {
|
|
t.Fatalf("ApplyTemplate() error = %v", err)
|
|
}
|
|
|
|
// Verify the actual input was passed, not the sentinel
|
|
expectedContain := "NESTED_TEST: What is Artificial Intelligence"
|
|
if !strings.Contains(got, expectedContain) {
|
|
t.Errorf("ApplyTemplate() = %q, should contain %q", got, expectedContain)
|
|
}
|
|
|
|
// Verify sentinel token does NOT appear
|
|
if strings.Contains(got, "__FABRIC_INPUT_SENTINEL_TOKEN__") {
|
|
t.Errorf("ApplyTemplate() output contains sentinel token (BUG NOT FIXED): %q", got)
|
|
}
|
|
|
|
// Verify {{input}} template tag does NOT appear
|
|
if strings.Contains(got, "{{input}}") {
|
|
t.Errorf("ApplyTemplate() output contains unresolved {{input}}: %q", got)
|
|
}
|
|
})
|
|
}
|