diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/java/com/external/plugins/AwsLambdaPlugin.java b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/java/com/external/plugins/AwsLambdaPlugin.java index 907e8f4913..e6e48c5bea 100644 --- a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/java/com/external/plugins/AwsLambdaPlugin.java +++ b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/java/com/external/plugins/AwsLambdaPlugin.java @@ -9,7 +9,11 @@ import com.amazonaws.services.lambda.model.AWSLambdaException; import com.amazonaws.services.lambda.model.FunctionConfiguration; import com.amazonaws.services.lambda.model.InvokeRequest; import com.amazonaws.services.lambda.model.InvokeResult; +import com.amazonaws.services.lambda.model.ListAliasesRequest; +import com.amazonaws.services.lambda.model.ListAliasesResult; import com.amazonaws.services.lambda.model.ListFunctionsResult; +import com.amazonaws.services.lambda.model.ListVersionsByFunctionRequest; +import com.amazonaws.services.lambda.model.ListVersionsByFunctionResult; import com.amazonaws.services.lambda.model.ResourceNotFoundException; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; @@ -70,6 +74,10 @@ public class AwsLambdaPlugin extends BasePlugin { ActionExecutionResult result; switch (Objects.requireNonNull(command)) { case "LIST_FUNCTIONS" -> result = listFunctions(actionConfiguration, connection); + case "LIST_FUNCTION_VERSIONS" -> result = + listFunctionVersions(actionConfiguration, connection); + case "LIST_FUNCTION_ALIASES" -> result = + listFunctionAliases(actionConfiguration, connection); case "INVOKE_FUNCTION" -> result = invokeFunction(actionConfiguration, connection); default -> throw new IllegalStateException("Unexpected value: " + command); } @@ -98,24 +106,108 @@ public class AwsLambdaPlugin extends BasePlugin { throw new AppsmithPluginException( AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "request type is missing"); } - ActionExecutionResult actionExecutionResult = listFunctions(null, connection); - ArrayNode body = (ArrayNode) actionExecutionResult.getBody(); - List> functionNames = StreamSupport.stream(body.spliterator(), false) - .map(function -> function.get("functionName").asText()) - .sorted() - .map(functionName -> Map.of("label", functionName, "value", functionName)) - .collect(Collectors.toList()); + + String requestType = request.getRequestType(); + ActionExecutionResult actionExecutionResult; + List> options; + Map params = request.getParameters() == null ? Map.of() : request.getParameters(); + + switch (requestType) { + case "FUNCTION_NAMES" -> { + actionExecutionResult = listFunctions(null, connection); + ArrayNode body = (ArrayNode) actionExecutionResult.getBody(); + options = StreamSupport.stream(body.spliterator(), false) + .map(function -> function.get("functionName").asText()) + .sorted() + .map(functionName -> Map.of("label", functionName, "value", functionName)) + .collect(Collectors.toList()); + } + case "FUNCTION_VERSIONS" -> { + // Handle both old and new parameter structures + String functionName; + if (params.containsKey("parameters") && params.get("parameters") instanceof Map) { + Map parameters = (Map) params.get("parameters"); + functionName = (String) parameters.get("functionName"); + } else { + functionName = (String) params.get("functionName"); + } + + if (!StringUtils.hasText(functionName)) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + "function name is required for listing versions"); + } + actionExecutionResult = listFunctionVersions(null, connection, functionName); + ArrayNode body = (ArrayNode) actionExecutionResult.getBody(); + options = StreamSupport.stream(body.spliterator(), false) + .map(version -> version.get("version").asText()) + .sorted() + .map(version -> Map.of("label", version, "value", version)) + .collect(Collectors.toList()); + } + case "FUNCTION_ALIASES" -> { + // Handle both old and new parameter structures + String functionName; + if (params.containsKey("parameters") && params.get("parameters") instanceof Map) { + Map parameters = (Map) params.get("parameters"); + functionName = (String) parameters.get("functionName"); + } else { + functionName = (String) params.get("functionName"); + } + + if (!StringUtils.hasText(functionName)) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + "function name is required for listing aliases"); + } + actionExecutionResult = listFunctionAliases(null, connection, functionName); + ArrayNode body = (ArrayNode) actionExecutionResult.getBody(); + options = StreamSupport.stream(body.spliterator(), false) + .map(alias -> alias.get("name").asText()) + .sorted() + .map(alias -> Map.of("label", alias, "value", alias)) + .collect(Collectors.toList()); + } + default -> throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Unsupported request type: " + requestType); + } TriggerResultDTO triggerResultDTO = new TriggerResultDTO(); - triggerResultDTO.setTrigger(functionNames); + triggerResultDTO.setTrigger(options); return Mono.just(triggerResultDTO); } ActionExecutionResult invokeFunction(ActionConfiguration actionConfiguration, AWSLambda connection) { InvokeRequest invokeRequest = new InvokeRequest(); - invokeRequest.setFunctionName( - getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "functionName", STRING_TYPE)); + + // Validate and set function name (required parameter) + String functionName = + getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "functionName", STRING_TYPE); + if (!StringUtils.hasText(functionName)) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + "Function name is required for Lambda invocation"); + } + + // Get version and alias parameters + String functionVersion = + getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "functionVersion", STRING_TYPE); + String functionAlias = + getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "functionAlias", STRING_TYPE); + + // Set function name (without qualifier) + invokeRequest.setFunctionName(functionName); + + // Use setQualifier for version/alias instead of embedding in function name + if (StringUtils.hasText(functionAlias)) { + // If alias is specified, use it (alias takes precedence over version) + invokeRequest.setQualifier(functionAlias); + } else if (StringUtils.hasText(functionVersion)) { + // If version is specified and no alias, use version + invokeRequest.setQualifier(functionVersion); + } + // If neither version nor alias is specified, defaults to $LATEST invokeRequest.setPayload( getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "body", STRING_TYPE)); invokeRequest.setInvocationType( @@ -145,6 +237,61 @@ public class AwsLambdaPlugin extends BasePlugin { return result; } + ActionExecutionResult listFunctionVersions( + ActionConfiguration actionConfiguration, AWSLambda connection, String functionName) { + if (actionConfiguration != null) { + functionName = + getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "functionName", STRING_TYPE); + } + if (!StringUtils.hasText(functionName)) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + "function name is required for listing versions"); + } + + ListVersionsByFunctionRequest request = new ListVersionsByFunctionRequest(); + request.setFunctionName(functionName); + + ListVersionsByFunctionResult listVersionsResult = connection.listVersionsByFunction(request); + List versions = listVersionsResult.getVersions(); + + ActionExecutionResult result = new ActionExecutionResult(); + result.setBody(objectMapper.valueToTree(versions)); + result.setIsExecutionSuccess(true); + return result; + } + + ActionExecutionResult listFunctionVersions(ActionConfiguration actionConfiguration, AWSLambda connection) { + String functionName = + getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "functionName", STRING_TYPE); + return listFunctionVersions(null, connection, functionName); + } + + ActionExecutionResult listFunctionAliases( + ActionConfiguration actionConfiguration, AWSLambda connection, String functionName) { + if (actionConfiguration != null) { + functionName = + getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "functionName", STRING_TYPE); + } + + ListAliasesRequest request = new ListAliasesRequest(); + request.setFunctionName(functionName); + + ListAliasesResult listAliasesResult = connection.listAliases(request); + List aliases = listAliasesResult.getAliases(); + + ActionExecutionResult result = new ActionExecutionResult(); + result.setBody(objectMapper.valueToTree(aliases)); + result.setIsExecutionSuccess(true); + return result; + } + + ActionExecutionResult listFunctionAliases(ActionConfiguration actionConfiguration, AWSLambda connection) { + String functionName = + getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "functionName", STRING_TYPE); + return listFunctionAliases(null, connection, functionName); + } + @Override public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { log.debug(Thread.currentThread().getName() + ": datasourceCreate() called for AWS Lambda plugin."); diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/invoke.json b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/invoke.json index fb9b4df449..23496934c9 100644 --- a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/invoke.json +++ b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/invoke.json @@ -36,6 +36,66 @@ } } }, + { + "label": "Function version", + "tooltipText": "Optional: Specify a version number (e.g., 1, 2, $LATEST) or leave empty for $LATEST.", + "subtitle": "", + "isRequired": false, + "propertyName": "function_version", + "configProperty": "actionConfiguration.formData.functionVersion.data", + "controlType": "DROP_DOWN", + "initialValue": "", + "options": [], + "placeholderText": "Leave empty for $LATEST version", + "fetchOptionsConditionally": true, + "setFirstOptionAsDefault": false, + "alternateViewTypes": ["json"], + "conditionals": { + "enable": "{{actionConfiguration.formData.functionName.data}}", + "fetchDynamicValues": { + "condition": "{{actionConfiguration.formData.command.data === 'INVOKE_FUNCTION' && actionConfiguration.formData.functionName.data}}", + "config": { + "params": { + "requestType": "FUNCTION_VERSIONS", + "displayType": "DROP_DOWN", + "parameters": { + "functionName": "{{actionConfiguration.formData.functionName.data}}" + } + } + } + } + } + }, + { + "label": "Function alias", + "tooltipText": "Optional: Specify an alias name (e.g., PROD, STAGING) or leave empty for no alias.", + "subtitle": "", + "isRequired": false, + "propertyName": "function_alias", + "configProperty": "actionConfiguration.formData.functionAlias.data", + "controlType": "DROP_DOWN", + "initialValue": "", + "options": [], + "placeholderText": "Leave empty for no alias", + "fetchOptionsConditionally": true, + "setFirstOptionAsDefault": false, + "alternateViewTypes": ["json"], + "conditionals": { + "enable": "{{actionConfiguration.formData.functionName.data}}", + "fetchDynamicValues": { + "condition": "{{actionConfiguration.formData.command.data === 'INVOKE_FUNCTION' && actionConfiguration.formData.functionName.data}}", + "config": { + "params": { + "requestType": "FUNCTION_ALIASES", + "displayType": "DROP_DOWN", + "parameters": { + "functionName": "{{actionConfiguration.formData.functionName.data}}" + } + } + } + } + } + }, { "label": "Type of invocation", "tooltipText": "Should the invocation be synchronous or asynchronous?", diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/listAliases.json b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/listAliases.json new file mode 100644 index 0000000000..7bce6ca7e5 --- /dev/null +++ b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/listAliases.json @@ -0,0 +1,42 @@ +{ + "identifier": "LIST_FUNCTION_ALIASES", + "controlType": "SECTION_V2", + "conditionals": { + "show": "{{actionConfiguration.formData.command.data === 'LIST_FUNCTION_ALIASES'}}" + }, + "children": [ + { + "controlType": "DOUBLE_COLUMN_ZONE", + "label": "Function details", + "children": [ + { + "label": "Function name", + "tooltipText": "The name of the AWS Lambda function to list aliases for.", + "subtitle": "", + "isRequired": true, + "propertyName": "function_name", + "configProperty": "actionConfiguration.formData.functionName.data", + "controlType": "DROP_DOWN", + "initialValue": "", + "options": [], + "placeholderText": "All function names will be fetched.", + "fetchOptionsConditionally": true, + "setFirstOptionAsDefault": true, + "alternateViewTypes": ["json"], + "conditionals": { + "enable": "{{true}}", + "fetchDynamicValues": { + "condition": "{{actionConfiguration.formData.command.data === 'LIST_FUNCTION_ALIASES'}}", + "config": { + "params": { + "requestType": "FUNCTION_NAMES", + "displayType": "DROP_DOWN" + } + } + } + } + } + ] + } + ] +} diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/listVersions.json b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/listVersions.json new file mode 100644 index 0000000000..56b868934c --- /dev/null +++ b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/listVersions.json @@ -0,0 +1,42 @@ +{ + "identifier": "LIST_FUNCTION_VERSIONS", + "controlType": "SECTION_V2", + "conditionals": { + "show": "{{actionConfiguration.formData.command.data === 'LIST_FUNCTION_VERSIONS'}}" + }, + "children": [ + { + "controlType": "DOUBLE_COLUMN_ZONE", + "label": "Function details", + "children": [ + { + "label": "Function name", + "tooltipText": "The name of the AWS Lambda function to list versions for.", + "subtitle": "", + "isRequired": true, + "propertyName": "function_name", + "configProperty": "actionConfiguration.formData.functionName.data", + "controlType": "DROP_DOWN", + "initialValue": "", + "options": [], + "placeholderText": "All function names will be fetched.", + "fetchOptionsConditionally": true, + "setFirstOptionAsDefault": true, + "alternateViewTypes": ["json"], + "conditionals": { + "enable": "{{true}}", + "fetchDynamicValues": { + "condition": "{{actionConfiguration.formData.command.data === 'LIST_FUNCTION_VERSIONS'}}", + "config": { + "params": { + "requestType": "FUNCTION_NAMES", + "displayType": "DROP_DOWN" + } + } + } + } + } + ] + } + ] +} diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/root.json b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/root.json index 00bc0a8e40..7d320fc43d 100644 --- a/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/root.json +++ b/app/server/appsmith-plugins/awsLambdaPlugin/src/main/resources/editor/root.json @@ -13,12 +13,21 @@ "description": "Choose the method you would like to use", "configProperty": "actionConfiguration.formData.command.data", "controlType": "DROP_DOWN", + "isRequired": true, "initialValue": "LIST_FUNCTIONS", "options": [ { "label": "List all functions", "value": "LIST_FUNCTIONS" }, + { + "label": "List function versions", + "value": "LIST_FUNCTION_VERSIONS" + }, + { + "label": "List function aliases", + "value": "LIST_FUNCTION_ALIASES" + }, { "label": "Invoke a function", "value": "INVOKE_FUNCTION" @@ -30,5 +39,5 @@ ] } ], - "files": ["list.json", "invoke.json"] + "files": ["list.json", "listVersions.json", "listAliases.json", "invoke.json"] } diff --git a/app/server/appsmith-plugins/awsLambdaPlugin/src/test/java/com/external/plugins/AwsLambdaPluginTest.java b/app/server/appsmith-plugins/awsLambdaPlugin/src/test/java/com/external/plugins/AwsLambdaPluginTest.java index 7a41e59d7c..6717257934 100644 --- a/app/server/appsmith-plugins/awsLambdaPlugin/src/test/java/com/external/plugins/AwsLambdaPluginTest.java +++ b/app/server/appsmith-plugins/awsLambdaPlugin/src/test/java/com/external/plugins/AwsLambdaPluginTest.java @@ -1,9 +1,15 @@ package com.external.plugins; import com.amazonaws.services.lambda.AWSLambda; +import com.amazonaws.services.lambda.model.AliasConfiguration; import com.amazonaws.services.lambda.model.FunctionConfiguration; +import com.amazonaws.services.lambda.model.InvokeRequest; import com.amazonaws.services.lambda.model.InvokeResult; +import com.amazonaws.services.lambda.model.ListAliasesRequest; +import com.amazonaws.services.lambda.model.ListAliasesResult; import com.amazonaws.services.lambda.model.ListFunctionsResult; +import com.amazonaws.services.lambda.model.ListVersionsByFunctionRequest; +import com.amazonaws.services.lambda.model.ListVersionsByFunctionResult; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; @@ -14,6 +20,7 @@ import com.appsmith.external.models.TriggerRequestDTO; import com.fasterxml.jackson.databind.node.ArrayNode; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.testcontainers.junit.jupiter.Testcontainers; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -31,6 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @Testcontainers @@ -193,4 +201,305 @@ public class AwsLambdaPluginTest { pluginExecutor.trigger(mockLambda, datasourceConfiguration, request).block(); }); } + + @Test + public void testExecuteListFunctionVersions() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + + Map configMap = new HashMap<>(); + setDataValueSafelyInFormData(configMap, "command", "LIST_FUNCTION_VERSIONS"); + setDataValueSafelyInFormData(configMap, "functionName", "test-aws-lambda"); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setFormData(configMap); + + // Mock the Lambda connection + AWSLambda mockLambda = mock(AWSLambda.class); + ListVersionsByFunctionResult mockVersionsResult = new ListVersionsByFunctionResult(); + mockVersionsResult.setVersions(List.of( + new FunctionConfiguration().withVersion("$LATEST"), + new FunctionConfiguration().withVersion("1"), + new FunctionConfiguration().withVersion("2"))); + when(mockLambda.listVersionsByFunction(any(ListVersionsByFunctionRequest.class))) + .thenReturn(mockVersionsResult); + + Mono resultMono = + pluginExecutor.execute(mockLambda, datasourceConfiguration, actionConfiguration); + StepVerifier.create(resultMono) + .assertNext(result -> { + assertEquals(3, ((ArrayNode) result.getBody()).size()); + }) + .verifyComplete(); + } + + @Test + public void testExecuteListFunctionAliases() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + + Map configMap = new HashMap<>(); + setDataValueSafelyInFormData(configMap, "command", "LIST_FUNCTION_ALIASES"); + setDataValueSafelyInFormData(configMap, "functionName", "test-aws-lambda"); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setFormData(configMap); + + // Mock the Lambda connection + AWSLambda mockLambda = mock(AWSLambda.class); + ListAliasesResult mockAliasesResult = new ListAliasesResult(); + mockAliasesResult.setAliases( + List.of(new AliasConfiguration().withName("PROD"), new AliasConfiguration().withName("STAGING"))); + when(mockLambda.listAliases(any(ListAliasesRequest.class))).thenReturn(mockAliasesResult); + + Mono resultMono = + pluginExecutor.execute(mockLambda, datasourceConfiguration, actionConfiguration); + StepVerifier.create(resultMono) + .assertNext(result -> { + assertEquals(2, ((ArrayNode) result.getBody()).size()); + }) + .verifyComplete(); + } + + @Test + public void testExecuteInvokeFunctionWithVersion() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + + Map configMap = new HashMap<>(); + setDataValueSafelyInFormData(configMap, "command", "INVOKE_FUNCTION"); + setDataValueSafelyInFormData(configMap, "body", "{\"data\": \"\"}"); + setDataValueSafelyInFormData(configMap, "functionName", "test-aws-lambda"); + setDataValueSafelyInFormData(configMap, "functionVersion", "2"); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setFormData(configMap); + + // Mock the Lambda connection + AWSLambda mockLambda = mock(AWSLambda.class); + InvokeResult mockResult = new InvokeResult(); + mockResult.setPayload(ByteBuffer.wrap("Hello World from version 2".getBytes())); + when(mockLambda.invoke(any())).thenReturn(mockResult); + + // Capture the InvokeRequest to verify the qualifier is set correctly + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(InvokeRequest.class); + + Mono resultMono = + pluginExecutor.execute(mockLambda, datasourceConfiguration, actionConfiguration); + StepVerifier.create(resultMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + assertEquals("Hello World from version 2", result.getBody().toString()); + }) + .verifyComplete(); + + // Verify that the InvokeRequest was called with the correct qualifier + verify(mockLambda).invoke(requestCaptor.capture()); + InvokeRequest capturedRequest = requestCaptor.getValue(); + assertEquals("test-aws-lambda", capturedRequest.getFunctionName()); + assertEquals("2", capturedRequest.getQualifier()); + } + + @Test + public void testExecuteInvokeFunctionWithAlias() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + + Map configMap = new HashMap<>(); + setDataValueSafelyInFormData(configMap, "command", "INVOKE_FUNCTION"); + setDataValueSafelyInFormData(configMap, "body", "{\"data\": \"\"}"); + setDataValueSafelyInFormData(configMap, "functionName", "test-aws-lambda"); + setDataValueSafelyInFormData(configMap, "functionAlias", "PROD"); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setFormData(configMap); + + // Mock the Lambda connection + AWSLambda mockLambda = mock(AWSLambda.class); + InvokeResult mockResult = new InvokeResult(); + mockResult.setPayload(ByteBuffer.wrap("Hello World from PROD alias".getBytes())); + when(mockLambda.invoke(any())).thenReturn(mockResult); + + // Capture the InvokeRequest to verify the qualifier is set correctly + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(InvokeRequest.class); + + Mono resultMono = + pluginExecutor.execute(mockLambda, datasourceConfiguration, actionConfiguration); + StepVerifier.create(resultMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + assertEquals("Hello World from PROD alias", result.getBody().toString()); + }) + .verifyComplete(); + + // Verify that the InvokeRequest was called with the correct qualifier + verify(mockLambda).invoke(requestCaptor.capture()); + InvokeRequest capturedRequest = requestCaptor.getValue(); + assertEquals("test-aws-lambda", capturedRequest.getFunctionName()); + assertEquals("PROD", capturedRequest.getQualifier()); + } + + @Test + public void testExecuteInvokeFunctionWithAliasTakesPrecedenceOverVersion() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + + Map configMap = new HashMap<>(); + setDataValueSafelyInFormData(configMap, "command", "INVOKE_FUNCTION"); + setDataValueSafelyInFormData(configMap, "body", "{\"data\": \"\"}"); + setDataValueSafelyInFormData(configMap, "functionName", "test-aws-lambda"); + setDataValueSafelyInFormData(configMap, "functionVersion", "2"); + setDataValueSafelyInFormData(configMap, "functionAlias", "PROD"); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setFormData(configMap); + + // Mock the Lambda connection + AWSLambda mockLambda = mock(AWSLambda.class); + InvokeResult mockResult = new InvokeResult(); + mockResult.setPayload(ByteBuffer.wrap("Hello World from PROD alias (alias takes precedence)".getBytes())); + when(mockLambda.invoke(any())).thenReturn(mockResult); + + // Capture the InvokeRequest to verify the qualifier is set correctly + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(InvokeRequest.class); + + Mono resultMono = + pluginExecutor.execute(mockLambda, datasourceConfiguration, actionConfiguration); + StepVerifier.create(resultMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + assertEquals( + "Hello World from PROD alias (alias takes precedence)", + result.getBody().toString()); + }) + .verifyComplete(); + + // Verify that the InvokeRequest was called with the alias (not version) as qualifier + verify(mockLambda).invoke(requestCaptor.capture()); + InvokeRequest capturedRequest = requestCaptor.getValue(); + assertEquals("test-aws-lambda", capturedRequest.getFunctionName()); + assertEquals("PROD", capturedRequest.getQualifier()); // Should be alias, not version "2" + } + + @Test + public void testExecuteInvokeFunctionWithoutVersionOrAlias() { + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + + Map configMap = new HashMap<>(); + setDataValueSafelyInFormData(configMap, "command", "INVOKE_FUNCTION"); + setDataValueSafelyInFormData(configMap, "body", "{\"data\": \"\"}"); + setDataValueSafelyInFormData(configMap, "functionName", "test-aws-lambda"); + // No functionVersion or functionAlias specified + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setFormData(configMap); + + // Mock the Lambda connection + AWSLambda mockLambda = mock(AWSLambda.class); + InvokeResult mockResult = new InvokeResult(); + mockResult.setPayload(ByteBuffer.wrap("Hello World from $LATEST".getBytes())); + when(mockLambda.invoke(any())).thenReturn(mockResult); + + // Capture the InvokeRequest to verify no qualifier is set (defaults to $LATEST) + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(InvokeRequest.class); + + Mono resultMono = + pluginExecutor.execute(mockLambda, datasourceConfiguration, actionConfiguration); + StepVerifier.create(resultMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + assertEquals("Hello World from $LATEST", result.getBody().toString()); + }) + .verifyComplete(); + + // Verify that the InvokeRequest was called without a qualifier (defaults to $LATEST) + verify(mockLambda).invoke(requestCaptor.capture()); + InvokeRequest capturedRequest = requestCaptor.getValue(); + assertEquals("test-aws-lambda", capturedRequest.getFunctionName()); + // When no qualifier is set, it should be null (AWS defaults to $LATEST) + assertEquals(null, capturedRequest.getQualifier()); + } + + @Test + public void testTriggerFunctionNames() { + AWSLambda mockLambda = mock(AWSLambda.class); + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + + ListFunctionsResult mockFunctionsResult = new ListFunctionsResult(); + mockFunctionsResult.setFunctions(List.of( + new FunctionConfiguration().withFunctionName("function1"), + new FunctionConfiguration().withFunctionName("function2"))); + when(mockLambda.listFunctions()).thenReturn(mockFunctionsResult); + + TriggerRequestDTO request = new TriggerRequestDTO(); + request.setRequestType("FUNCTION_NAMES"); + + Mono resultMono = + pluginExecutor.trigger(mockLambda, datasourceConfiguration, request); + StepVerifier.create(resultMono) + .assertNext(result -> { + assertEquals(2, ((List) result.getTrigger()).size()); + }) + .verifyComplete(); + } + + @Test + public void testTriggerFunctionVersions() { + AWSLambda mockLambda = mock(AWSLambda.class); + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + + ListVersionsByFunctionResult mockVersionsResult = new ListVersionsByFunctionResult(); + mockVersionsResult.setVersions(List.of( + new FunctionConfiguration().withVersion("$LATEST"), + new FunctionConfiguration().withVersion("1"), + new FunctionConfiguration().withVersion("2"))); + when(mockLambda.listVersionsByFunction(any(ListVersionsByFunctionRequest.class))) + .thenReturn(mockVersionsResult); + + TriggerRequestDTO request = new TriggerRequestDTO(); + request.setRequestType("FUNCTION_VERSIONS"); + Map params = new HashMap<>(); + params.put("functionName", "test-function"); + request.setParameters(params); + + Mono resultMono = + pluginExecutor.trigger(mockLambda, datasourceConfiguration, request); + StepVerifier.create(resultMono) + .assertNext(result -> { + assertEquals(3, ((List) result.getTrigger()).size()); + }) + .verifyComplete(); + } + + @Test + public void testTriggerFunctionAliases() { + AWSLambda mockLambda = mock(AWSLambda.class); + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + + ListAliasesResult mockAliasesResult = new ListAliasesResult(); + mockAliasesResult.setAliases( + List.of(new AliasConfiguration().withName("PROD"), new AliasConfiguration().withName("STAGING"))); + when(mockLambda.listAliases(any(ListAliasesRequest.class))).thenReturn(mockAliasesResult); + + TriggerRequestDTO request = new TriggerRequestDTO(); + request.setRequestType("FUNCTION_ALIASES"); + Map params = new HashMap<>(); + params.put("functionName", "test-function"); + request.setParameters(params); + + Mono resultMono = + pluginExecutor.trigger(mockLambda, datasourceConfiguration, request); + StepVerifier.create(resultMono) + .assertNext(result -> { + assertEquals(2, ((List) result.getTrigger()).size()); + }) + .verifyComplete(); + } + + @Test + public void testTriggerUnsupportedRequestType() { + AWSLambda mockLambda = mock(AWSLambda.class); + DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration(); + TriggerRequestDTO request = new TriggerRequestDTO(); + request.setRequestType("UNSUPPORTED_TYPE"); + + assertThrows(AppsmithPluginException.class, () -> { + pluginExecutor.trigger(mockLambda, datasourceConfiguration, request).block(); + }); + } }