// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may not // use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, // either express or implied. See the License for the specific language governing // permissions and limitations under the License. //go:build freebsd || linux || netbsd || openbsd // +build freebsd linux netbsd openbsd // Package runpluginutil run plugin utility functions without referencing the actually plugin impl packages package runpluginutil import ( "fmt" "testing" "time" "github.com/aws/amazon-ssm-agent/agent/appconfig" "github.com/aws/amazon-ssm-agent/agent/context" "github.com/aws/amazon-ssm-agent/agent/contracts" "github.com/aws/amazon-ssm-agent/agent/log" contextmocks "github.com/aws/amazon-ssm-agent/agent/mocks/context" "github.com/aws/amazon-ssm-agent/agent/task" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) const ( testPlugin0 = "plugin0" testPlugin1 = "plugin1" testPlugin2 = "plugin2" testUnknownPlugin = "plugin3" testUnsupportedPlugin = "plugin4" ) var origIsSupported func(log log.T, pluginName string) (isKnown bool, isSupported bool, message string) func setIsSupportedMock() { origIsSupported = isSupportedPlugin isSupportedPlugin = func(log log.T, pluginName string) (isKnown bool, isSupported bool, message string) { switch pluginName { case testUnknownPlugin: return false, true, "" case testUnsupportedPlugin: return true, false, "" default: return true, true, "" } } } func restoreIsSupported() { isSupportedPlugin = origIsSupported } // TestRunPlugins tests that RunPluginsWithRegistry calls all the expected plugins. func TestRunPluginsWithNewDocument(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) ioConfig := contracts.IOConfiguration{} for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ PluginID: name, PluginName: name, StartDateTime: defaultTime, EndDateTime: defaultTime, Output: "", } pluginInstances[name].On("Execute", pluginConfigs[name].Configuration, cancelFlag, mock.Anything).Return() pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) close(ch) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Document with steps containing unknown plugin (i.e. when plugin handler is not found), steps must fail func TestRunPluginsWithMissingPluginHandler(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) ioConfig := contracts.IOConfiguration{} for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginError := fmt.Sprintf("Plugin with name %s not found. Step name: %s", name, name) pluginResults[name] = &contracts.PluginResult{ PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusFailed, Error: pluginError, } pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // TODO cancelFlag should not fail subsequent plugins func TestRunPluginsWithCancelFlagShutdown(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginStates := make([]contracts.PluginState, 2) pluginResults := make(map[string]*contracts.PluginResult) plugins := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() ioConfig := contracts.IOConfiguration{} for index, name := range pluginNames { plugins[name] = new(PluginMock) config := contracts.Configuration{ PluginID: name, PluginName: name, UpstreamServiceName: contracts.MessageGatewayService, } pluginState := contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "", PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, } if name == testPlugin1 { plugins[name].On("Execute", pluginState.Configuration, cancelFlag, mock.Anything).Run(func(args mock.Arguments) { flag := args.Get(1).(task.CancelFlag) flag.Set(task.ShutDown) }).Return() } else { plugins[name].On("Execute", pluginState.Configuration, cancelFlag, mock.Anything).Return() } pluginStates[index] = pluginState pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(plugins[name], nil) pluginRegistry[name] = pluginFactory } ch := make(chan contracts.PluginResult, 2) outputs := RunPlugins(ctx, pluginStates, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) close(ch) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } for _, mockPlugin := range plugins { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) //empty struct assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) } func TestRunPluginsWithInProgressDocuments(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginStates := make([]contracts.PluginState, 2) pluginResults := make(map[string]*contracts.PluginResult) plugins := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{ OrchestrationDirectory: "test", } var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() config := appconfig.SsmagentConfig{} config.Ssm.OrchestrationDirectoryCleanup = appconfig.OrchestrationDirCleanupForSuccessFailedCommand var ctx = contextmocks.NewMockDefaultWithConfig(config) defaultTime := time.Now() for index, name := range pluginNames { plugins[name] = new(PluginMock) config := contracts.Configuration{ PluginID: name, PluginName: name, UpstreamServiceName: contracts.MessageGatewayService, } pluginState := contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "", Status: contracts.ResultStatusSuccess, PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, } //plugin1 has already been executed, plugin2 has not started yet if name == testPlugin1 { pluginState.Result = *pluginResults[name] } else { pluginState.Result.Status = contracts.ResultStatusNotStarted plugins[name].On("Execute", pluginState.Configuration, cancelFlag, mock.Anything).Return() } pluginStates[index] = pluginState pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(plugins[name], nil) pluginRegistry[name] = pluginFactory } ch := make(chan contracts.PluginResult, 2) // Not Deletion case - ResultStatusNotStarted var deleteDirectoryFlag bool deleteDirectoryRef = func(dirName string) (err error) { deleteDirectoryFlag = true return nil } outputs := RunPlugins(ctx, pluginStates, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) close(ch) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } for _, mockPlugin := range plugins { mockPlugin.AssertExpectations(t) } pluginResults[testPlugin2].Status = "" assert.False(t, deleteDirectoryFlag) assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) } //TODO this test wont work cuz we don't have a good way to mock lib functions //func TestEngineUnhandledPlugins(t *testing.T) { // pluginName := "nonexited_plugin" // pluginStates := make([]contracts.PluginState, 1) // pluginResults := make(map[string]*contracts.PluginResult) // plugins := make(map[string]*PluginMock) // pluginRegistry := PluginRegistry{} // // var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() // ctx := context.NewMockDefault() // defaultTime := time.Now() // // for index, name := range pluginNames { // plugins[name] = new(PluginMock) // config := contracts.Configuration{ // PluginID: name, // } // pluginState := contracts.PluginState{ // Name: name, // Id: name, // Configuration: config, // } // // pluginResults[pluginName] = &contracts.PluginResult{ // Status: contracts.ResultStatusFailed, // PluginName: pluginName, // } // pluginStates[0] = pluginState // outputs := (ctx, pluginStates, pluginRegistry, sendResponse, nil, cancelFlag) // plugins[pluginName].AssertExpectations(t) // assert.Equal(t, pluginResults[pluginName], outputs[pluginName]) //} func TestRunPluginsWithDuplicatePluginType(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginType := "aws:runShellScript" pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) // create an instance of our test object plugin := new(PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag ctx := contextmocks.NewMockDefault() defaultTime := time.Now() pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) for index, name := range pluginNames { // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: pluginType, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "", PluginID: name, PluginName: pluginType, StartDateTime: defaultTime, EndDateTime: defaultTime, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(plugin, nil) plugin.On("Execute", pluginConfigs[name].Configuration, cancelFlag, mock.Anything).Return(*pluginResults[name]) pluginRegistry[pluginType] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met plugin.AssertExpectations(t) ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginType, outputs[testPlugin1].PluginName) assert.Equal(t, pluginType, outputs[testPlugin2].PluginName) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with compatible precondition, steps must be executed // Precondition = "StringEquals": ["platformType", "Linux"] func TestRunPluginsWithCompatiblePlatformPrecondition(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["platformType", "Linux"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "platformType", ResolvedArgumentValue: "platformType", }, contracts.PreconditionArgument{ InitialArgumentValue: "Linux", ResolvedArgumentValue: "Linux", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "", PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginInstances[name].On("Execute", pluginConfigs[name].Configuration, cancelFlag, mock.Anything).Return() pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with compatible precondition, steps must be executed // Precondition = "StringEquals": ["Linux", "platformType"] func TestRunPluginsWithCompatiblePlatformPreconditionWithValueFirst(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["Linux", "platformType"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "Linux", ResolvedArgumentValue: "Linux", }, contracts.PreconditionArgument{ InitialArgumentValue: "platformType", ResolvedArgumentValue: "platformType", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "", PluginID: name, PluginName: name, StartDateTime: defaultTime, EndDateTime: defaultTime, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginInstances[name].On("Execute", pluginConfigs[name].Configuration, cancelFlag, mock.Anything).Return() pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with incompatible platform precondition, steps must be skipped // Precondition = "StringEquals": ["platformType", "Windows"] func TestRunPluginsWithIncompatiblePlatformPrecondition(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{ OrchestrationDirectory: "test", } var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() config := appconfig.SsmagentConfig{} config.Ssm.OrchestrationDirectoryCleanup = appconfig.OrchestrationDirCleanupForSuccessFailedCommand var ctx = contextmocks.NewMockDefaultWithConfig(config) defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["platformType", "Windows"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "platformType", ResolvedArgumentValue: "platformType", }, contracts.PreconditionArgument{ InitialArgumentValue: "Windows", ResolvedArgumentValue: "Windows", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "Step execution skipped due to unsatisfied preconditions: '\"StringEquals\": [platformType, Windows]'. Step name: " + name, PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusSkipped, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // Deletion case - ResultStatusSkipped var deleteDirectoryFlag bool deleteDirectoryRef = func(dirName string) (err error) { deleteDirectoryFlag = true return nil } // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.True(t, deleteDirectoryFlag) assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document preconditions must be forward-compatible for future platform types // Preconditions with such OS types will be skipped for now // Precondition = "StringEquals": ["platformType", "FutureOS"] func TestRunPluginsWithFuturePlatformPrecondition(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["platformType", "FutureOS"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "platformType", ResolvedArgumentValue: "platformType", }, contracts.PreconditionArgument{ InitialArgumentValue: "FutureOS", ResolvedArgumentValue: "FutureOS", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "Step execution skipped due to unsatisfied preconditions: '\"StringEquals\": [platformType, FutureOS]'. Step name: " + name, PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusSkipped, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with unknown plugin (i.e. when plugin handler is not found), steps must be skipped func TestRunPluginsWithCompatiblePreconditionButMissingPluginHandler(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testUnsupportedPlugin, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["platformType", "Linux"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "platformType", ResolvedArgumentValue: "platformType", }, contracts.PreconditionArgument{ InitialArgumentValue: "Linux", ResolvedArgumentValue: "Linux", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } if name == testUnsupportedPlugin { pluginResults[name] = &contracts.PluginResult{ Output: "Step execution skipped due to unsupported plugin: " + name + ". Step name: " + name, PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusSkipped, } } else { pluginResults[name] = &contracts.PluginResult{ Output: "", PluginID: name, PluginName: name, StartDateTime: defaultTime, EndDateTime: defaultTime, } pluginInstances[name].On("Execute", pluginConfigs[name].Configuration, cancelFlag, mock.Anything).Return(*pluginResults[name]) } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called > 2 { assert.Fail(t, "there shouldn't be more than 3 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testUnsupportedPlugin], outputs[testUnsupportedPlugin]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with more than 1 precondition, steps must fail func TestRunPluginsWithMoreThanOnePrecondition(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{ OrchestrationDirectory: "test", } var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() config := appconfig.SsmagentConfig{} config.Ssm.OrchestrationDirectoryCleanup = appconfig.OrchestrationDirCleanupForSuccessFailedCommand var ctx = contextmocks.NewMockDefaultWithConfig(config) defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: {"StringEquals": ["Linux", "platformType"], "foo": ["operand1", "operand2"]} parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "Linux", ResolvedArgumentValue: "Linux", }, contracts.PreconditionArgument{ InitialArgumentValue: "platformType", ResolvedArgumentValue: "platformType", }, }, "foo": { contracts.PreconditionArgument{ InitialArgumentValue: "operand1", ResolvedArgumentValue: "operand1", }, contracts.PreconditionArgument{ InitialArgumentValue: "operand2", ResolvedArgumentValue: "operand2", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginError := fmt.Sprintf( "Unrecognized precondition(s): 'unrecognized operator: \"foo\"', please update agent to latest version. Step name: %s", name) pluginResults[name] = &contracts.PluginResult{ PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusFailed, Error: pluginError, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // Deletion case - ResultStatusFailed var deleteDirectoryFlag bool deleteDirectoryRef = func(dirName string) (err error) { deleteDirectoryFlag = true return nil } // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.True(t, deleteDirectoryFlag) assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with unrecognized precondition operator, steps must fail func TestRunPluginsWithUnrecognizedPreconditionOperator(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "foo": ["platformType", "Linux"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "foo": { contracts.PreconditionArgument{ InitialArgumentValue: "platformType", ResolvedArgumentValue: "platformType", }, contracts.PreconditionArgument{ InitialArgumentValue: "Linux", ResolvedArgumentValue: "Linux", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginError := fmt.Sprintf( "Unrecognized precondition(s): 'unrecognized operator: \"foo\"', please update agent to latest version. Step name: %s", name) pluginResults[name] = &contracts.PluginResult{ PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusFailed, Error: pluginError, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with invalid precondition, steps must fail // Precondition: "StringEquals": ["platformType", "platformType"] func TestRunPluginsWithUnrecognizedPreconditionDuplicateVariable(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["platformType", "platformType"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "platformType", ResolvedArgumentValue: "platformType", }, contracts.PreconditionArgument{ InitialArgumentValue: "platformType", ResolvedArgumentValue: "platformType", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginError := fmt.Sprintf( "Unrecognized precondition(s): '\"StringEquals\": [platformType platformType]', please update agent to latest version. Step name: %s", name) pluginResults[name] = &contracts.PluginResult{ PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusFailed, Error: pluginError, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with invalid precondition, steps must fail // Precondition: "StringEquals": ["{{ paramName }}", "{{ paramName }}"] func TestRunPluginsWithUnrecognizedPreconditionDuplicateParameter(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["{{ paramName }}", "{{ paramName }}"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "{{ paramName }}", ResolvedArgumentValue: "paramValue", }, contracts.PreconditionArgument{ InitialArgumentValue: "{{ paramName }}", ResolvedArgumentValue: "paramValue", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginError := fmt.Sprintf( "Unrecognized precondition(s): '\"StringEquals\": operator's arguments can't be identical', please update agent to latest version. Step name: %s", name) pluginResults[name] = &contracts.PluginResult{ PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusFailed, Error: pluginError, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with invalid precondition, steps must fail // Precondition: "StringEquals": ["foo", "foo"] func TestRunPluginsWithUnrecognizedPreconditionDuplicateConstant(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["foo", "foo"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "foo", ResolvedArgumentValue: "foo", }, contracts.PreconditionArgument{ InitialArgumentValue: "foo", ResolvedArgumentValue: "foo", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginError := fmt.Sprintf( "Unrecognized precondition(s): '\"StringEquals\": operator's arguments can't be identical', please update agent to latest version. Step name: %s", name) pluginResults[name] = &contracts.PluginResult{ PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusFailed, Error: pluginError, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with invalid precondition, steps must fail // Precondition: "StringEquals": ["{{ ssm:parameter }}", "bar"] func TestRunPluginsWithUnrecognizedPreconditionSSMParameter(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["{{ ssm:parameter }}", "bar"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "{{ ssm:parameter }}", ResolvedArgumentValue: "{{ ssm:parameter }}", }, contracts.PreconditionArgument{ InitialArgumentValue: "bar", ResolvedArgumentValue: "bar", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginError := fmt.Sprintf( "Unrecognized precondition(s): '\"StringEquals\": operator's arguments can't contain SSM parameters', please update agent to latest version. Step name: %s", name) pluginResults[name] = &contracts.PluginResult{ PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusFailed, Error: pluginError, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with invalid precondition, steps must fail // Precondition: "StringEquals": ["{{ ssm-secure:parameter }}", "bar"] func TestRunPluginsWithUnrecognizedPreconditionSecureSSMParameter(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["{{ ssm-secure:parameter }}", "bar"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "{{ ssm-secure:parameter }}", ResolvedArgumentValue: "{{ ssm-secure:parameter }}", }, contracts.PreconditionArgument{ InitialArgumentValue: "bar", ResolvedArgumentValue: "bar", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginError := fmt.Sprintf( "Unrecognized precondition(s): '\"StringEquals\": operator's arguments can't contain secure SSM parameters', please update agent to latest version. Step name: %s", name) pluginResults[name] = &contracts.PluginResult{ PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusFailed, Error: pluginError, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with invalid precondition, steps must fail // Precondition: "StringEquals": ["foo", "bar"] func TestRunPluginsWithUnrecognizedPreconditionNoDocumentParameters(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["foo", "bar"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "foo", ResolvedArgumentValue: "foo", }, contracts.PreconditionArgument{ InitialArgumentValue: "bar", ResolvedArgumentValue: "bar", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginError := fmt.Sprintf( "Unrecognized precondition(s): '\"StringEquals\": at least one of operator's arguments must contain a valid document parameter', please update agent to latest version. Step name: %s", name) pluginResults[name] = &contracts.PluginResult{ PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusFailed, Error: pluginError, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with invalid precondition, steps must fail // Precondition: "StringEquals": ["{{ unknown }} foo", "{{ unknown }} foo"] func TestRunPluginsWithUnrecognizedPreconditionUnrecognizedParameter(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["{{ unknown }}", "bar"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "{{ unknown }}", ResolvedArgumentValue: "{{ unknown }}", }, contracts.PreconditionArgument{ InitialArgumentValue: "bar", ResolvedArgumentValue: "bar", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginError := fmt.Sprintf( "Unrecognized precondition(s): '\"StringEquals\": at least one of operator's arguments must contain a valid document parameter', please update agent to latest version. Step name: %s", name) pluginResults[name] = &contracts.PluginResult{ PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusFailed, Error: pluginError, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with platformType and document parameters should fail // Precondition: "StringEquals": ["platformType", "{{ platformValue }}=Linux"] func TestRunPluginsPlatformPreconditionWithDocumentParameters(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["platformType", "{{ platformValue }}"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "platformType", ResolvedArgumentValue: "platformType", }, contracts.PreconditionArgument{ InitialArgumentValue: "{{ platformValue }}", ResolvedArgumentValue: "Linux", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginError := fmt.Sprintf( "Unrecognized precondition(s): '\"StringEquals\": the second argument for the platformType variable can't contain document parameters', please update agent to latest version. Step name: %s", name) pluginResults[name] = &contracts.PluginResult{ PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusFailed, Error: pluginError, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with compatible precondition, steps must be executed // Precondition = "StringEquals": ["{{ param1 }}", "{{ param2 }}"] func TestRunPluginsWithCompatibleParamParamPrecondition(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["{{ param1 }}", "{{ param2 }}"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "{{ param1 }}", ResolvedArgumentValue: "foo", }, contracts.PreconditionArgument{ InitialArgumentValue: "{{ param2 }}", ResolvedArgumentValue: "foo", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "", PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginInstances[name].On("Execute", pluginConfigs[name].Configuration, cancelFlag, mock.Anything).Return() pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with incompatible platform precondition, steps must be skipped // Precondition = "StringEquals": ["{{ param1 }}", "{{ param2 }}"] func TestRunPluginsWithIncompatibleParamParamPrecondition(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{ OrchestrationDirectory: "test", } var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() config := appconfig.SsmagentConfig{} config.Ssm.OrchestrationDirectoryCleanup = appconfig.OrchestrationDirCleanupForSuccessFailedCommand var ctx = contextmocks.NewMockDefaultWithConfig(config) defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["{{ param1 }}", "{{ param2 }}"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "{{ param1 }}", ResolvedArgumentValue: "foo", }, contracts.PreconditionArgument{ InitialArgumentValue: "{{ param2 }}", ResolvedArgumentValue: "bar", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "Step execution skipped due to unsatisfied preconditions: '\"StringEquals\": [{{ param1 }}, {{ param2 }}]'. Step name: " + name, PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusSkipped, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // Deletion case - ResultStatusSkipped var deleteDirectoryFlag bool deleteDirectoryRef = func(dirName string) (err error) { deleteDirectoryFlag = true return nil } // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.True(t, deleteDirectoryFlag) assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with compatible precondition, steps must be executed // Precondition = "StringEquals": ["{{ param }}", "foo"] func TestRunPluginsWithCompatibleParamValuePrecondition(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["{{ param }}", "foo"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "{{ param }}", ResolvedArgumentValue: "", }, contracts.PreconditionArgument{ InitialArgumentValue: "", ResolvedArgumentValue: "", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "", PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginInstances[name].On("Execute", pluginConfigs[name].Configuration, cancelFlag, mock.Anything).Return() pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with compatible precondition, steps must be executed // Precondition = "StringEquals": ["foo", "{{ param }}"] func TestRunPluginsWithCompatibleValueParamPrecondition(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["foo", "{{ param }}"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "foo", ResolvedArgumentValue: "foo", }, contracts.PreconditionArgument{ InitialArgumentValue: "{{ param1 }}", ResolvedArgumentValue: "foo", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "", PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginInstances[name].On("Execute", pluginConfigs[name].Configuration, cancelFlag, mock.Anything).Return() pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with incompatible platform precondition, steps must be skipped // Precondition = "StringEquals": ["{{ param }}", "bar}"] func TestRunPluginsWithIncompatibleParamValuePrecondition(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["{{ param }}", "bar"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "{{ param }}", ResolvedArgumentValue: "foo", }, contracts.PreconditionArgument{ InitialArgumentValue: "bar", ResolvedArgumentValue: "bar", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "Step execution skipped due to unsatisfied preconditions: '\"StringEquals\": [{{ param }}, bar]'. Step name: " + name, PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusSkipped, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with compatible precondition, steps must be executed // Precondition = "StringEquals": ["{{ param1 }} bar {{ not a param }}", "foo {{ param2 }} {{ not a param }}"] func TestRunPluginsWithCompatibleMixedPrecondition(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["{{ param1 }} bar {{ not a param }}", "foo {{ param2 }} {{ not a param }}"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "{{ param1 }} bar {{ not a param }}", ResolvedArgumentValue: "foo bar {{ not a param }}", }, contracts.PreconditionArgument{ InitialArgumentValue: "foo {{ param2 }} {{ not a param }}", ResolvedArgumentValue: "foo bar {{ not a param }}", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "", PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginInstances[name].On("Execute", pluginConfigs[name].Configuration, cancelFlag, mock.Anything).Return() pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with incompatible precondition, steps must be skipped // Precondition = "StringEquals": ["{{ param1 }} bar {{ not a param }}", "foo {{ param2 }} {{ wrong not a param }}"] func TestRunPluginsWithIncompatibleMixedPrecondition(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["{{ param1 }} bar {{ not a param }}", "{{ param2 }}"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "{{ param1 }} bar {{ not a param }}", ResolvedArgumentValue: "foo bar {{ not a param }}", }, contracts.PreconditionArgument{ InitialArgumentValue: "foo {{ param2 }} {{ wrong not a param }}", ResolvedArgumentValue: "foo bar {{ wrong not a param }}", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ Output: "Step execution skipped due to unsatisfied preconditions: '\"StringEquals\": [{{ param1 }} bar {{ not a param }}, foo {{ param2 }} {{ wrong not a param }}]'. Step name: " + name, PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusSkipped, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with more than 2 precondition operands, steps must fail func TestRunPluginsWithMoreThanTwoPreconditionOperands(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["platformType", "Linux", "foo"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "platformType", ResolvedArgumentValue: "platformType", }, contracts.PreconditionArgument{ InitialArgumentValue: "Linux", ResolvedArgumentValue: "Linux", }, contracts.PreconditionArgument{ InitialArgumentValue: "foo", ResolvedArgumentValue: "foo", }, }, } for index, name := range pluginNames { // create an instance of our test object pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginError := fmt.Sprintf( "Unrecognized precondition(s): '\"StringEquals\": operator accepts exactly 2 arguments', please update agent to latest version. Step name: %s", name) pluginResults[name] = &contracts.PluginResult{ PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusFailed, Error: pluginError, } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else if called == 1 { assert.Equal(t, result, *pluginResults[testPlugin2]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } // Crossplatform document with unknown plugin, steps must fail func TestRunPluginsWithUnknownPlugin(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testUnknownPlugin, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} ioConfig := contracts.IOConfiguration{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() defaultOutput := "" pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) // initial precondition: "StringEquals": ["platformType", "Linux"] parsedPreconditions := map[string][]contracts.PreconditionArgument{ "StringEquals": { contracts.PreconditionArgument{ InitialArgumentValue: "platformType", ResolvedArgumentValue: "platformType", }, contracts.PreconditionArgument{ InitialArgumentValue: "Linux", ResolvedArgumentValue: "Linux", }, }, } for index, name := range pluginNames { // create an instance of our test object, but not if it is an unknown plugin if name != testUnknownPlugin { pluginInstances[name] = new(PluginMock) } // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, IsPreconditionEnabled: true, Preconditions: parsedPreconditions, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } if name == testUnknownPlugin { pluginError := fmt.Sprintf( "Plugin with name %s is not supported by this version of ssm agent, please update to latest version. Step name: %s", name, name) pluginResults[name] = &contracts.PluginResult{ PluginName: name, PluginID: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: defaultOutput, StandardError: defaultOutput, Status: contracts.ResultStatusFailed, Error: pluginError, } } else { pluginResults[name] = &contracts.PluginResult{ Output: "", PluginID: name, PluginName: name, StartDateTime: defaultTime, EndDateTime: defaultTime, } pluginInstances[name].On("Execute", pluginConfigs[name].Configuration, cancelFlag, mock.Anything).Return(*pluginResults[name]) } pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called > 2 { assert.Fail(t, "there shouldn't be more than 3 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) assert.Equal(t, pluginResults[testUnknownPlugin], outputs[testUnknownPlugin]) assert.Equal(t, pluginResults[testPlugin2], outputs[testPlugin2]) assert.Equal(t, pluginResults, outputs) } func TestRunPluginSuccessWithNonTruncatedResult(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) ioConfig := contracts.IOConfiguration{} for index, name := range pluginNames { // create mock plugin instance for testing pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginResults[name] = &contracts.PluginResult{ PluginID: name, PluginName: name, StartDateTime: defaultTime, EndDateTime: defaultTime, StandardOutput: "", } pluginInstances[name].On("Execute", pluginConfigs[name].Configuration, cancelFlag, mock.Anything).Return() pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } ch := make(chan contracts.PluginResult) defer func() { close(ch) }() go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } for pluginID, output := range outputs { assert.Equal(t, pluginResults[pluginID].StandardOutput, output.StandardOutput) } } func TestRunPluginSetsUpstreamServiceNameInEachPlugin(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1, testPlugin2} pluginConfigs := make(map[string]contracts.PluginState) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} var cancelFlag task.CancelFlag = task.NewChanneledCancelFlag() ctx := contextmocks.NewMockDefault() defaultTime := time.Now() pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) ioConfig := contracts.IOConfiguration{} for index, name := range pluginNames { // create mock plugin instance for testing pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, UpstreamServiceName: contracts.MessageDeliveryService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } pluginInstances[name].On("Execute", pluginConfigs[name].Configuration, cancelFlag, mock.Anything).Return() pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } ch := make(chan contracts.PluginResult) defer func() { close(ch) }() go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } }() RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageDeliveryService, pluginRegistry, ch, cancelFlag) // assert that the expectations were met // assert that the expectations were met for _, mockPlugin := range pluginInstances { mockPlugin.AssertExpectations(t) } } func TestGetStepNameV1Documents(t *testing.T) { inputPluginName := "testPluginName1" testProperties := make(map[string]string) PropID := "0.aws:plugin1" testProperties["id"] = PropID config := contracts.Configuration{ PluginID: testPlugin1, PluginName: testPlugin1, Properties: testProperties, } output, err := getStepName(inputPluginName, config) assert.Equal(t, err, nil) assert.Equal(t, output, PropID) } func TestGetStepNameV2Documents(t *testing.T) { inputPluginName := "testPluginName1" config := contracts.Configuration{ PluginID: "PluginID", PluginName: testPlugin1, } output, err := getStepName(inputPluginName, config) assert.Equal(t, err, nil) assert.Equal(t, output, config.PluginID) } func TestGetStepNameCloudWatchDocument(t *testing.T) { inputPluginName := "aws:cloudWatch" config := contracts.Configuration{ PluginID: testPlugin1, PluginName: testPlugin1, } output, err := getStepName(inputPluginName, config) assert.Equal(t, err, nil) assert.Equal(t, output, inputPluginName) } func TestGetStepNameError(t *testing.T) { inputPluginName := "error test" config := contracts.Configuration{ PluginID: testPlugin1, PluginName: testPlugin1, } _, err := getStepName(inputPluginName, config) assert.Nil(t, err) } func TestGetShouldPluginSkipBasedOnControlFlow(t *testing.T) { pluginNames := []string{testPlugin0, testPlugin1, testPlugin2, testUnknownPlugin} plugins := make([]contracts.PluginState, len(pluginNames)) pluginResults := make(map[string]*contracts.PluginResult) testProperties := make([]interface{}, len(pluginNames)) config := make([]contracts.Configuration, len(pluginNames)) ctx := contextmocks.NewMockDefault() defaultStatus := contracts.ResultStatusSuccess defaultStatus2 := contracts.ResultStatusSuccess pluginCode := []int{0, 0, 1, 168, 169} pluginCode2 := []int{1, 0, 168, 169, 0} // plugin config properties testProperties[0] = map[string]interface{}{ contracts.OnFailureModifier: "exit", } testProperties[1] = map[string]interface{}{ contracts.OnSuccessModifier: "exit", contracts.FinallyStepModifier: "false", } testProperties[2] = map[string]interface{}{ contracts.OnFailureModifier: "exit", contracts.FinallyStepModifier: "true", } testProperties[3] = map[string]interface{}{ contracts.OnFailureModifier: "exit", contracts.FinallyStepModifier: "true", } for index, name := range pluginNames { config[index] = contracts.Configuration{ PluginID: name, PluginName: name, Properties: testProperties[index], } plugins[index] = contracts.PluginState{ Name: name, Id: name, Configuration: config[index], } } for index, code := range pluginCode { if code == 1 || code == 169 { defaultStatus = contracts.ResultStatusFailed } else { defaultStatus = contracts.ResultStatusSuccess } pluginResults[testPlugin0] = &contracts.PluginResult{ PluginID: testPlugin0, PluginName: testPlugin0, Status: defaultStatus, Code: code, } // plugin 1 if pluginCode2[index] == 1 || pluginCode2[index] == 169 { defaultStatus2 = contracts.ResultStatusFailed } else { defaultStatus2 = contracts.ResultStatusSuccess } pluginResults[testPlugin1] = &contracts.PluginResult{ PluginID: testPlugin1, PluginName: testPlugin1, Status: defaultStatus2, Code: pluginCode2[index], } // plugin 2 pluginResults[testPlugin2] = &contracts.PluginResult{ PluginID: testPlugin2, PluginName: testPlugin2, Status: contracts.ResultStatusSuccess, Code: 0, } // plugin testUnknownPlugin pluginResults[testUnknownPlugin] = &contracts.PluginResult{ PluginID: testUnknownPlugin, PluginName: testUnknownPlugin, Status: contracts.ResultStatusSuccess, Code: 0, } shouldPluginSkipBasedOnControlFlow0 := getShouldPluginSkipBasedOnControlFlow(ctx, plugins, 0, pluginResults) shouldPluginSkipBasedOnControlFlow1 := getShouldPluginSkipBasedOnControlFlow(ctx, plugins, 1, pluginResults) shouldPluginSkipBasedOnControlFlow2 := getShouldPluginSkipBasedOnControlFlow(ctx, plugins, 2, pluginResults) shouldPluginSkipBasedOnControlFlow3 := getShouldPluginSkipBasedOnControlFlow(ctx, plugins, 3, pluginResults) assert.Equal(t, shouldPluginSkipBasedOnControlFlow0, false) assert.Equal(t, shouldPluginSkipBasedOnControlFlow3, false) // test getShouldPluginSkipBasedOnControlFlow if index == 0 { assert.Equal(t, shouldPluginSkipBasedOnControlFlow1, false) assert.Equal(t, shouldPluginSkipBasedOnControlFlow2, false) } else if index == 1 { assert.Equal(t, shouldPluginSkipBasedOnControlFlow1, false) assert.Equal(t, shouldPluginSkipBasedOnControlFlow2, true) } else { assert.Equal(t, shouldPluginSkipBasedOnControlFlow1, true) assert.Equal(t, shouldPluginSkipBasedOnControlFlow2, true) } } } func TestGetStringPropByName(t *testing.T) { testProperties := map[string]interface{}{ contracts.OnFailureModifier: "exit", contracts.OnSuccessModifier: "exit", contracts.FinallyStepModifier: "true", } config := contracts.Configuration{ PluginID: testPlugin1, PluginName: testPlugin1, Properties: testProperties, } pluginState := contracts.PluginState{ Name: testPlugin1, Id: testPlugin1, Configuration: config, } // test getStringPropByName onFailureOutput := getStringPropByName(config.Properties, contracts.OnFailureModifier) onSuccessOutput := getStringPropByName(config.Properties, contracts.OnSuccessModifier) finallyStepOutput := getStringPropByName(pluginState.Configuration.Properties, contracts.FinallyStepModifier) assert.Equal(t, onFailureOutput, "exit") assert.Equal(t, onSuccessOutput, "exit") assert.Equal(t, finallyStepOutput, "true") } func TestRunPluginWithOnFailureProperty168(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} var cancelFlag task.CancelFlag ctx := contextmocks.NewMockDefault() defaultTime := time.Now() ioConfig := contracts.IOConfiguration{} pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) for index, name := range pluginNames { // create mock plugin instance for testing pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } standardOutput := "\nStep exited with code 168. Therefore, marking step as succeeded. Further document steps will be skipped." standardError := "" defaultCode := contracts.ExitWithSuccess defaultStatus := contracts.ResultStatusSuccess outputMessage := "" pluginResults[name] = &contracts.PluginResult{ PluginID: name, PluginName: name, StartDateTime: defaultTime, EndDateTime: defaultTime, Status: defaultStatus, Code: defaultCode, StandardOutput: standardOutput, StandardError: standardError, Output: outputMessage, } oldRunPlugin := runPlugin runPlugin = func(context context.T, factory PluginFactory, pluginName string, config contracts.Configuration, cancelFlag task.CancelFlag, ioConfig contracts.IOConfiguration, ) (res contracts.PluginResult) { res.Code = defaultCode res.Status = defaultStatus res.Output = "" return } defer func() { runPlugin = oldRunPlugin }() pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) close(ch) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) } func TestRunPluginWithOnFailureProperty169(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} var cancelFlag task.CancelFlag ctx := contextmocks.NewMockDefault() defaultTime := time.Now() ioConfig := contracts.IOConfiguration{} pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) for index, name := range pluginNames { // create mock plugin instance for testing pluginInstances[name] = new(PluginMock) // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, UpstreamServiceName: contracts.MessageGatewayService, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } standardOutput := "\nStep exited with code 169. Therefore, marking step as Failed. Further document steps will be skipped." standardError := standardOutput defaultCode := contracts.ExitWithFailure defaultStatus := contracts.ResultStatusFailed outputMessage := "" pluginResults[name] = &contracts.PluginResult{ PluginID: name, PluginName: name, StartDateTime: defaultTime, EndDateTime: defaultTime, Status: defaultStatus, Code: defaultCode, StandardOutput: standardOutput, StandardError: standardError, Output: outputMessage, } oldRunPlugin := runPlugin runPlugin = func(context context.T, factory PluginFactory, pluginName string, config contracts.Configuration, cancelFlag task.CancelFlag, ioConfig contracts.IOConfiguration, ) (res contracts.PluginResult) { res.Code = defaultCode res.Status = defaultStatus res.Output = "" return } defer func() { runPlugin = oldRunPlugin }() pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) close(ch) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) } func TestRunPluginWithOnFailureProperty1(t *testing.T) { setIsSupportedMock() defer restoreIsSupported() pluginNames := []string{testPlugin1} pluginConfigs := make(map[string]contracts.PluginState) pluginResults := make(map[string]*contracts.PluginResult) pluginInstances := make(map[string]*PluginMock) pluginRegistry := PluginRegistry{} var cancelFlag task.CancelFlag ctx := contextmocks.NewMockDefault() defaultTime := time.Now() ioConfig := contracts.IOConfiguration{} pluginConfigs2 := make([]contracts.PluginState, len(pluginNames)) for index, name := range pluginNames { // create mock plugin instance for testing pluginInstances[name] = new(PluginMock) // create properties testProperties := map[string]interface{}{ contracts.OnFailureModifier: contracts.ModifierValueSuccessAndExit, } // create configuration for execution config := contracts.Configuration{ PluginID: name, PluginName: name, Properties: testProperties, } // setup expectations pluginConfigs[name] = contracts.PluginState{ Name: name, Id: name, Configuration: config, } standardOutput := "\nStep was found to have onFailure property. Further document steps will be skipped." standardError := standardOutput defaultCode := 1 defaultStatus := contracts.ResultStatusFailed outputMessage := "" pluginResults[name] = &contracts.PluginResult{ PluginID: name, PluginName: name, StartDateTime: defaultTime, EndDateTime: defaultTime, Status: contracts.ResultStatusSuccess, Code: contracts.ExitWithSuccess, StandardOutput: standardOutput, StandardError: standardError, Output: outputMessage, } oldRunPlugin := runPlugin runPlugin = func(context context.T, factory PluginFactory, pluginName string, config contracts.Configuration, cancelFlag task.CancelFlag, ioConfig contracts.IOConfiguration, ) (res contracts.PluginResult) { res.Code = defaultCode res.Status = defaultStatus res.Output = "" return } defer func() { runPlugin = oldRunPlugin }() pluginFactory := new(PluginFactoryMock) pluginFactory.On("Create", mock.Anything).Return(pluginInstances[name], nil) pluginRegistry[name] = pluginFactory pluginConfigs2[index] = pluginConfigs[name] } called := 0 ch := make(chan contracts.PluginResult) go func() { for result := range ch { result.EndDateTime = defaultTime result.StartDateTime = defaultTime if called == 0 { assert.Equal(t, result, *pluginResults[testPlugin1]) } else { assert.Fail(t, "there shouldn't be more than 2 update") } called++ } }() // call the code we are testing outputs := RunPlugins(ctx, pluginConfigs2, ioConfig, contracts.MessageGatewayService, pluginRegistry, ch, cancelFlag) close(ch) // fix the times expectation. for _, result := range outputs { result.EndDateTime = defaultTime result.StartDateTime = defaultTime } ctx.AssertCalled(t, "Log") assert.Equal(t, pluginResults[testPlugin1], outputs[testPlugin1]) }