feat: add support for listing function versions and aliases in AWS Lambda Plugin (#41263)

This commit is contained in:
Rahul Barwal 2025-09-26 20:45:46 +05:30 committed by GitHub
parent b79b160d2f
commit 180af275b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 620 additions and 11 deletions

View File

@ -9,7 +9,11 @@ import com.amazonaws.services.lambda.model.AWSLambdaException;
import com.amazonaws.services.lambda.model.FunctionConfiguration; import com.amazonaws.services.lambda.model.FunctionConfiguration;
import com.amazonaws.services.lambda.model.InvokeRequest; import com.amazonaws.services.lambda.model.InvokeRequest;
import com.amazonaws.services.lambda.model.InvokeResult; 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.ListFunctionsResult;
import com.amazonaws.services.lambda.model.ListVersionsByFunctionRequest;
import com.amazonaws.services.lambda.model.ListVersionsByFunctionResult;
import com.amazonaws.services.lambda.model.ResourceNotFoundException; import com.amazonaws.services.lambda.model.ResourceNotFoundException;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
@ -70,6 +74,10 @@ public class AwsLambdaPlugin extends BasePlugin {
ActionExecutionResult result; ActionExecutionResult result;
switch (Objects.requireNonNull(command)) { switch (Objects.requireNonNull(command)) {
case "LIST_FUNCTIONS" -> result = listFunctions(actionConfiguration, connection); 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); case "INVOKE_FUNCTION" -> result = invokeFunction(actionConfiguration, connection);
default -> throw new IllegalStateException("Unexpected value: " + command); default -> throw new IllegalStateException("Unexpected value: " + command);
} }
@ -98,24 +106,108 @@ public class AwsLambdaPlugin extends BasePlugin {
throw new AppsmithPluginException( throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "request type is missing"); AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "request type is missing");
} }
ActionExecutionResult actionExecutionResult = listFunctions(null, connection);
ArrayNode body = (ArrayNode) actionExecutionResult.getBody(); String requestType = request.getRequestType();
List<Map<String, String>> functionNames = StreamSupport.stream(body.spliterator(), false) ActionExecutionResult actionExecutionResult;
.map(function -> function.get("functionName").asText()) List<Map<String, String>> options;
.sorted() Map<?, Object> params = request.getParameters() == null ? Map.of() : request.getParameters();
.map(functionName -> Map.of("label", functionName, "value", functionName))
.collect(Collectors.toList()); 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 triggerResultDTO = new TriggerResultDTO();
triggerResultDTO.setTrigger(functionNames); triggerResultDTO.setTrigger(options);
return Mono.just(triggerResultDTO); return Mono.just(triggerResultDTO);
} }
ActionExecutionResult invokeFunction(ActionConfiguration actionConfiguration, AWSLambda connection) { ActionExecutionResult invokeFunction(ActionConfiguration actionConfiguration, AWSLambda connection) {
InvokeRequest invokeRequest = new InvokeRequest(); 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( invokeRequest.setPayload(
getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "body", STRING_TYPE)); getDataValueSafelyFromFormData(actionConfiguration.getFormData(), "body", STRING_TYPE));
invokeRequest.setInvocationType( invokeRequest.setInvocationType(
@ -145,6 +237,61 @@ public class AwsLambdaPlugin extends BasePlugin {
return result; 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<FunctionConfiguration> 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<com.amazonaws.services.lambda.model.AliasConfiguration> 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 @Override
public Mono<AWSLambda> datasourceCreate(DatasourceConfiguration datasourceConfiguration) { public Mono<AWSLambda> datasourceCreate(DatasourceConfiguration datasourceConfiguration) {
log.debug(Thread.currentThread().getName() + ": datasourceCreate() called for AWS Lambda plugin."); log.debug(Thread.currentThread().getName() + ": datasourceCreate() called for AWS Lambda plugin.");

View File

@ -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", "label": "Type of invocation",
"tooltipText": "Should the invocation be synchronous or asynchronous?", "tooltipText": "Should the invocation be synchronous or asynchronous?",

View File

@ -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"
}
}
}
}
}
]
}
]
}

View File

@ -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"
}
}
}
}
}
]
}
]
}

View File

@ -13,12 +13,21 @@
"description": "Choose the method you would like to use", "description": "Choose the method you would like to use",
"configProperty": "actionConfiguration.formData.command.data", "configProperty": "actionConfiguration.formData.command.data",
"controlType": "DROP_DOWN", "controlType": "DROP_DOWN",
"isRequired": true,
"initialValue": "LIST_FUNCTIONS", "initialValue": "LIST_FUNCTIONS",
"options": [ "options": [
{ {
"label": "List all functions", "label": "List all functions",
"value": "LIST_FUNCTIONS" "value": "LIST_FUNCTIONS"
}, },
{
"label": "List function versions",
"value": "LIST_FUNCTION_VERSIONS"
},
{
"label": "List function aliases",
"value": "LIST_FUNCTION_ALIASES"
},
{ {
"label": "Invoke a function", "label": "Invoke a function",
"value": "INVOKE_FUNCTION" "value": "INVOKE_FUNCTION"
@ -30,5 +39,5 @@
] ]
} }
], ],
"files": ["list.json", "invoke.json"] "files": ["list.json", "listVersions.json", "listAliases.json", "invoke.json"]
} }

View File

@ -1,9 +1,15 @@
package com.external.plugins; package com.external.plugins;
import com.amazonaws.services.lambda.AWSLambda; 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.FunctionConfiguration;
import com.amazonaws.services.lambda.model.InvokeRequest;
import com.amazonaws.services.lambda.model.InvokeResult; 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.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.exceptions.pluginExceptions.AppsmithPluginException;
import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.ActionExecutionResult;
@ -14,6 +20,7 @@ import com.appsmith.external.models.TriggerRequestDTO;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.junit.jupiter.Testcontainers;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; 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.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@Testcontainers @Testcontainers
@ -193,4 +201,305 @@ public class AwsLambdaPluginTest {
pluginExecutor.trigger(mockLambda, datasourceConfiguration, request).block(); pluginExecutor.trigger(mockLambda, datasourceConfiguration, request).block();
}); });
} }
@Test
public void testExecuteListFunctionVersions() {
DatasourceConfiguration datasourceConfiguration = createDatasourceConfiguration();
Map<String, Object> 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<ActionExecutionResult> 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<String, Object> 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<ActionExecutionResult> 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<String, Object> 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<InvokeRequest> requestCaptor = ArgumentCaptor.forClass(InvokeRequest.class);
Mono<ActionExecutionResult> 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<String, Object> 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<InvokeRequest> requestCaptor = ArgumentCaptor.forClass(InvokeRequest.class);
Mono<ActionExecutionResult> 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<String, Object> 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<InvokeRequest> requestCaptor = ArgumentCaptor.forClass(InvokeRequest.class);
Mono<ActionExecutionResult> 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<String, Object> 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<InvokeRequest> requestCaptor = ArgumentCaptor.forClass(InvokeRequest.class);
Mono<ActionExecutionResult> 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<com.appsmith.external.models.TriggerResultDTO> 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<String, Object> params = new HashMap<>();
params.put("functionName", "test-function");
request.setParameters(params);
Mono<com.appsmith.external.models.TriggerResultDTO> 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<String, Object> params = new HashMap<>();
params.put("functionName", "test-function");
request.setParameters(params);
Mono<com.appsmith.external.models.TriggerResultDTO> 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();
});
}
} }