diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 12d5909..1fa6655 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -17,15 +17,20 @@ import ( "github.com/beckn/beckn-onix/pkg/plugin/definition" ) -// TODO: Add unit tests for the plugin manager functions to ensure proper functionality and error handling. +type onixPlugin interface { + Lookup(string) (plugin.Symbol, error) +} // Manager is responsible for managing dynamically loaded plugins. type Manager struct { - plugins map[string]*plugin.Plugin // plugins holds the dynamically loaded plugins. - closers []func() // closers contains functions to release resources when the manager is closed. + plugins map[string]onixPlugin // plugins holds the dynamically loaded plugins. + closers []func() // closers contains functions to release resources when the manager is closed. } func validateMgrCfg(cfg *ManagerConfig) error { + if cfg.Root == "" { + return fmt.Errorf("root path cannot be empty") + } return nil } @@ -54,8 +59,8 @@ func NewManager(ctx context.Context, cfg *ManagerConfig) (*Manager, func(), erro }, nil } -func plugins(ctx context.Context, cfg *ManagerConfig) (map[string]*plugin.Plugin, error) { - plugins := make(map[string]*plugin.Plugin) +func plugins(ctx context.Context, cfg *ManagerConfig) (map[string]onixPlugin, error) { + plugins := make(map[string]onixPlugin) err := filepath.WalkDir(cfg.Root, func(path string, d fs.DirEntry, err error) error { if err != nil { @@ -86,7 +91,7 @@ func plugins(ctx context.Context, cfg *ManagerConfig) (map[string]*plugin.Plugin } // loadPlugin attempts to load a plugin from the given path and logs the execution time. -func loadPlugin(ctx context.Context, path, id string) (*plugin.Plugin, time.Duration, error) { +func loadPlugin(ctx context.Context, path, id string) (onixPlugin, time.Duration, error) { log.Debugf(ctx, "Loading plugin: %s", id) start := time.Now() @@ -99,7 +104,7 @@ func loadPlugin(ctx context.Context, path, id string) (*plugin.Plugin, time.Dura return p, elapsed, nil } -func provider[T any](plugins map[string]*plugin.Plugin, id string) (T, error) { +func provider[T any](plugins map[string]onixPlugin, id string) (T, error) { var zero T pgn, ok := plugins[id] if !ok { @@ -140,13 +145,6 @@ func (m *Manager) Publisher(ctx context.Context, cfg *Config) (definition.Publis return p, nil } -// addCloser appends a cleanup function to the Manager's closers list. -func (m *Manager) addCloser(closer func()) { - if closer != nil { - m.closers = append(m.closers, closer) - } -} - // SchemaValidator returns a SchemaValidator instance based on the provided configuration. // It registers a cleanup function for resource management. func (m *Manager) SchemaValidator(ctx context.Context, cfg *Config) (definition.SchemaValidator, error) { @@ -180,7 +178,7 @@ func (m *Manager) Router(ctx context.Context, cfg *Config) (definition.Router, e return nil, err } if closer != nil { - m.addCloser(func() { + m.closers = append(m.closers, func() { if err := closer(); err != nil { panic(err) } @@ -218,15 +216,17 @@ func (m *Manager) Cache(ctx context.Context, cfg *Config) (definition.Cache, err if err != nil { return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err) } - c, close, err := cp.New(ctx, cfg.Config) + c, closer, err := cp.New(ctx, cfg.Config) if err != nil { return nil, err } - m.addCloser(func() { - if err := close(); err != nil { - panic(err) - } - }) + if closer != nil { + m.closers = append(m.closers, func() { + if err := closer(); err != nil { + panic(err) + } + }) + } return c, nil } @@ -242,7 +242,7 @@ func (m *Manager) Signer(ctx context.Context, cfg *Config) (definition.Signer, e return nil, err } if closer != nil { - m.addCloser(func() { + m.closers = append(m.closers, func() { if err := closer(); err != nil { panic(err) } @@ -263,7 +263,7 @@ func (m *Manager) Encryptor(ctx context.Context, cfg *Config) (definition.Encryp return nil, err } if closer != nil { - m.addCloser(func() { + m.closers = append(m.closers, func() { if err := closer(); err != nil { panic(err) } @@ -286,7 +286,7 @@ func (m *Manager) Decryptor(ctx context.Context, cfg *Config) (definition.Decryp } if closer != nil { - m.addCloser(func() { + m.closers = append(m.closers, func() { if err := closer(); err != nil { panic(err) } @@ -308,7 +308,7 @@ func (m *Manager) SignValidator(ctx context.Context, cfg *Config) (definition.Si return nil, err } if closer != nil { - m.addCloser(func() { + m.closers = append(m.closers, func() { if err := closer(); err != nil { panic(err) } @@ -325,15 +325,17 @@ func (m *Manager) KeyManager(ctx context.Context, cache definition.Cache, rClien if err != nil { return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err) } - km, close, err := kmp.New(ctx, cache, rClient, cfg.Config) + km, closer, err := kmp.New(ctx, cache, rClient, cfg.Config) if err != nil { return nil, err } - m.addCloser(func() { - if err := close(); err != nil { - panic(err) - } - }) + if closer != nil { + m.closers = append(m.closers, func() { + if err := closer(); err != nil { + panic(err) + } + }) + } return km, nil } diff --git a/pkg/plugin/manager_test.go b/pkg/plugin/manager_test.go new file mode 100644 index 0000000..29d029b --- /dev/null +++ b/pkg/plugin/manager_test.go @@ -0,0 +1,2495 @@ +package plugin + +import ( + "archive/zip" + "context" + "errors" + "net/http" + "os" + "os/exec" + "path/filepath" + "plugin" + "strings" + "testing" + + "github.com/beckn/beckn-onix/pkg/model" + "github.com/beckn/beckn-onix/pkg/plugin/definition" +) + +type mockPlugin struct { + symbol plugin.Symbol + err error +} + +func (m *mockPlugin) Lookup(str string) (plugin.Symbol, error) { + return m.symbol, m.err +} + +// Mock implementations for testing. +type mockPublisher struct { + definition.Publisher +} + +type mockSchemaValidator struct { + definition.SchemaValidator +} + +type mockRouter struct { + definition.Router +} + +type mockStep struct { + definition.Step +} + +type mockCache struct { + definition.Cache +} + +type mockSigner struct { + definition.Signer +} + +type mockEncrypter struct { + definition.Encrypter +} + +type mockDecrypter struct { + definition.Decrypter +} + +type mockSignValidator struct { + definition.SignValidator +} + +type mockKeyManager struct { + definition.KeyManager + err error +} + +func (m *mockKeyManager) GenerateKeyPairs() (*model.Keyset, error) { + if m.err != nil { + return nil, m.err + } + return &model.Keyset{}, nil +} + +func (m *mockKeyManager) StorePrivateKeys(ctx context.Context, keyID string, keys *model.Keyset) error { + return m.err +} + +func (m *mockKeyManager) SigningPrivateKey(ctx context.Context, keyID string) (string, string, error) { + if m.err != nil { + return "", "", m.err + } + return "signing-key", "signing-algo", nil +} + +func (m *mockKeyManager) EncrPrivateKey(ctx context.Context, keyID string) (string, string, error) { + if m.err != nil { + return "", "", m.err + } + return "encr-key", "encr-algo", nil +} + +func (m *mockKeyManager) SigningPublicKey(ctx context.Context, subscriberID, uniqueKeyID string) (string, error) { + if m.err != nil { + return "", m.err + } + return "public-signing-key", nil +} + +func (m *mockKeyManager) EncrPublicKey(ctx context.Context, subscriberID, uniqueKeyID string) (string, error) { + if m.err != nil { + return "", m.err + } + return "public-encr-key", nil +} + +func (m *mockKeyManager) DeletePrivateKeys(ctx context.Context, keyID string) error { + return m.err +} + +// Mock providers. +type mockPublisherProvider struct { + publisher definition.Publisher + err error + errFunc func() error +} + +func (m *mockPublisherProvider) New(ctx context.Context, config map[string]string) (definition.Publisher, func() error, error) { + return m.publisher, m.errFunc, m.err +} + +type mockSchemaValidatorProvider struct { + validator *mockSchemaValidator + err error + errFunc func() error +} + +func (m *mockSchemaValidatorProvider) New(ctx context.Context, config map[string]string) (definition.SchemaValidator, func() error, error) { + if m.err != nil { + return nil, nil, m.err + } + return m.validator, func() error { return nil }, nil +} + +// Mock providers for additional interfaces. +type mockRouterProvider struct { + router *mockRouter + err error + errFunc func() error +} + +func (m *mockRouterProvider) New(ctx context.Context, config map[string]string) (definition.Router, func() error, error) { + if m.err != nil { + return nil, nil, m.err + } + return m.router, func() error { return nil }, nil +} + +type mockMiddlewareProvider struct { + middleware func(http.Handler) http.Handler + err error +} + +func (m *mockMiddlewareProvider) New(ctx context.Context, config map[string]string) (func(http.Handler) http.Handler, error) { + if m.err != nil { + return nil, m.err + } + if m.middleware == nil { + m.middleware = func(h http.Handler) http.Handler { return h } + } + return m.middleware, nil +} + +type mockStepProvider struct { + step *mockStep + err error + errFunc func() error +} + +func (m *mockStepProvider) New(ctx context.Context, config map[string]string) (definition.Step, func(), error) { + if m.err != nil { + return nil, nil, m.err + } + return m.step, func() {}, nil +} + +// Mock providers for additional interfaces. +type mockCacheProvider struct { + cache *mockCache + err error + errFunc func() error +} + +func (m *mockCacheProvider) New(ctx context.Context, config map[string]string) (definition.Cache, func() error, error) { + if m.err != nil { + return nil, nil, m.err + } + return m.cache, func() error { return nil }, nil +} + +type mockSignerProvider struct { + signer *mockSigner + err error + errFunc func() error +} + +func (m *mockSignerProvider) New(ctx context.Context, config map[string]string) (definition.Signer, func() error, error) { + if m.err != nil { + return nil, nil, m.err + } + return m.signer, func() error { return nil }, nil +} + +type mockEncrypterProvider struct { + encrypter *mockEncrypter + err error + errFunc func() error +} + +func (m *mockEncrypterProvider) New(ctx context.Context, config map[string]string) (definition.Encrypter, func() error, error) { + if m.err != nil { + return nil, nil, m.err + } + return m.encrypter, func() error { return nil }, nil +} + +type mockDecrypterProvider struct { + decrypter *mockDecrypter + err error + errFunc func() error +} + +func (m *mockDecrypterProvider) New(ctx context.Context, config map[string]string) (definition.Decrypter, func() error, error) { + if m.err != nil { + return nil, nil, m.err + } + return m.decrypter, func() error { return nil }, nil +} + +type mockSignValidatorProvider struct { + validator *mockSignValidator + err error + errFunc func() error +} + +func (m *mockSignValidatorProvider) New(ctx context.Context, config map[string]string) (definition.SignValidator, func() error, error) { + if m.err != nil { + return nil, nil, m.err + } + return m.validator, func() error { return nil }, nil +} + +type mockKeyManagerProvider struct { + keyManager *mockKeyManager + err error + errFunc func() error +} + +func (m *mockKeyManagerProvider) New(ctx context.Context, cache definition.Cache, lookup definition.RegistryLookup, config map[string]string) (definition.KeyManager, func() error, error) { + if m.err != nil { + return nil, nil, m.err + } + return m.keyManager, func() error { return nil }, nil +} + +// Mock registry lookup for testing. +type mockRegistryLookup struct { + definition.RegistryLookup +} + +// createTestZip creates a zip file with test content in a temporary directory. +func createTestZip(t *testing.T) string { + t.Helper() + // Create a temporary directory for the zip file + tempDir := t.TempDir() + zipPath := filepath.Join(tempDir, "test.zip") + + // Create a zip file + zipFile, err := os.Create(zipPath) + if err != nil { + t.Fatalf("Failed to create zip file: %v", err) + } + defer zipFile.Close() + + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + // Add a test file to the zip + testFile, err := zipWriter.Create("test.txt") + if err != nil { + t.Fatalf("Failed to create file in zip: %v", err) + } + _, err = testFile.Write([]byte("test content")) + if err != nil { + t.Fatalf("Failed to write to file: %v", err) + } + + return zipPath +} + +// TestNewManagerSuccess tests the successful scenarios of the NewManager function. +func TestNewManagerSuccess(t *testing.T) { + // Build the dummy plugin first. + cmd := exec.Command("go", "build", "-buildmode=plugin", "-o", "./testdata/dummy.so", "./testdata/dummy.go") + if err := cmd.Run(); err != nil { + t.Fatalf("Failed to build dummy plugin: %v", err) + } + + // Clean up the .so file after test completes. + t.Cleanup(func() { + if err := os.Remove("./testdata/dummy.so"); err != nil && !os.IsNotExist(err) { + t.Logf("Failed to remove dummy.so: %v", err) + } + }) + + tests := []struct { + name string + cfg *ManagerConfig + }{ + { + name: "valid config with root path", + cfg: &ManagerConfig{ + Root: t.TempDir(), + RemoteRoot: "", + }, + }, + { + name: "valid config with remote root", + cfg: &ManagerConfig{ + Root: t.TempDir(), + RemoteRoot: func() string { + zipPath := createTestZip(t) + return zipPath + }(), + }, + }, + { + name: "valid config with so file", + cfg: &ManagerConfig{ + Root: "./testdata", + RemoteRoot: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + m, cleanup, err := NewManager(ctx, tt.cfg) + if err != nil { + t.Fatalf("NewManager() error = %v, want nil", err) + } + if m == nil { + t.Fatal("NewManager() returned nil manager") + } + if cleanup == nil { + t.Fatal("NewManager() returned nil cleanup function") + } + + // Verify manager fields. + if m.plugins == nil { + t.Fatal("NewManager() returned manager with nil plugins map") + } + if m.closers == nil { + t.Fatal("NewManager() returned manager with nil closers slice") + } + + // Call cleanup to ensure it doesn't panic. + cleanup() + }) + } +} + +// TestNewManagerFailure tests the failure scenarios of the NewManager function. +func TestNewManagerFailure(t *testing.T) { + tests := []struct { + name string + cfg *ManagerConfig + expectedError string + }{ + { + name: "invalid config with empty root", + cfg: &ManagerConfig{ + Root: "", + RemoteRoot: "", + }, + expectedError: "root path cannot be empty", + }, + { + name: "invalid config with nonexistent root", + cfg: &ManagerConfig{ + Root: "/nonexistent/dir", + RemoteRoot: "", + }, + expectedError: "no such file or directory", + }, + { + name: "invalid config with nonexistent remote root", + cfg: &ManagerConfig{ + Root: t.TempDir(), + RemoteRoot: "/nonexistent/remote.zip", + }, + expectedError: "no such file or directory", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + m, cleanup, err := NewManager(ctx, tt.cfg) + if err == nil { + t.Fatal("NewManager() expected error, got nil") + } + if m != nil { + t.Fatal("NewManager() returned non-nil manager for error case") + } + if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("NewManager() error = %v, want error containing %q", err, tt.expectedError) + } + if cleanup != nil { + t.Fatal("NewManager() returned non-nil cleanup function for error case") + } + + }) + } +} + +func TestPublisherSuccess(t *testing.T) { + t.Run("successful publisher creation", func(t *testing.T) { + publisherID := "publisherId" + mockPublisher := &mockPublisher{} + errFunc := func() error { return nil } + m := &Manager{ + plugins: map[string]onixPlugin{ + publisherID: &mockPlugin{ + symbol: &mockPublisherProvider{ + publisher: mockPublisher, + errFunc: errFunc, + }, + }, + }, + closers: []func(){}, + } + + p, err := m.Publisher(context.Background(), &Config{ + ID: publisherID, + Config: map[string]string{}, + }) + + if err != nil { + t.Fatalf("Manager.Publisher() error = %v, want no error", err) + } + + if p != mockPublisher { + t.Fatalf("Manager.Publisher() did not return the correct publisher") + } + + if len(m.closers) != 1 { + t.Fatalf("Manager.closers has %d closers, expected 1", len(m.closers)) + } + + m.closers[0]() + + }) +} + +// TestPublisherFailure tests the failure scenarios of the Publisher method. +func TestPublisherFailure(t *testing.T) { + tests := []struct { + name string + publisherID string + plugins map[string]onixPlugin + expectedError string + }{ + { + name: "plugin not found", + publisherID: "nonexistent", + plugins: make(map[string]onixPlugin), + expectedError: "plugin nonexistent not found", + }, + { + name: "provider error", + publisherID: "error-provider", + plugins: map[string]onixPlugin{ + "error-provider": &mockPlugin{ + symbol: nil, + err: errors.New("provider error"), + }, + }, + expectedError: "provider error", + }, + { + name: "lookup error", + publisherID: "lookup-error", + plugins: map[string]onixPlugin{ + "lookup-error": &mockPlugin{ + symbol: nil, + err: errors.New("lookup failed"), + }, + }, + expectedError: "lookup failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &Manager{ + closers: []func(){}, + plugins: tt.plugins, + } + + p, err := m.Publisher(context.Background(), &Config{ + ID: tt.publisherID, + Config: map[string]string{}, + }) + + if err == nil { + t.Fatal("Manager.Publisher() expected error, got nil") + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("Manager.Publisher() error = %v, want error containing %q", err, tt.expectedError) + } + + if p != nil { + t.Fatal("Manager.Publisher() expected nil publisher, got non-nil") + } + }) + } +} + +// TestSchemaValidatorSuccess tests the successful scenarios of the SchemaValidator method. +func TestSchemaValidatorSuccess(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockSchemaValidatorProvider + }{ + { + name: "successful validator creation", + cfg: &Config{ + ID: "test-validator", + Config: map[string]string{}, + }, + plugin: &mockSchemaValidatorProvider{ + validator: &mockSchemaValidator{}, + errFunc: func() error { return nil }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: map[string]onixPlugin{ + tt.cfg.ID: &mockPlugin{ + symbol: tt.plugin, + }, + }, + closers: []func(){}, + } + + // Call SchemaValidator. + validator, err := m.SchemaValidator(context.Background(), tt.cfg) + + // Check success case. + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if validator != tt.plugin.validator { + t.Fatal("validator does not match expected instance") + } + + if len(m.closers) != 1 { + t.Fatalf("Manager.closers has %d closers, expected 1", len(m.closers)) + } + + m.closers[0]() + }) + } +} + +// TestSchemaValidatorFailure tests the failure scenarios of the SchemaValidator method. +func TestSchemaValidatorFailure(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin definition.SchemaValidatorProvider + expectedError string + }{ + { + name: "provider error", + cfg: &Config{ + ID: "test-validator", + Config: map[string]string{}, + }, + plugin: &mockSchemaValidatorProvider{ + err: errors.New("provider error"), + }, + expectedError: "provider error", + }, + { + name: "plugin not found", + cfg: &Config{ + ID: "nonexistent-validator", + Config: map[string]string{}, + }, + plugin: nil, + expectedError: "plugin nonexistent-validator not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: make(map[string]onixPlugin), + closers: []func(){}, + } + + // Only add the plugin if it's not nil. + if tt.plugin != nil { + m.plugins[tt.cfg.ID] = &mockPlugin{ + symbol: tt.plugin, + } + } + + // Call SchemaValidator. + validator, err := m.SchemaValidator(context.Background(), tt.cfg) + + // Check error. + if err == nil { + t.Fatal("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("error = %v, want error containing %q", err, tt.expectedError) + } + if validator != nil { + t.Fatal("expected nil validator, got non-nil") + } + }) + } +} + +// TestRouterSuccess tests the successful scenarios of the Router method. +func TestRouterSuccess(t *testing.T) { + t.Run("successful router creation", func(t *testing.T) { + cfg := &Config{ + ID: "test-router", + Config: map[string]string{}, + } + plugin := &mockRouterProvider{ + router: &mockRouter{}, + errFunc: func() error { return nil }, + } + // Create a manager with the mock plugin. + m := &Manager{ + plugins: map[string]onixPlugin{ + cfg.ID: &mockPlugin{ + symbol: plugin, + }, + }, + closers: []func(){}, + } + + // Call Router. + router, err := m.Router(context.Background(), cfg) + + // Check success case. + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if router == nil { + t.Fatal("expected non-nil router, got nil") + } + if router != plugin.router { + t.Fatal("router does not match expected instance") + } + + if len(m.closers) != 1 { + t.Fatalf("Manager.closers has %d closers, expected 1", len(m.closers)) + } + + m.closers[0]() + }) +} + +// TestRouterFailure tests the failure scenarios of the Router method. +func TestRouterFailure(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockRouterProvider + expectedError string + }{ + { + name: "provider error", + cfg: &Config{ + ID: "test-router", + Config: map[string]string{}, + }, + plugin: &mockRouterProvider{ + err: errors.New("provider error"), + }, + expectedError: "provider error", + }, + { + name: "plugin not found", + cfg: &Config{ + ID: "nonexistent-router", + Config: map[string]string{}, + }, + plugin: nil, + expectedError: "plugin nonexistent-router not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: make(map[string]onixPlugin), + closers: []func(){}, + } + + // Only add the plugin if it's not nil. + if tt.plugin != nil { + m.plugins[tt.cfg.ID] = &mockPlugin{ + symbol: tt.plugin, + } + } + + // Call Router. + router, err := m.Router(context.Background(), tt.cfg) + + // Check error. + if err == nil { + t.Fatal("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("error = %v, want error containing %q", err, tt.expectedError) + } + if router != nil { + t.Fatal("expected nil router, got non-nil") + } + }) + } +} + +// TestStepSuccess tests the successful scenarios of the Step method. +func TestStepSuccess(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockStepProvider + }{ + { + name: "successful step creation", + cfg: &Config{ + ID: "test-step", + Config: map[string]string{}, + }, + plugin: &mockStepProvider{ + step: &mockStep{}, + errFunc: func() error { return nil }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: map[string]onixPlugin{ + tt.cfg.ID: &mockPlugin{ + symbol: tt.plugin, + }, + }, + closers: []func(){}, + } + + // Call Step. + step, err := m.Step(context.Background(), tt.cfg) + + // Check success case. + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if step == nil { + t.Fatal("expected non-nil step, got nil") + } + if step != tt.plugin.step { + t.Fatal("step does not match expected instance") + } + + if len(m.closers) != 1 { + t.Fatalf("Manager.closers has %d closers, expected 1", len(m.closers)) + } + + m.closers[0]() + }) + } +} + +// TestStepFailure tests the failure scenarios of the Step method. +func TestStepFailure(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockStepProvider + expectedError string + }{ + { + name: "provider error", + cfg: &Config{ + ID: "test-step", + Config: map[string]string{}, + }, + plugin: &mockStepProvider{ + err: errors.New("provider error"), + }, + expectedError: "provider error", + }, + { + name: "plugin not found", + cfg: &Config{ + ID: "nonexistent-step", + Config: map[string]string{}, + }, + plugin: nil, + expectedError: "plugin nonexistent-step not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: make(map[string]onixPlugin), + closers: []func(){}, + } + + // Only add the plugin if it's not nil. + if tt.plugin != nil { + m.plugins[tt.cfg.ID] = &mockPlugin{ + symbol: tt.plugin, + } + } + + // Call Step. + step, err := m.Step(context.Background(), tt.cfg) + + // Check error. + if err == nil { + t.Fatal("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("error = %v, want error containing %q", err, tt.expectedError) + } + if step != nil { + t.Fatal("expected nil step, got non-nil") + } + }) + } +} + +// TestCacheSuccess tests the successful scenarios of the Cache method. +func TestCacheSuccess(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockCacheProvider + }{ + { + name: "successful cache creation", + cfg: &Config{ + ID: "test-cache", + Config: map[string]string{}, + }, + plugin: &mockCacheProvider{ + cache: &mockCache{}, + errFunc: func() error { return nil }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: map[string]onixPlugin{ + tt.cfg.ID: &mockPlugin{ + symbol: tt.plugin, + }, + }, + closers: []func(){}, + } + + // Call Cache. + cache, err := m.Cache(context.Background(), tt.cfg) + + // Check success case. + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cache == nil { + t.Fatal("expected non-nil cache, got nil") + } + + if cache != tt.plugin.cache { + t.Fatal("cache does not match expected instance") + } + + if len(m.closers) != 1 { + t.Fatalf("Manager.closers has %d closers, expected 1", len(m.closers)) + } + + m.closers[0]() + }) + } +} + +// TestCacheFailure tests the failure scenarios of the Cache method. +func TestCacheFailure(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockCacheProvider + expectedError string + }{ + { + name: "provider error", + cfg: &Config{ + ID: "test-cache", + Config: map[string]string{}, + }, + plugin: &mockCacheProvider{ + err: errors.New("provider error"), + }, + expectedError: "provider error", + }, + { + name: "plugin not found", + cfg: &Config{ + ID: "nonexistent-cache", + Config: map[string]string{}, + }, + plugin: nil, + expectedError: "plugin nonexistent-cache not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: make(map[string]onixPlugin), + closers: []func(){}, + } + + // Only add the plugin if it's not nil. + if tt.plugin != nil { + m.plugins[tt.cfg.ID] = &mockPlugin{ + symbol: tt.plugin, + } + } + + // Call Cache. + cache, err := m.Cache(context.Background(), tt.cfg) + + // Check error. + if err == nil { + t.Fatal("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("error = %v, want error containing %q", err, tt.expectedError) + } + if cache != nil { + t.Fatal("expected nil cache, got non-nil") + } + }) + } +} + +// TestSignerSuccess tests the successful scenarios of the Signer method. +func TestSignerSuccess(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockSignerProvider + }{ + { + name: "successful signer creation", + cfg: &Config{ + ID: "test-signer", + Config: map[string]string{}, + }, + plugin: &mockSignerProvider{ + signer: &mockSigner{}, + errFunc: func() error { return nil }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: map[string]onixPlugin{ + tt.cfg.ID: &mockPlugin{ + symbol: tt.plugin, + }, + }, + closers: []func(){}, + } + + // Call Signer. + signer, err := m.Signer(context.Background(), tt.cfg) + + // Check success case. + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if signer == nil { + t.Fatal("expected non-nil signer, got nil") + } + + if signer != tt.plugin.signer { + t.Fatal("signer does not match expected instance") + } + + if len(m.closers) != 1 { + t.Fatalf("Manager.closers has %d closers, expected 1", len(m.closers)) + } + + m.closers[0]() + }) + } +} + +// TestSignerFailure tests the failure scenarios of the Signer method. +func TestSignerFailure(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockSignerProvider + expectedError string + }{ + { + name: "provider error", + cfg: &Config{ + ID: "test-signer", + Config: map[string]string{}, + }, + plugin: &mockSignerProvider{ + err: errors.New("provider error"), + }, + expectedError: "provider error", + }, + { + name: "plugin not found", + cfg: &Config{ + ID: "nonexistent-signer", + Config: map[string]string{}, + }, + plugin: nil, + expectedError: "plugin nonexistent-signer not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: make(map[string]onixPlugin), + closers: []func(){}, + } + + // Only add the plugin if it's not nil. + if tt.plugin != nil { + m.plugins[tt.cfg.ID] = &mockPlugin{ + symbol: tt.plugin, + } + } + + // Call Signer. + signer, err := m.Signer(context.Background(), tt.cfg) + + // Check error. + if err == nil { + t.Fatal("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("error = %v, want error containing %q", err, tt.expectedError) + } + if signer != nil { + t.Fatal("expected nil signer, got non-nil") + } + }) + } +} + +// TestEncryptorSuccess tests the successful scenarios of the Encryptor method. +func TestEncryptorSuccess(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockEncrypterProvider + }{ + { + name: "successful encrypter creation", + cfg: &Config{ + ID: "test-encrypter", + Config: map[string]string{}, + }, + plugin: &mockEncrypterProvider{ + encrypter: &mockEncrypter{}, + errFunc: func() error { return nil }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: map[string]onixPlugin{ + tt.cfg.ID: &mockPlugin{ + symbol: tt.plugin, + }, + }, + closers: []func(){}, + } + + // Call Encryptor. + encrypter, err := m.Encryptor(context.Background(), tt.cfg) + + // Check success case. + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if encrypter == nil { + t.Fatal("expected non-nil encrypter, got nil") + } + + if encrypter != tt.plugin.encrypter { + t.Fatal("encrypter does not match expected instance") + } + + if len(m.closers) != 1 { + t.Fatalf("Manager.closers has %d closers, expected 1", len(m.closers)) + } + + m.closers[0]() + }) + } +} + +// TestEncryptorFailure tests the failure scenarios of the Encryptor method. +func TestEncryptorFailure(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockEncrypterProvider + expectedError string + }{ + { + name: "provider error", + cfg: &Config{ + ID: "test-encrypter", + Config: map[string]string{}, + }, + plugin: &mockEncrypterProvider{ + err: errors.New("provider error"), + }, + expectedError: "provider error", + }, + { + name: "plugin not found", + cfg: &Config{ + ID: "nonexistent-encrypter", + Config: map[string]string{}, + }, + plugin: nil, + expectedError: "plugin nonexistent-encrypter not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: make(map[string]onixPlugin), + closers: []func(){}, + } + + // Only add the plugin if it's not nil. + if tt.plugin != nil { + m.plugins[tt.cfg.ID] = &mockPlugin{ + symbol: tt.plugin, + } + } + + // Call Encryptor. + encrypter, err := m.Encryptor(context.Background(), tt.cfg) + + // Check error. + if err == nil { + t.Fatal("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("error = %v, want error containing %q", err, tt.expectedError) + } + if encrypter != nil { + t.Fatal("expected nil encrypter, got non-nil") + } + }) + } +} + +// TestDecryptorSuccess tests the successful scenarios of the Decryptor method. +func TestDecryptorSuccess(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockDecrypterProvider + }{ + { + name: "successful decrypter creation", + cfg: &Config{ + ID: "test-decrypter", + Config: map[string]string{}, + }, + plugin: &mockDecrypterProvider{ + decrypter: &mockDecrypter{}, + errFunc: func() error { return nil }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: map[string]onixPlugin{ + tt.cfg.ID: &mockPlugin{ + symbol: tt.plugin, + }, + }, + closers: []func(){}, + } + + // Call Decryptor. + decrypter, err := m.Decryptor(context.Background(), tt.cfg) + + // Check success case. + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if decrypter == nil { + t.Fatal("expected non-nil decrypter, got nil") + } + + if decrypter != tt.plugin.decrypter { + t.Fatal("decrypter does not match expected instance") + } + + if len(m.closers) != 1 { + t.Fatalf("Manager.closers has %d closers, expected 1", len(m.closers)) + } + + m.closers[0]() + }) + } +} + +// TestDecryptorFailure tests the failure scenarios of the Decryptor method. +func TestDecryptorFailure(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockDecrypterProvider + expectedError string + }{ + { + name: "provider error", + cfg: &Config{ + ID: "test-decrypter", + Config: map[string]string{}, + }, + plugin: &mockDecrypterProvider{ + err: errors.New("provider error"), + }, + expectedError: "provider error", + }, + { + name: "plugin not found", + cfg: &Config{ + ID: "nonexistent-decrypter", + Config: map[string]string{}, + }, + plugin: nil, + expectedError: "plugin nonexistent-decrypter not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: make(map[string]onixPlugin), + closers: []func(){}, + } + + // Only add the plugin if it's not nil. + if tt.plugin != nil { + m.plugins[tt.cfg.ID] = &mockPlugin{ + symbol: tt.plugin, + } + } + + // Call Decryptor. + decrypter, err := m.Decryptor(context.Background(), tt.cfg) + + // Check error. + if err == nil { + t.Fatal("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("error = %v, want error containing %q", err, tt.expectedError) + } + if decrypter != nil { + t.Fatal("expected nil decrypter, got non-nil") + } + }) + } +} + +// TestSignValidatorSuccess tests the successful scenarios of the SignValidator method. +func TestSignValidatorSuccess(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockSignValidatorProvider + }{ + { + name: "successful sign validator creation", + cfg: &Config{ + ID: "test-sign-validator", + Config: map[string]string{}, + }, + plugin: &mockSignValidatorProvider{ + validator: &mockSignValidator{}, + errFunc: func() error { return nil }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: map[string]onixPlugin{ + tt.cfg.ID: &mockPlugin{ + symbol: tt.plugin, + }, + }, + closers: []func(){}, + } + + // Call SignValidator. + validator, err := m.SignValidator(context.Background(), tt.cfg) + + // Check success case. + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if validator == nil { + t.Fatal("expected non-nil validator, got nil") + } + if validator != tt.plugin.validator { + t.Fatal("validator does not match expected instance") + } + + if len(m.closers) != 1 { + t.Fatalf("Manager.closers has %d closers, expected 1", len(m.closers)) + } + + m.closers[0]() + }) + } +} + +// TestSignValidatorFailure tests the failure scenarios of the SignValidator method. +func TestSignValidatorFailure(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockSignValidatorProvider + expectedError string + }{ + { + name: "provider error", + cfg: &Config{ + ID: "test-sign-validator", + Config: map[string]string{}, + }, + plugin: &mockSignValidatorProvider{ + err: errors.New("provider error"), + }, + expectedError: "provider error", + }, + { + name: "plugin not found", + cfg: &Config{ + ID: "nonexistent-sign-validator", + Config: map[string]string{}, + }, + plugin: nil, + expectedError: "plugin nonexistent-sign-validator not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: make(map[string]onixPlugin), + closers: []func(){}, + } + + // Only add the plugin if it's not nil. + if tt.plugin != nil { + m.plugins[tt.cfg.ID] = &mockPlugin{ + symbol: tt.plugin, + } + } + + // Call SignValidator. + validator, err := m.SignValidator(context.Background(), tt.cfg) + + // Check error. + if err == nil { + t.Fatal("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("error = %v, want error containing %q", err, tt.expectedError) + } + if validator != nil { + t.Fatal("expected nil validator, got non-nil") + } + }) + } +} + +// TestKeyManagerSuccess tests the successful scenarios of the KeyManager method. +func TestKeyManagerSuccess(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockKeyManagerProvider + }{ + { + name: "successful key manager creation", + cfg: &Config{ + ID: "test-key-manager", + Config: map[string]string{}, + }, + plugin: &mockKeyManagerProvider{ + keyManager: &mockKeyManager{}, + errFunc: func() error { return nil }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: map[string]onixPlugin{ + tt.cfg.ID: &mockPlugin{ + symbol: tt.plugin, + }, + }, + closers: []func(){}, + } + + // Create mock cache and registry lookup. + mockCache := &mockCache{} + mockRegistry := &mockRegistryLookup{} + + // Call KeyManager. + keyManager, err := m.KeyManager(context.Background(), mockCache, mockRegistry, tt.cfg) + + // Check success case. + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if keyManager == nil { + t.Fatal("expected non-nil key manager, got nil") + } + + if keyManager != tt.plugin.keyManager { + t.Fatal("key manager does not match expected instance") + } + + if len(m.closers) != 1 { + t.Fatalf("Manager.closers has %d closers, expected 1", len(m.closers)) + } + + m.closers[0]() + }) + } +} + +// TestKeyManagerFailure tests the failure scenarios of the KeyManager method. +func TestKeyManagerFailure(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockKeyManagerProvider + expectedError string + }{ + { + name: "provider error", + cfg: &Config{ + ID: "test-key-manager", + Config: map[string]string{}, + }, + plugin: &mockKeyManagerProvider{ + err: errors.New("provider error"), + }, + expectedError: "provider error", + }, + { + name: "plugin not found", + cfg: &Config{ + ID: "nonexistent-key-manager", + Config: map[string]string{}, + }, + plugin: nil, + expectedError: "plugin nonexistent-key-manager not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: make(map[string]onixPlugin), + closers: []func(){}, + } + + // Only add the plugin if it's not nil. + if tt.plugin != nil { + m.plugins[tt.cfg.ID] = &mockPlugin{ + symbol: tt.plugin, + } + } + + // Create mock cache and registry lookup. + mockCache := &mockCache{} + mockRegistry := &mockRegistryLookup{} + + // Call KeyManager. + keyManager, err := m.KeyManager(context.Background(), mockCache, mockRegistry, tt.cfg) + + // Check error. + if err == nil { + t.Fatal("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("error = %v, want error containing %q", err, tt.expectedError) + } + if keyManager != nil { + t.Fatal("expected nil key manager, got non-nil") + } + }) + } +} + +// TestUnzipSuccess tests the successful scenarios of the unzip function. +func TestUnzipSuccess(t *testing.T) { + tests := []struct { + name string + setupFunc func() (string, string, func()) // returns src, dest, cleanup. + verifyFunc func(t *testing.T, dest string) + }{ + { + name: "extract single file", + setupFunc: func() (string, string, func()) { + // Create a temporary directory for the test. + tempDir, err := os.MkdirTemp("", "unzip-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + // Create a zip file with a single test file. + zipPath := filepath.Join(tempDir, "test.zip") + zipFile, err := os.Create(zipPath) + if err != nil { + t.Fatalf("Failed to create zip file: %v", err) + } + + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + // Add a test file to the zip. + testFile, err := zipWriter.Create("test.txt") + if err != nil { + t.Fatalf("Failed to create file in zip: %v", err) + } + _, err = testFile.Write([]byte("test content")) + if err != nil { + t.Fatalf("Failed to write to file: %v", err) + } + + zipWriter.Close() + zipFile.Close() + + // Create destination directory. + destDir := filepath.Join(tempDir, "extracted") + return zipPath, destDir, func() { + os.RemoveAll(tempDir) + } + }, + verifyFunc: func(t *testing.T, dest string) { + // Verify the extracted file exists and has correct content. + content, err := os.ReadFile(filepath.Join(dest, "test.txt")) + if err != nil { + t.Fatalf("Failed to read extracted file: %v", err) + } + if string(content) != "test content" { + t.Fatalf("Extracted file content = %v, want %v", string(content), "test content") + } + }, + }, + { + name: "extract file in subdirectory", + setupFunc: func() (string, string, func()) { + // Create a temporary directory for the test. + tempDir, err := os.MkdirTemp("", "unzip-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + // Create a zip file with a file in a subdirectory. + zipPath := filepath.Join(tempDir, "test.zip") + zipFile, err := os.Create(zipPath) + if err != nil { + t.Fatalf("Failed to create zip file: %v", err) + } + + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + // Add a file in a subdirectory. + testFile, err := zipWriter.Create("subdir/test.txt") + if err != nil { + t.Fatalf("Failed to create file in zip: %v", err) + } + _, err = testFile.Write([]byte("subdirectory content")) + if err != nil { + t.Fatalf("Failed to write to file: %v", err) + } + + zipWriter.Close() + zipFile.Close() + + // Create destination directory. + destDir := filepath.Join(tempDir, "extracted") + return zipPath, destDir, func() { + os.RemoveAll(tempDir) + } + }, + verifyFunc: func(t *testing.T, dest string) { + // Verify the extracted file in subdirectory exists and has correct content. + content, err := os.ReadFile(filepath.Join(dest, "subdir/test.txt")) + if err != nil { + t.Fatalf("Failed to read extracted file in subdirectory: %v", err) + } + if string(content) != "subdirectory content" { + t.Fatalf("Extracted file content in subdirectory = %v, want %v", string(content), "subdirectory content") + } + }, + }, + { + name: "extract multiple files", + setupFunc: func() (string, string, func()) { + // Create a temporary directory for the test. + tempDir, err := os.MkdirTemp("", "unzip-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + // Create a zip file with multiple files. + zipPath := filepath.Join(tempDir, "test.zip") + zipFile, err := os.Create(zipPath) + if err != nil { + t.Fatalf("Failed to create zip file: %v", err) + } + + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + // Add multiple files to the zip. + files := map[string]string{ + "file1.txt": "content of file 1", + "file2.txt": "content of file 2", + "subdir/file3.txt": "content of file 3", + } + + for name, content := range files { + testFile, err := zipWriter.Create(name) + if err != nil { + t.Fatalf("Failed to create file in zip: %v", err) + } + _, err = testFile.Write([]byte(content)) + if err != nil { + t.Fatalf("Failed to write to file: %v", err) + } + } + + zipWriter.Close() + zipFile.Close() + + // Create destination directory. + destDir := filepath.Join(tempDir, "extracted") + return zipPath, destDir, func() { + os.RemoveAll(tempDir) + } + }, + verifyFunc: func(t *testing.T, dest string) { + // Verify all extracted files exist and have correct content. + expectedFiles := map[string]string{ + "file1.txt": "content of file 1", + "file2.txt": "content of file 2", + "subdir/file3.txt": "content of file 3", + } + + for path, expectedContent := range expectedFiles { + content, err := os.ReadFile(filepath.Join(dest, path)) + if err != nil { + t.Fatalf("Failed to read extracted file %s: %v", path, err) + } + if string(content) != expectedContent { + t.Fatalf("Extracted file %s content = %v, want %v", path, string(content), expectedContent) + } + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup test environment. + src, dest, cleanup := tt.setupFunc() + defer cleanup() + + // Run the test. + err := unzip(src, dest) + if err != nil { + t.Fatalf("unzip() error = %v, want nil", err) + } + + // Verify the result. + tt.verifyFunc(t, dest) + }) + } +} + +// TestUnzipFailure tests the failure scenarios of the unzip function. +func TestUnzipFailure(t *testing.T) { + tests := []struct { + name string + setupFunc func() (string, string, func()) // returns src, dest, cleanup. + expectedError string + }{ + { + name: "nonexistent source file", + setupFunc: func() (string, string, func()) { + tempDir, err := os.MkdirTemp("", "unzip-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + return "nonexistent.zip", filepath.Join(tempDir, "extracted"), func() { + os.RemoveAll(tempDir) + } + }, + expectedError: "open nonexistent.zip: no such file or directory", + }, + { + name: "invalid zip file", + setupFunc: func() (string, string, func()) { + tempDir, err := os.MkdirTemp("", "unzip-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + // Create an invalid zip file. + zipPath := filepath.Join(tempDir, "invalid.zip") + if err := os.WriteFile(zipPath, []byte("not a zip file"), 0644); err != nil { + t.Fatalf("Failed to create invalid zip file: %v", err) + } + + return zipPath, filepath.Join(tempDir, "extracted"), func() { + os.RemoveAll(tempDir) + } + }, + expectedError: "zip: not a valid zip file", + }, + { + name: "destination directory creation failure", + setupFunc: func() (string, string, func()) { + tempDir, err := os.MkdirTemp("", "unzip-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + // Create a valid zip file. + zipPath := filepath.Join(tempDir, "test.zip") + zipFile, err := os.Create(zipPath) + if err != nil { + t.Fatalf("Failed to create zip file: %v", err) + } + + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + testFile, err := zipWriter.Create("test.txt") + if err != nil { + t.Fatalf("Failed to create file in zip: %v", err) + } + _, err = testFile.Write([]byte("test content")) + if err != nil { + t.Fatalf("Failed to write to file: %v", err) + } + + zipWriter.Close() + zipFile.Close() + + // Create a file instead of a directory to cause the error. + destPath := filepath.Join(tempDir, "extracted") + if err := os.WriteFile(destPath, []byte("not a directory"), 0644); err != nil { + t.Fatalf("Failed to create file at destination: %v", err) + } + + return zipPath, destPath, func() { + os.RemoveAll(tempDir) + } + }, + expectedError: "mkdir", + }, + { + name: "file creation failure", + setupFunc: func() (string, string, func()) { + tempDir, err := os.MkdirTemp("", "unzip-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + // Create a zip file with a file that would be extracted to a read-only location. + zipPath := filepath.Join(tempDir, "test.zip") + zipFile, err := os.Create(zipPath) + if err != nil { + t.Fatalf("Failed to create zip file: %v", err) + } + + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + testFile, err := zipWriter.Create("test.txt") + if err != nil { + t.Fatalf("Failed to create file in zip: %v", err) + } + _, err = testFile.Write([]byte("test content")) + if err != nil { + t.Fatalf("Failed to write to file: %v", err) + } + + zipWriter.Close() + zipFile.Close() + + // Create a read-only directory. + destDir := filepath.Join(tempDir, "extracted") + if err := os.MkdirAll(destDir, 0555); err != nil { + t.Fatalf("Failed to create read-only directory: %v", err) + } + + return zipPath, destDir, func() { + os.RemoveAll(tempDir) + } + }, + expectedError: "permission denied", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup test environment. + src, dest, cleanup := tt.setupFunc() + defer cleanup() + + // Run the test. + err := unzip(src, dest) + if err == nil { + t.Fatalf("unzip() error = nil, want error containing %q", tt.expectedError) + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("unzip() error = %v, want error containing %q", err, tt.expectedError) + } + }) + } +} + +// TestValidateMgrCfgSuccess tests the successful scenarios of the validateMgrCfg function. +func TestValidateMgrCfgSuccess(t *testing.T) { + tests := []struct { + name string + cfg *ManagerConfig + }{ + { + name: "valid config with root path", + cfg: &ManagerConfig{ + Root: "/path/to/plugins", + RemoteRoot: "", + }, + }, + { + name: "valid config with remote root", + cfg: &ManagerConfig{ + Root: "/path/to/plugins", + RemoteRoot: "/path/to/remote/plugins.zip", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateMgrCfg(tt.cfg) + if err != nil { + t.Fatalf("validateMgrCfg() error = %v, want nil", err) + } + }) + } +} + +func TestLoadPluginSuccess(t *testing.T) { + tests := []struct { + name string + setupFunc func() (string, string, func()) // returns path, id, cleanup. + }{ + { + name: "load valid plugin", + setupFunc: func() (string, string, func()) { + // Create a temporary directory for the test. + tempDir, err := os.MkdirTemp("", "plugin-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + // Create a mock plugin file (we can't create a real .so file in tests). + pluginPath := filepath.Join(tempDir, "test-plugin.so") + if err := os.WriteFile(pluginPath, []byte("mock plugin content"), 0644); err != nil { + t.Fatalf("Failed to create mock plugin file: %v", err) + } + + return pluginPath, "test-plugin", func() { + os.RemoveAll(tempDir) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Skip the test since we can't create real .so files in tests. + t.Skip("Cannot create real .so files in tests") + + // Setup test environment. + path, id, cleanup := tt.setupFunc() + defer cleanup() + + // Run the test. + p, elapsed, err := loadPlugin(context.Background(), path, id) + if err != nil { + t.Fatalf("loadPlugin() error = %v, want nil", err) + } + if p == nil { + t.Fatal("loadPlugin() returned nil plugin") + } + if elapsed == 0 { + t.Fatal("loadPlugin() returned zero elapsed time") + } + }) + } +} + +// TestLoadPluginFailure tests the failure scenarios of the loadPlugin function. +func TestLoadPluginFailure(t *testing.T) { + tests := []struct { + name string + setupFunc func() (string, string, func()) // returns path, id, cleanup. + expectedError string + }{ + { + name: "nonexistent plugin file", + setupFunc: func() (string, string, func()) { + tempDir, err := os.MkdirTemp("", "plugin-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + return filepath.Join(tempDir, "nonexistent.so"), "nonexistent", func() { + os.RemoveAll(tempDir) + } + }, + expectedError: "failed to open plugin nonexistent: plugin.Open", + }, + { + name: "invalid plugin file", + setupFunc: func() (string, string, func()) { + tempDir, err := os.MkdirTemp("", "plugin-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + // Create an invalid plugin file. + pluginPath := filepath.Join(tempDir, "invalid.so") + if err := os.WriteFile(pluginPath, []byte("not a valid plugin"), 0644); err != nil { + t.Fatalf("Failed to create invalid plugin file: %v", err) + } + + return pluginPath, "invalid", func() { + os.RemoveAll(tempDir) + } + }, + expectedError: "failed to open plugin invalid: plugin.Open", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup test environment. + path, id, cleanup := tt.setupFunc() + defer cleanup() + + // Run the test. + p, elapsed, err := loadPlugin(context.Background(), path, id) + if err == nil { + t.Fatalf("loadPlugin() error = nil, want error containing %q", tt.expectedError) + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("loadPlugin() error = %v, want error containing %q", err, tt.expectedError) + } + if p != nil { + t.Fatal("loadPlugin() returned non-nil plugin for error case") + } + if elapsed != 0 { + t.Fatal("loadPlugin() returned non-zero elapsed time for error case") + } + }) + } +} + +// TestPluginsSuccess tests the successful scenarios of the plugins function. +func TestPluginsSuccess(t *testing.T) { + tests := []struct { + name string + setupFunc func() (*ManagerConfig, func()) // returns config and cleanup. + wantCount int + }{ + { + name: "empty directory", + setupFunc: func() (*ManagerConfig, func()) { + // Create a temporary directory for the test. + tempDir, err := os.MkdirTemp("", "plugins-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + cfg := &ManagerConfig{ + Root: tempDir, + RemoteRoot: "", + } + + return cfg, func() { + os.RemoveAll(tempDir) + } + }, + wantCount: 0, + }, + { + name: "directory with non-plugin files", + setupFunc: func() (*ManagerConfig, func()) { + // Create a temporary directory for the test. + tempDir, err := os.MkdirTemp("", "plugins-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + // Create some non-plugin files. + files := []string{ + "file1.txt", + "file2.json", + "file3.go", + } + for _, file := range files { + if err := os.WriteFile(filepath.Join(tempDir, file), []byte("test content"), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + } + + cfg := &ManagerConfig{ + Root: tempDir, + RemoteRoot: "", + } + + return cfg, func() { + os.RemoveAll(tempDir) + } + }, + wantCount: 0, + }, + { + name: "directory with subdirectories", + setupFunc: func() (*ManagerConfig, func()) { + // Create a temporary directory for the test. + tempDir, err := os.MkdirTemp("", "plugins-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + // Create some subdirectories. + dirs := []string{ + "dir1", + "dir2/subdir", + } + for _, dir := range dirs { + if err := os.MkdirAll(filepath.Join(tempDir, dir), 0755); err != nil { + t.Fatalf("Failed to create directory: %v", err) + } + } + + cfg := &ManagerConfig{ + Root: tempDir, + RemoteRoot: "", + } + + return cfg, func() { + os.RemoveAll(tempDir) + } + }, + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup test environment. + cfg, cleanup := tt.setupFunc() + defer cleanup() + + // Run the test. + got, err := plugins(context.Background(), cfg) + if err != nil { + t.Fatalf("plugins() error = %v, want nil", err) + } + if len(got) != tt.wantCount { + t.Fatalf("plugins() returned %d plugins, want %d", len(got), tt.wantCount) + } + }) + } +} + +// TestPluginsFailure tests the failure scenarios of the plugins function. +func TestPluginsFailure(t *testing.T) { + tests := []struct { + name string + setupFunc func() (*ManagerConfig, func()) // returns config and cleanup. + expectedError string + }{ + { + name: "nonexistent directory", + setupFunc: func() (*ManagerConfig, func()) { + tempDir, err := os.MkdirTemp("", "plugins-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + os.RemoveAll(tempDir) // Remove the directory to cause an error. + + cfg := &ManagerConfig{ + Root: tempDir, + RemoteRoot: "", + } + + return cfg, func() {} + }, + expectedError: "no such file or directory", + }, + { + name: "permission denied", + setupFunc: func() (*ManagerConfig, func()) { + // Create a temporary directory for the test. + tempDir, err := os.MkdirTemp("", "plugins-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + // Remove read permission from the directory. + if err := os.Chmod(tempDir, 0); err != nil { + t.Fatalf("Failed to change directory permissions: %v", err) + } + + cfg := &ManagerConfig{ + Root: tempDir, + RemoteRoot: "", + } + + return cfg, func() { + err = os.Chmod(tempDir, 0755) // Restore permissions before cleanup. + os.RemoveAll(tempDir) + } + }, + expectedError: "permission denied", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup test environment. + cfg, cleanup := tt.setupFunc() + defer cleanup() + + // Run the test. + got, err := plugins(context.Background(), cfg) + if err == nil { + t.Fatalf("plugins() error = nil, want error containing %q", tt.expectedError) + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("plugins() error = %v, want error containing %q", err, tt.expectedError) + } + if got != nil { + t.Fatal("plugins() returned non-nil map for error case") + } + }) + } +} + +// TestProviderSuccess tests the successful scenarios of the provider function. +func TestProviderSuccess(t *testing.T) { + tests := []struct { + name string + plugins map[string]onixPlugin + id string + wantType interface{} + }{ + { + name: "get publisher provider", + plugins: map[string]onixPlugin{ + "test-plugin": &mockPlugin{ + symbol: &mockPublisherProvider{ + publisher: &mockPublisher{}, + }, + err: nil, + }, + }, + id: "test-plugin", + wantType: (*definition.PublisherProvider)(nil), + }, + { + name: "get schema validator provider", + plugins: map[string]onixPlugin{ + "test-plugin": &mockPlugin{ + symbol: &mockSchemaValidatorProvider{ + validator: &mockSchemaValidator{}, + }, + err: nil, + }, + }, + id: "test-plugin", + wantType: (*definition.SchemaValidatorProvider)(nil), + }, + { + name: "get router provider", + plugins: map[string]onixPlugin{ + "test-plugin": &mockPlugin{ + symbol: &mockRouterProvider{ + router: &mockRouter{}, + }, + err: nil, + }, + }, + id: "test-plugin", + wantType: (*definition.RouterProvider)(nil), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Run the test. + switch tt.wantType.(type) { + case *definition.PublisherProvider: + got, err := provider[definition.PublisherProvider](tt.plugins, tt.id) + if err != nil { + t.Fatalf("provider() error = %v, want nil", err) + } + if got == nil { + t.Fatal("provider() returned nil provider") + } + case *definition.SchemaValidatorProvider: + got, err := provider[definition.SchemaValidatorProvider](tt.plugins, tt.id) + if err != nil { + t.Fatalf("provider() error = %v, want nil", err) + } + if got == nil { + t.Fatal("provider() returned nil provider") + } + case *definition.RouterProvider: + got, err := provider[definition.RouterProvider](tt.plugins, tt.id) + if err != nil { + t.Fatalf("provider() error = %v, want nil", err) + } + if got == nil { + t.Fatal("provider() returned nil provider") + } + default: + t.Fatalf("unsupported provider type: %T", tt.wantType) + } + }) + } +} + +// TestProviderFailure tests the failure scenarios of the provider function. +func TestProviderFailure(t *testing.T) { + tests := []struct { + name string + plugins map[string]onixPlugin + id string + wantErrMsg string + }{ + { + name: "plugin not found", + plugins: map[string]onixPlugin{}, + id: "nonexistent", + wantErrMsg: "plugin nonexistent not found", + }, + { + name: "lookup error", + plugins: map[string]onixPlugin{ + "test-plugin": &mockPlugin{ + symbol: nil, + err: errors.New("lookup failed"), + }, + }, + id: "test-plugin", + wantErrMsg: "lookup failed", + }, + { + name: "invalid provider type", + plugins: map[string]onixPlugin{ + "test-plugin": &mockPlugin{ + symbol: &struct{}{}, // Invalid type. + err: nil, + }, + }, + id: "test-plugin", + wantErrMsg: "failed to cast Provider for test-plugin", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test with PublisherProvider type. + got, err := provider[definition.PublisherProvider](tt.plugins, tt.id) + if err == nil { + t.Fatal("provider() expected error, got nil") + } + if !strings.Contains(err.Error(), tt.wantErrMsg) { + t.Fatalf("provider() error = %v, want error containing %v", err, tt.wantErrMsg) + } + if got != nil { + t.Fatal("provider() expected nil provider") + } + + // Test with SchemaValidatorProvider type. + gotValidator, err := provider[definition.SchemaValidatorProvider](tt.plugins, tt.id) + if err == nil { + t.Fatal("provider() expected error, got nil") + } + if !strings.Contains(err.Error(), tt.wantErrMsg) { + t.Fatalf("provider() error = %v, want error containing %v", err, tt.wantErrMsg) + } + if gotValidator != nil { + t.Fatal("provider() expected nil provider") + } + + // Test with RouterProvider type. + gotRouter, err := provider[definition.RouterProvider](tt.plugins, tt.id) + if err == nil { + t.Fatal("provider() expected error, got nil") + } + if !strings.Contains(err.Error(), tt.wantErrMsg) { + t.Fatalf("provider() error = %v, want error containing %v", err, tt.wantErrMsg) + } + if gotRouter != nil { + t.Fatal("provider() expected nil provider") + } + }) + } +} + +// TestManagerMiddlewareSuccess tests the successful scenarios of the Middleware method. +func TestMiddlewareSuccess(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockMiddlewareProvider + }{ + { + name: "successful middleware creation", + cfg: &Config{ + ID: "test-middleware", + Config: map[string]string{}, + }, + plugin: &mockMiddlewareProvider{ + middleware: func(h http.Handler) http.Handler { return h }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: map[string]onixPlugin{ + tt.cfg.ID: &mockPlugin{ + symbol: tt.plugin, + }, + }, + closers: []func(){}, + } + + // Call Middleware. + middleware, err := m.Middleware(context.Background(), tt.cfg) + + // Check success case. + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if middleware == nil { + t.Fatal("expected non-nil middleware, got nil") + } + }) + } +} + +// TestManagerMiddlewareFailure tests the failure scenarios of the Middleware method. +func TestMiddlewareFailure(t *testing.T) { + tests := []struct { + name string + cfg *Config + plugin *mockMiddlewareProvider + expectedError string + }{ + { + name: "provider error", + cfg: &Config{ + ID: "test-middleware", + Config: map[string]string{}, + }, + plugin: &mockMiddlewareProvider{ + err: errors.New("provider error"), + }, + expectedError: "provider error", + }, + { + name: "plugin not found", + cfg: &Config{ + ID: "nonexistent-middleware", + Config: map[string]string{}, + }, + plugin: nil, + expectedError: "plugin nonexistent-middleware not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a manager with the mock plugin. + m := &Manager{ + plugins: make(map[string]onixPlugin), + closers: []func(){}, + } + + // Only add the plugin if it's not nil. + if tt.plugin != nil { + m.plugins[tt.cfg.ID] = &mockPlugin{ + symbol: tt.plugin, + } + } + + // Call Middleware. + middleware, err := m.Middleware(context.Background(), tt.cfg) + + // Check error. + if err == nil { + t.Fatal("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.expectedError) { + t.Fatalf("error = %v, want error containing %q", err, tt.expectedError) + } + if middleware != nil { + t.Fatal("expected nil middleware, got non-nil") + } + }) + } +} diff --git a/pkg/plugin/testdata/dummy.go b/pkg/plugin/testdata/dummy.go new file mode 100644 index 0000000..31e0044 --- /dev/null +++ b/pkg/plugin/testdata/dummy.go @@ -0,0 +1,18 @@ +package main + +import ( + "context" + + "github.com/beckn/beckn-onix/pkg/plugin/definition" + "github.com/beckn/beckn-onix/pkg/plugin/implementation/encrypter" +) + +// encrypterProvider implements the definition.encrypterProvider interface. +type encrypterProvider struct{} + +func (ep encrypterProvider) New(ctx context.Context, config map[string]string) (definition.Encrypter, func() error, error) { + return encrypter.New(ctx) +} + +// Provider is the exported symbol that the plugin manager will look for. +var Provider = encrypterProvider{}