From 6fc75282e8802c16948376f06012e0daf4ccaa7f Mon Sep 17 00:00:00 2001 From: Matt Joyce Date: Tue, 3 Dec 2024 23:28:47 +1100 Subject: [PATCH] added tests for extension manager, registration and execution. --- plugins/template/extension_executor_test.go | 360 ++++++++++++++++++++ plugins/template/extension_manager_test.go | 184 ++++++++++ plugins/template/extension_registry.go | 41 +++ plugins/template/extension_registry_test.go | 75 ++++ 4 files changed, 660 insertions(+) create mode 100644 plugins/template/extension_executor_test.go create mode 100644 plugins/template/extension_manager_test.go create mode 100644 plugins/template/extension_registry_test.go diff --git a/plugins/template/extension_executor_test.go b/plugins/template/extension_executor_test.go new file mode 100644 index 00000000..72033979 --- /dev/null +++ b/plugins/template/extension_executor_test.go @@ -0,0 +1,360 @@ +package template + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestExtensionExecutor(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "fabric-ext-executor-*") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create test script that has both stdout and file output modes + testScript := filepath.Join(tmpDir, "test-script.sh") + scriptContent := `#!/bin/bash +case "$1" in + "stdout") + echo "Hello, $2!" + ;; + "file") + echo "Hello, $2!" > "$3" + echo "$3" # Print the filename for path_from_stdout + ;; + *) + echo "Unknown command" >&2 + exit 1 + ;; +esac` + + if err := os.WriteFile(testScript, []byte(scriptContent), 0755); err != nil { + t.Fatalf("Failed to create test script: %v", err) + } + + // Create registry and register our test extensions + registry := NewExtensionRegistry(tmpDir) + executor := NewExtensionExecutor(registry) + + // Test stdout-based extension + t.Run("StdoutExecution", func(t *testing.T) { + configPath := filepath.Join(tmpDir, "stdout-extension.yaml") + configContent := `name: stdout-test +executable: ` + testScript + ` +type: executable +timeout: 30s +operations: + greet: + cmd_template: "{{executable}} stdout {{1}}" +config: + output: + method: stdout` + + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + t.Fatalf("Failed to create config: %v", err) + } + + if err := registry.Register(configPath); err != nil { + t.Fatalf("Failed to register extension: %v", err) + } + + output, err := executor.Execute("stdout-test", "greet", "World") + if err != nil { + t.Errorf("Failed to execute: %v", err) + } + + expected := "Hello, World!\n" + if output != expected { + t.Errorf("Expected output %q, got %q", expected, output) + } + }) + + // Test file-based extension + t.Run("FileExecution", func(t *testing.T) { + configPath := filepath.Join(tmpDir, "file-extension.yaml") + configContent := `name: file-test +executable: ` + testScript + ` +type: executable +timeout: 30s +operations: + greet: + cmd_template: "{{executable}} file {{1}} {{2}}" +config: + output: + method: file + file_config: + cleanup: true + path_from_stdout: true` + + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + t.Fatalf("Failed to create config: %v", err) + } + + if err := registry.Register(configPath); err != nil { + t.Fatalf("Failed to register extension: %v", err) + } + + output, err := executor.Execute("file-test", "greet", "World|/tmp/test.txt") + if err != nil { + t.Errorf("Failed to execute: %v", err) + } + + expected := "Hello, World!\n" + if output != expected { + t.Errorf("Expected output %q, got %q", expected, output) + } + }) + + // Test execution errors + t.Run("ExecutionErrors", func(t *testing.T) { + // Test with non-existent extension + _, err := executor.Execute("nonexistent", "test", "value") + if err == nil { + t.Error("Expected error executing non-existent extension, got nil") + } + + // Test with invalid command that should exit non-zero + configPath := filepath.Join(tmpDir, "error-extension.yaml") + configContent := `name: error-test +executable: ` + testScript + ` +type: executable +timeout: 30s +operations: + invalid: + cmd_template: "{{executable}} invalid {{1}}" +config: + output: + method: stdout` + + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + t.Fatalf("Failed to create config: %v", err) + } + + if err := registry.Register(configPath); err != nil { + t.Fatalf("Failed to register extension: %v", err) + } + + _, err = executor.Execute("error-test", "invalid", "test") + if err == nil { + t.Error("Expected error from invalid command, got nil") + } + if !strings.Contains(err.Error(), "Unknown command") { + t.Errorf("Expected 'Unknown command' in error, got: %v", err) + } + }) +} + +func TestFixedFileExtensionExecutor(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "fabric-ext-executor-fixed-*") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create test script + testScript := filepath.Join(tmpDir, "test-script.sh") + scriptContent := `#!/bin/bash +case "$1" in + "write") + echo "Hello, $2!" > "$3" + ;; + "append") + echo "Hello, $2!" >> "$3" + ;; + "large") + for i in {1..1000}; do + echo "Line $i" >> "$3" + done + ;; + "error") + echo "Error message" >&2 + exit 1 + ;; + *) + echo "Unknown command" >&2 + exit 1 + ;; +esac` + + if err := os.WriteFile(testScript, []byte(scriptContent), 0755); err != nil { + t.Fatalf("Failed to create test script: %v", err) + } + + registry := NewExtensionRegistry(tmpDir) + executor := NewExtensionExecutor(registry) + + // Helper function to create and register extension + createExtension := func(name, opName, cmdTemplate string, config map[string]interface{}) error { + configPath := filepath.Join(tmpDir, name+".yaml") + configContent := `name: ` + name + ` +executable: ` + testScript + ` +type: executable +timeout: 30s +operations: + ` + opName + `: + cmd_template: "` + cmdTemplate + `" +config: + output: + method: file + file_config:` + + // Add config options + for k, v := range config { + configContent += "\n " + k + ": " + strings.TrimSpace(v.(string)) + } + + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + return err + } + + return registry.Register(configPath) + } + + // Test basic fixed file output + t.Run("BasicFixedFile", func(t *testing.T) { + outputFile := filepath.Join(tmpDir, "output.txt") + config := map[string]interface{}{ + "output_file": `"output.txt"`, + "work_dir": `"` + tmpDir + `"`, + "cleanup": "true", + } + + err := createExtension("basic-test", "write", + "{{executable}} write {{1}} "+outputFile, config) + if err != nil { + t.Fatalf("Failed to create extension: %v", err) + } + + output, err := executor.Execute("basic-test", "write", "World") + if err != nil { + t.Errorf("Failed to execute: %v", err) + } + + expected := "Hello, World!\n" + if output != expected { + t.Errorf("Expected output %q, got %q", expected, output) + } + }) + + // Test no work_dir specified + t.Run("NoWorkDir", func(t *testing.T) { + config := map[string]interface{}{ + "output_file": `"direct-output.txt"`, + "cleanup": "true", + } + + err := createExtension("no-workdir-test", "write", + "{{executable}} write {{1}} direct-output.txt", config) + if err != nil { + t.Fatalf("Failed to create extension: %v", err) + } + + _, err = executor.Execute("no-workdir-test", "write", "World") + if err != nil { + t.Errorf("Failed to execute: %v", err) + } + }) + + // Test cleanup behavior + t.Run("CleanupBehavior", func(t *testing.T) { + outputFile := filepath.Join(tmpDir, "cleanup-test.txt") + + // Test with cleanup enabled + config := map[string]interface{}{ + "output_file": `"cleanup-test.txt"`, + "work_dir": `"` + tmpDir + `"`, + "cleanup": "true", + } + + err := createExtension("cleanup-test", "write", + "{{executable}} write {{1}} "+outputFile, config) + if err != nil { + t.Fatalf("Failed to create extension: %v", err) + } + + _, err = executor.Execute("cleanup-test", "write", "World") + if err != nil { + t.Errorf("Failed to execute: %v", err) + } + + // File should be deleted after execution + if _, err := os.Stat(outputFile); !os.IsNotExist(err) { + t.Error("Expected output file to be cleaned up") + } + + // Test with cleanup disabled + config["cleanup"] = "false" + err = createExtension("no-cleanup-test", "write", + "{{executable}} write {{1}} "+outputFile, config) + if err != nil { + t.Fatalf("Failed to create extension: %v", err) + } + + _, err = executor.Execute("no-cleanup-test", "write", "World") + if err != nil { + t.Errorf("Failed to execute: %v", err) + } + + // File should remain after execution + if _, err := os.Stat(outputFile); os.IsNotExist(err) { + t.Error("Expected output file to remain") + } + }) + + // Test error cases + t.Run("ErrorCases", func(t *testing.T) { + outputFile := filepath.Join(tmpDir, "error-test.txt") + config := map[string]interface{}{ + "output_file": `"error-test.txt"`, + "work_dir": `"` + tmpDir + `"`, + "cleanup": "true", + } + + // Test command error + err := createExtension("error-test", "error", + "{{executable}} error {{1}} "+outputFile, config) + if err != nil { + t.Fatalf("Failed to create extension: %v", err) + } + + _, err = executor.Execute("error-test", "error", "World") + if err == nil { + t.Error("Expected error from failing command, got nil") + } + + // Test invalid work_dir + config["work_dir"] = `"/nonexistent/directory"` + err = createExtension("invalid-dir-test", "write", + "{{executable}} write {{1}} output.txt", config) + if err != nil { + t.Fatalf("Failed to create extension: %v", err) + } + + _, err = executor.Execute("invalid-dir-test", "write", "World") + if err == nil { + t.Error("Expected error from invalid work_dir, got nil") + } + }) + + // Test with missing output_file + t.Run("MissingOutputFile", func(t *testing.T) { + config := map[string]interface{}{ + "work_dir": `"` + tmpDir + `"`, + "cleanup": "true", + } + + err := createExtension("missing-output-test", "write", + "{{executable}} write {{1}} output.txt", config) + if err != nil { + t.Fatalf("Failed to create extension: %v", err) + } + + _, err = executor.Execute("missing-output-test", "write", "World") + if err == nil { + t.Error("Expected error from missing output_file, got nil") + } + }) +} \ No newline at end of file diff --git a/plugins/template/extension_manager_test.go b/plugins/template/extension_manager_test.go new file mode 100644 index 00000000..2deec2f1 --- /dev/null +++ b/plugins/template/extension_manager_test.go @@ -0,0 +1,184 @@ +package template + +import ( + "os" + "path/filepath" + "testing" +) + +// TestExtensionManager is the main test suite for ExtensionManager +func TestExtensionManager(t *testing.T) { + // Create temporary directory for tests + tmpDir, err := os.MkdirTemp("", "fabric-ext-test-*") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create test extension config + testConfig := filepath.Join(tmpDir, "test-extension.yaml") + testScript := filepath.Join(tmpDir, "test-script.sh") + + // Create test script + scriptContent := `#!/bin/bash +if [ "$1" = "echo" ]; then + echo "Hello, $2!" +fi` + + err = os.WriteFile(testScript, []byte(scriptContent), 0755) + if err != nil { + t.Fatalf("Failed to create test script: %v", err) + } + + // Create test config + configContent := `name: test-extension +executable: ` + testScript + ` +type: executable +timeout: 30s +description: "Test extension" +version: "1.0.0" +operations: + echo: + cmd_template: "{{executable}} echo {{1}}" +` + + err = os.WriteFile(testConfig, []byte(configContent), 0644) + if err != nil { + t.Fatalf("Failed to create test config: %v", err) + } + + // Initialize manager + manager := NewExtensionManager(tmpDir) + + // Test cases + t.Run("RegisterExtension", func(t *testing.T) { + err := manager.RegisterExtension(testConfig) + if err != nil { + t.Errorf("Failed to register extension: %v", err) + } + }) + + t.Run("ListExtensions", func(t *testing.T) { + err := manager.ListExtensions() + if err != nil { + t.Errorf("Failed to list extensions: %v", err) + } + // Note: Output validation would require capturing stdout + }) + + t.Run("ProcessExtension", func(t *testing.T) { + output, err := manager.ProcessExtension("test-extension", "echo", "World") + if err != nil { + t.Errorf("Failed to process extension: %v", err) + } + expected := "Hello, World!\n" + if output != expected { + t.Errorf("Expected output %q, got %q", expected, output) + } + }) + + t.Run("RemoveExtension", func(t *testing.T) { + err := manager.RemoveExtension("test-extension") + if err != nil { + t.Errorf("Failed to remove extension: %v", err) + } + + // Verify extension is removed by trying to process it + _, err = manager.ProcessExtension("test-extension", "echo", "World") + if err == nil { + t.Error("Expected error processing removed extension, got nil") + } + }) +} + +// TestExtensionManagerErrors tests error cases +func TestExtensionManagerErrors(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "fabric-ext-test-errors-*") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + manager := NewExtensionManager(tmpDir) + + t.Run("RegisterNonexistentConfig", func(t *testing.T) { + err := manager.RegisterExtension("/nonexistent/config.yaml") + if err == nil { + t.Error("Expected error registering nonexistent config, got nil") + } + }) + + t.Run("ProcessNonexistentExtension", func(t *testing.T) { + _, err := manager.ProcessExtension("nonexistent", "echo", "test") + if err == nil { + t.Error("Expected error processing nonexistent extension, got nil") + } + }) + + t.Run("RemoveNonexistentExtension", func(t *testing.T) { + err := manager.RemoveExtension("nonexistent") + if err == nil { + t.Error("Expected error removing nonexistent extension, got nil") + } + }) +} + +// TestExtensionManagerWithInvalidConfig tests handling of invalid configurations +func TestExtensionManagerWithInvalidConfig(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "fabric-ext-test-invalid-*") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + invalidConfig := filepath.Join(tmpDir, "invalid-extension.yaml") + + // Test cases with different invalid configurations + testCases := []struct { + name string + config string + wantErr bool + }{ + { + name: "MissingExecutable", + config: `name: invalid-extension +type: executable +timeout: 30s`, + wantErr: true, + }, + { + name: "InvalidTimeout", + config: `name: invalid-extension +executable: /bin/echo +type: executable +timeout: invalid`, + wantErr: true, + }, + { + name: "EmptyName", + config: `name: "" +executable: /bin/echo +type: executable +timeout: 30s`, + wantErr: true, + }, + } + + manager := NewExtensionManager(tmpDir) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := os.WriteFile(invalidConfig, []byte(tc.config), 0644) + if err != nil { + t.Fatalf("Failed to create invalid config file: %v", err) + } + + err = manager.RegisterExtension(invalidConfig) + if tc.wantErr && err == nil { + t.Error("Expected error registering invalid config, got nil") + } else if !tc.wantErr && err != nil { + t.Errorf("Unexpected error registering config: %v", err) + } + }) + } +} \ No newline at end of file diff --git a/plugins/template/extension_registry.go b/plugins/template/extension_registry.go index 1f156059..af75808b 100644 --- a/plugins/template/extension_registry.go +++ b/plugins/template/extension_registry.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "time" "gopkg.in/yaml.v3" // Add this import @@ -103,6 +104,8 @@ func (r *ExtensionRegistry) ensureConfigDir() error { return os.MkdirAll(extDir, 0755) } +// Update the Register method in extension_registry.go + func (r *ExtensionRegistry) Register(configPath string) error { // Read and parse the extension definition to verify it data, err := os.ReadFile(configPath) @@ -115,6 +118,11 @@ func (r *ExtensionRegistry) Register(configPath string) error { return fmt.Errorf("failed to parse config file: %w", err) } + // Add validation + if err := r.validateExtensionDefinition(&ext); err != nil { + return fmt.Errorf("invalid extension configuration: %w", err) + } + // Verify executable exists if _, err := os.Stat(ext.Executable); err != nil { return fmt.Errorf("executable not found: %w", err) @@ -143,6 +151,39 @@ func (r *ExtensionRegistry) Register(configPath string) error { return r.saveRegistry() } +func (r *ExtensionRegistry) validateExtensionDefinition(ext *ExtensionDefinition) error { + // Validate required fields + if ext.Name == "" { + return fmt.Errorf("extension name is required") + } + if ext.Executable == "" { + return fmt.Errorf("executable path is required") + } + if ext.Type == "" { + return fmt.Errorf("extension type is required") + } + + // Validate timeout format + if ext.Timeout != "" { + if _, err := time.ParseDuration(ext.Timeout); err != nil { + return fmt.Errorf("invalid timeout format: %w", err) + } + } + + // Validate operations + if len(ext.Operations) == 0 { + return fmt.Errorf("at least one operation must be defined") + } + for name, op := range ext.Operations { + if op.CmdTemplate == "" { + return fmt.Errorf("command template is required for operation %s", name) + } + } + + return nil +} + + func (r *ExtensionRegistry) Remove(name string) error { if _, exists := r.registry.Extensions[name]; !exists { return fmt.Errorf("extension %s not found", name) diff --git a/plugins/template/extension_registry_test.go b/plugins/template/extension_registry_test.go new file mode 100644 index 00000000..5dcadc4c --- /dev/null +++ b/plugins/template/extension_registry_test.go @@ -0,0 +1,75 @@ +package template + +import ( + "os" + "path/filepath" + "testing" +) + +func TestRegistryPersistence(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "fabric-ext-registry-persist-*") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create test executable + execPath := filepath.Join(tmpDir, "test-exec.sh") + execContent := []byte("#!/bin/bash\necho \"test\"") + err = os.WriteFile(execPath, execContent, 0755) + if err != nil { + t.Fatalf("Failed to create test executable: %v", err) + } + + // Create valid config + configContent := `name: test-extension +executable: ` + execPath + ` +type: executable +timeout: 30s +operations: + test: + cmd_template: "{{executable}} {{operation}}"` + + configPath := filepath.Join(tmpDir, "test-extension.yaml") + err = os.WriteFile(configPath, []byte(configContent), 0644) + if err != nil { + t.Fatalf("Failed to create test config: %v", err) + } + + // Test registry persistence + t.Run("SaveAndReload", func(t *testing.T) { + // Create and populate first registry + registry1 := NewExtensionRegistry(tmpDir) + err := registry1.Register(configPath) + if err != nil { + t.Fatalf("Failed to register extension: %v", err) + } + + // Create new registry instance and verify it loads the saved state + registry2 := NewExtensionRegistry(tmpDir) + ext, err := registry2.GetExtension("test-extension") + if err != nil { + t.Fatalf("Failed to get extension from reloaded registry: %v", err) + } + if ext.Name != "test-extension" { + t.Errorf("Expected extension name 'test-extension', got %q", ext.Name) + } + }) + + // Test hash verification + t.Run("HashVerification", func(t *testing.T) { + registry := NewExtensionRegistry(tmpDir) + + // Modify executable after registration + modifiedExecContent := []byte("#!/bin/bash\necho \"modified\"") + err := os.WriteFile(execPath, modifiedExecContent, 0755) + if err != nil { + t.Fatalf("Failed to modify executable: %v", err) + } + + _, err = registry.GetExtension("test-extension") + if err == nil { + t.Error("Expected error when executable modified, got nil") + } + }) +} \ No newline at end of file