|
1 | 1 | package plugin |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "context" |
| 5 | + "io" |
4 | 6 | "testing" |
5 | 7 |
|
6 | 8 | "github.com/CrisisTextLine/modular" |
7 | 9 | "github.com/GoCodeAlone/workflow/capability" |
8 | 10 | "github.com/GoCodeAlone/workflow/config" |
| 11 | + "github.com/GoCodeAlone/workflow/deploy" |
9 | 12 | "github.com/GoCodeAlone/workflow/schema" |
10 | 13 | ) |
11 | 14 |
|
@@ -166,6 +169,158 @@ func TestPluginLoader_DuplicateModuleTypeConflict(t *testing.T) { |
166 | 169 | } |
167 | 170 | } |
168 | 171 |
|
| 172 | +func TestPluginLoader_LoadPluginWithOverride_ModuleType(t *testing.T) { |
| 173 | + loader := newTestEngineLoader() |
| 174 | + |
| 175 | + p1 := &modulePlugin{ |
| 176 | + BaseEnginePlugin: *makeEnginePlugin("builtin-plugin", "1.0.0", nil), |
| 177 | + modules: map[string]ModuleFactory{ |
| 178 | + "shared.module": func(name string, cfg map[string]any) modular.Module { |
| 179 | + return nil |
| 180 | + }, |
| 181 | + }, |
| 182 | + } |
| 183 | + p2 := &modulePlugin{ |
| 184 | + BaseEnginePlugin: *makeEnginePlugin("external-plugin", "1.0.0", nil), |
| 185 | + modules: map[string]ModuleFactory{ |
| 186 | + "shared.module": func(name string, cfg map[string]any) modular.Module { |
| 187 | + return nil |
| 188 | + }, |
| 189 | + }, |
| 190 | + } |
| 191 | + |
| 192 | + if err := loader.LoadPlugin(p1); err != nil { |
| 193 | + t.Fatalf("first load should succeed: %v", err) |
| 194 | + } |
| 195 | + // LoadPlugin should still reject duplicates. |
| 196 | + if err := loader.LoadPlugin(p2); err == nil { |
| 197 | + t.Fatal("expected duplicate module type error from LoadPlugin") |
| 198 | + } |
| 199 | + // LoadPluginWithOverride should allow replacing the type. |
| 200 | + if err := loader.LoadPluginWithOverride(p2); err != nil { |
| 201 | + t.Fatalf("LoadPluginWithOverride should succeed: %v", err) |
| 202 | + } |
| 203 | + if got := len(loader.ModuleFactories()); got != 1 { |
| 204 | + t.Errorf("expected 1 module factory after override, got %d", got) |
| 205 | + } |
| 206 | + if got := len(loader.LoadedPlugins()); got != 2 { |
| 207 | + t.Errorf("expected 2 loaded plugins, got %d", got) |
| 208 | + } |
| 209 | +} |
| 210 | + |
| 211 | +func TestPluginLoader_LoadPluginWithOverride_StepType(t *testing.T) { |
| 212 | + loader := newTestEngineLoader() |
| 213 | + |
| 214 | + p1 := &modulePlugin{ |
| 215 | + BaseEnginePlugin: *makeEnginePlugin("builtin-steps", "1.0.0", nil), |
| 216 | + steps: map[string]StepFactory{ |
| 217 | + "step.authz_check": func(name string, cfg map[string]any, _ modular.Application) (any, error) { |
| 218 | + return "builtin", nil |
| 219 | + }, |
| 220 | + }, |
| 221 | + } |
| 222 | + p2 := &modulePlugin{ |
| 223 | + BaseEnginePlugin: *makeEnginePlugin("external-authz", "1.0.0", nil), |
| 224 | + steps: map[string]StepFactory{ |
| 225 | + "step.authz_check": func(name string, cfg map[string]any, _ modular.Application) (any, error) { |
| 226 | + return "external", nil |
| 227 | + }, |
| 228 | + }, |
| 229 | + } |
| 230 | + |
| 231 | + if err := loader.LoadPlugin(p1); err != nil { |
| 232 | + t.Fatalf("first load should succeed: %v", err) |
| 233 | + } |
| 234 | + // LoadPlugin should still reject duplicate step types. |
| 235 | + if err := loader.LoadPlugin(p2); err == nil { |
| 236 | + t.Fatal("expected duplicate step type error from LoadPlugin") |
| 237 | + } |
| 238 | + if err := loader.LoadPluginWithOverride(p2); err != nil { |
| 239 | + t.Fatalf("LoadPluginWithOverride should succeed: %v", err) |
| 240 | + } |
| 241 | + |
| 242 | + // Verify the override replaced the factory. |
| 243 | + factories := loader.StepFactories() |
| 244 | + if got := len(factories); got != 1 { |
| 245 | + t.Fatalf("expected 1 step factory, got %d", got) |
| 246 | + } |
| 247 | + result, err := factories["step.authz_check"]("test", nil, nil) |
| 248 | + if err != nil { |
| 249 | + t.Fatalf("step factory returned error: %v", err) |
| 250 | + } |
| 251 | + if result != "external" { |
| 252 | + t.Errorf("expected overridden factory to return %q, got %q", "external", result) |
| 253 | + } |
| 254 | +} |
| 255 | + |
| 256 | +func TestPluginLoader_LoadPluginWithOverride_AllTypes(t *testing.T) { |
| 257 | + loader := newTestEngineLoader() |
| 258 | + |
| 259 | + p1 := &fullPlugin{ |
| 260 | + BaseEnginePlugin: *makeEnginePlugin("builtin", "1.0.0", nil), |
| 261 | + modules: map[string]ModuleFactory{ |
| 262 | + "mod.type": func(name string, cfg map[string]any) modular.Module { return nil }, |
| 263 | + }, |
| 264 | + steps: map[string]StepFactory{ |
| 265 | + "step.type": func(name string, cfg map[string]any, _ modular.Application) (any, error) { return nil, nil }, |
| 266 | + }, |
| 267 | + triggers: map[string]TriggerFactory{ |
| 268 | + "trigger.type": func() any { return nil }, |
| 269 | + }, |
| 270 | + handlers: map[string]WorkflowHandlerFactory{ |
| 271 | + "handler.type": func() any { return nil }, |
| 272 | + }, |
| 273 | + deployTargets: map[string]deploy.DeployTarget{"deploy.target": &mockDeployTarget{name: "builtin-target"}}, |
| 274 | + sidecarProviders: map[string]deploy.SidecarProvider{"sidecar.type": &mockSidecarProvider{typeName: "builtin-sidecar"}}, |
| 275 | + } |
| 276 | + p2 := &fullPlugin{ |
| 277 | + BaseEnginePlugin: *makeEnginePlugin("external", "1.0.0", nil), |
| 278 | + modules: map[string]ModuleFactory{ |
| 279 | + "mod.type": func(name string, cfg map[string]any) modular.Module { return nil }, |
| 280 | + }, |
| 281 | + steps: map[string]StepFactory{ |
| 282 | + "step.type": func(name string, cfg map[string]any, _ modular.Application) (any, error) { return nil, nil }, |
| 283 | + }, |
| 284 | + triggers: map[string]TriggerFactory{ |
| 285 | + "trigger.type": func() any { return nil }, |
| 286 | + }, |
| 287 | + handlers: map[string]WorkflowHandlerFactory{ |
| 288 | + "handler.type": func() any { return nil }, |
| 289 | + }, |
| 290 | + deployTargets: map[string]deploy.DeployTarget{"deploy.target": &mockDeployTarget{name: "external-target"}}, |
| 291 | + sidecarProviders: map[string]deploy.SidecarProvider{"sidecar.type": &mockSidecarProvider{typeName: "external-sidecar"}}, |
| 292 | + } |
| 293 | + |
| 294 | + if err := loader.LoadPlugin(p1); err != nil { |
| 295 | + t.Fatalf("first load should succeed: %v", err) |
| 296 | + } |
| 297 | + // Verify LoadPlugin rejects all duplicate types. |
| 298 | + if err := loader.LoadPlugin(p2); err == nil { |
| 299 | + t.Fatal("expected duplicate type error from LoadPlugin") |
| 300 | + } |
| 301 | + if err := loader.LoadPluginWithOverride(p2); err != nil { |
| 302 | + t.Fatalf("LoadPluginWithOverride should succeed for all types: %v", err) |
| 303 | + } |
| 304 | + if got := len(loader.ModuleFactories()); got != 1 { |
| 305 | + t.Errorf("expected 1 module factory, got %d", got) |
| 306 | + } |
| 307 | + if got := len(loader.StepFactories()); got != 1 { |
| 308 | + t.Errorf("expected 1 step factory, got %d", got) |
| 309 | + } |
| 310 | + if got := len(loader.TriggerFactories()); got != 1 { |
| 311 | + t.Errorf("expected 1 trigger factory, got %d", got) |
| 312 | + } |
| 313 | + if got := len(loader.WorkflowHandlerFactories()); got != 1 { |
| 314 | + t.Errorf("expected 1 handler factory, got %d", got) |
| 315 | + } |
| 316 | + if got := len(loader.DeployTargets()); got != 1 { |
| 317 | + t.Errorf("expected 1 deploy target, got %d", got) |
| 318 | + } |
| 319 | + if got := len(loader.SidecarProviders()); got != 1 { |
| 320 | + t.Errorf("expected 1 sidecar provider, got %d", got) |
| 321 | + } |
| 322 | +} |
| 323 | + |
169 | 324 | func TestPluginLoader_WiringHooksSortedByPriority(t *testing.T) { |
170 | 325 | loader := newTestEngineLoader() |
171 | 326 |
|
@@ -235,3 +390,54 @@ type hookPlugin struct { |
235 | 390 | } |
236 | 391 |
|
237 | 392 | func (p *hookPlugin) WiringHooks() []WiringHook { return p.hooks } |
| 393 | + |
| 394 | +// fullPlugin embeds BaseEnginePlugin and overrides all factory methods including |
| 395 | +// deploy targets and sidecar providers. |
| 396 | +type fullPlugin struct { |
| 397 | + BaseEnginePlugin |
| 398 | + modules map[string]ModuleFactory |
| 399 | + steps map[string]StepFactory |
| 400 | + triggers map[string]TriggerFactory |
| 401 | + handlers map[string]WorkflowHandlerFactory |
| 402 | + deployTargets map[string]deploy.DeployTarget |
| 403 | + sidecarProviders map[string]deploy.SidecarProvider |
| 404 | +} |
| 405 | + |
| 406 | +func (p *fullPlugin) ModuleFactories() map[string]ModuleFactory { return p.modules } |
| 407 | +func (p *fullPlugin) StepFactories() map[string]StepFactory { return p.steps } |
| 408 | +func (p *fullPlugin) TriggerFactories() map[string]TriggerFactory { return p.triggers } |
| 409 | +func (p *fullPlugin) WorkflowHandlers() map[string]WorkflowHandlerFactory { return p.handlers } |
| 410 | +func (p *fullPlugin) DeployTargets() map[string]deploy.DeployTarget { return p.deployTargets } |
| 411 | +func (p *fullPlugin) SidecarProviders() map[string]deploy.SidecarProvider { |
| 412 | + return p.sidecarProviders |
| 413 | +} |
| 414 | + |
| 415 | +// mockDeployTarget is a no-op deploy target for tests. |
| 416 | +type mockDeployTarget struct{ name string } |
| 417 | + |
| 418 | +func (m *mockDeployTarget) Name() string { return m.name } |
| 419 | +func (m *mockDeployTarget) Generate(_ context.Context, _ *deploy.DeployRequest) (*deploy.DeployArtifacts, error) { |
| 420 | + return nil, nil |
| 421 | +} |
| 422 | +func (m *mockDeployTarget) Apply(_ context.Context, _ *deploy.DeployArtifacts, _ deploy.ApplyOpts) (*deploy.DeployResult, error) { |
| 423 | + return nil, nil |
| 424 | +} |
| 425 | +func (m *mockDeployTarget) Destroy(_ context.Context, _, _ string) error { return nil } |
| 426 | +func (m *mockDeployTarget) Status(_ context.Context, _, _ string) (*deploy.DeployStatus, error) { |
| 427 | + return nil, nil |
| 428 | +} |
| 429 | +func (m *mockDeployTarget) Diff(_ context.Context, _ *deploy.DeployArtifacts) (string, error) { |
| 430 | + return "", nil |
| 431 | +} |
| 432 | +func (m *mockDeployTarget) Logs(_ context.Context, _, _ string, _ deploy.LogOpts) (io.ReadCloser, error) { |
| 433 | + return nil, nil |
| 434 | +} |
| 435 | + |
| 436 | +// mockSidecarProvider is a no-op sidecar provider for tests. |
| 437 | +type mockSidecarProvider struct{ typeName string } |
| 438 | + |
| 439 | +func (m *mockSidecarProvider) Type() string { return m.typeName } |
| 440 | +func (m *mockSidecarProvider) Validate(_ config.SidecarConfig) error { return nil } |
| 441 | +func (m *mockSidecarProvider) Resolve(_ config.SidecarConfig, _ string) (*deploy.SidecarSpec, error) { |
| 442 | + return nil, nil |
| 443 | +} |
0 commit comments