diff --git a/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/ApplicationIntegrationToolset.java b/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/ApplicationIntegrationToolset.java index ece6346f6..fc1abd4f0 100644 --- a/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/ApplicationIntegrationToolset.java +++ b/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/ApplicationIntegrationToolset.java @@ -5,9 +5,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.adk.agents.ReadonlyContext; import com.google.adk.tools.BaseTool; +import com.google.adk.tools.BaseToolset; import com.google.adk.tools.applicationintegrationtoolset.IntegrationConnectorTool.DefaultHttpExecutor; import com.google.adk.tools.applicationintegrationtoolset.IntegrationConnectorTool.HttpExecutor; +import io.reactivex.rxjava3.core.Flowable; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -15,7 +18,7 @@ import org.jspecify.annotations.Nullable; /** Application Integration Toolset */ -public class ApplicationIntegrationToolset { +public class ApplicationIntegrationToolset implements BaseToolset { String project; String location; @Nullable String integration; @@ -23,6 +26,7 @@ public class ApplicationIntegrationToolset { @Nullable String connection; @Nullable Map> entityOperations; @Nullable List actions; + String serviceAccountJson; @Nullable String toolNamePrefix; @Nullable String toolInstructions; public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -35,16 +39,16 @@ public class ApplicationIntegrationToolset { * *

integrationTool = new ApplicationIntegrationToolset( project="test-project", * location="us-central1", integration="test-integration", - * triggers=ImmutableList.of("api_trigger/test_trigger", - * "api_trigger/test_trigger_2"),connection=null,enitityOperations=null,actions=null,toolNamePrefix="test-integration-tool",toolInstructions="This + * triggers=ImmutableList.of("api_trigger/test_trigger", "api_trigger/test_trigger_2", + * serviceAccountJson="{....}"),connection=null,enitityOperations=null,actions=null,toolNamePrefix="test-integration-tool",toolInstructions="This * tool is used to get response from test-integration."); * *

connectionTool = new ApplicationIntegrationToolset( project="test-project", * location="us-central1", integration=null, triggers=null, connection="test-connection", * entityOperations=ImmutableMap.of("Entity1", ImmutableList.of("LIST", "GET", "UPDATE")), * "Entity2", ImmutableList.of()), actions=ImmutableList.of("ExecuteCustomQuery"), - * toolNamePrefix="test-tool", toolInstructions="This tool is used to list, get and update issues - * in Jira."); + * serviceAccountJson="{....}", toolNamePrefix="test-tool", toolInstructions="This tool is used to + * list, get and update issues in Jira."); * * @param project The GCP project ID. * @param location The GCP location of integration. @@ -53,6 +57,9 @@ public class ApplicationIntegrationToolset { * @param connection(Optional) The connection name. * @param entityOperations(Optional) The entity operations. * @param actions(Optional) The actions. + * @param serviceAccountJson(Optional) The service account configuration as a dictionary. Required + * if not using default service credential. Used for fetching the Application Integration or + * Integration Connector resource. * @param toolNamePrefix(Optional) The tool name prefix. * @param toolInstructions(Optional) The tool instructions. */ @@ -64,6 +71,7 @@ public ApplicationIntegrationToolset( String connection, Map> entityOperations, List actions, + String serviceAccountJson, String toolNamePrefix, String toolInstructions) { this( @@ -74,9 +82,10 @@ public ApplicationIntegrationToolset( connection, entityOperations, actions, + serviceAccountJson, toolNamePrefix, toolInstructions, - new DefaultHttpExecutor()); + new DefaultHttpExecutor().createExecutor(serviceAccountJson)); } ApplicationIntegrationToolset( @@ -87,6 +96,7 @@ public ApplicationIntegrationToolset( String connection, Map> entityOperations, List actions, + String serviceAccountJson, String toolNamePrefix, String toolInstructions, HttpExecutor httpExecutor) { @@ -97,6 +107,7 @@ public ApplicationIntegrationToolset( this.connection = connection; this.entityOperations = entityOperations; this.actions = actions; + this.serviceAccountJson = serviceAccountJson; this.toolNamePrefix = toolNamePrefix; this.toolInstructions = toolInstructions; this.httpExecutor = httpExecutor; @@ -121,7 +132,7 @@ List getPathUrl(String openApiSchemaString) throws Exception { return pathUrls; } - public List getTools() throws Exception { + private List getAllTools() throws Exception { String openApiSchemaString = null; List tools = new ArrayList<>(); if (!isNullOrEmpty(this.integration)) { @@ -142,7 +153,15 @@ public List getTools() throws Exception { if (toolName != null) { tools.add( new IntegrationConnectorTool( - openApiSchemaString, pathUrl, toolName, "", null, null, null, this.httpExecutor)); + openApiSchemaString, + pathUrl, + toolName, + toolInstructions, + null, + null, + null, + this.serviceAccountJson, + this.httpExecutor)); } } } else if (!isNullOrEmpty(this.connection) @@ -182,6 +201,7 @@ public List getTools() throws Exception { connectionDetails.name, connectionDetails.serviceName, connectionDetails.host, + this.serviceAccountJson, this.httpExecutor)); } } @@ -193,4 +213,19 @@ public List getTools() throws Exception { return tools; } + + @Override + public Flowable getTools(@Nullable ReadonlyContext readonlyContext) { + try { + List allTools = getAllTools(); + return Flowable.fromIterable(allTools); + } catch (Exception e) { + return Flowable.error(e); + } + } + + @Override + public void close() throws Exception { + // Nothing to close. + } } diff --git a/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/IntegrationConnectorTool.java b/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/IntegrationConnectorTool.java index aee9ebfaa..86b39e732 100644 --- a/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/IntegrationConnectorTool.java +++ b/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/IntegrationConnectorTool.java @@ -1,6 +1,7 @@ package com.google.adk.tools.applicationintegrationtoolset; import static com.google.common.base.Strings.isNullOrEmpty; +import static java.nio.charset.StandardCharsets.UTF_8; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -15,7 +16,9 @@ import com.google.genai.types.FunctionDeclaration; import com.google.genai.types.Schema; import io.reactivex.rxjava3.core.Single; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -45,10 +48,27 @@ HttpResponse send(HttpRequest request, HttpResponse.BodyHandler respon throws IOException, InterruptedException; String getToken() throws IOException; + + public HttpExecutor createExecutor(String serviceAccountJson); } static class DefaultHttpExecutor implements HttpExecutor { private final HttpClient client = HttpClient.newHttpClient(); + private final String serviceAccountJson; + + /** Default constructor for when no service account is specified. */ + DefaultHttpExecutor() { + this(null); + } + + /** + * Constructor that accepts an optional service account JSON string. + * + * @param serviceAccountJson The service account key as a JSON string, or null. + */ + DefaultHttpExecutor(@Nullable String serviceAccountJson) { + this.serviceAccountJson = serviceAccountJson; + } @Override public HttpResponse send( @@ -59,12 +79,42 @@ public HttpResponse send( @Override public String getToken() throws IOException { - GoogleCredentials credentials = - GoogleCredentials.getApplicationDefault() - .createScoped("https://www.googleapis.com/auth/cloud-platform"); + GoogleCredentials credentials; + + if (this.serviceAccountJson != null && !this.serviceAccountJson.trim().isEmpty()) { + try (InputStream is = new ByteArrayInputStream(this.serviceAccountJson.getBytes(UTF_8))) { + credentials = + GoogleCredentials.fromStream(is) + .createScoped("https://www.googleapis.com/auth/cloud-platform"); + } catch (IOException e) { + throw new IOException("Failed to load credentials from service_account_json.", e); + } + } else { + try { + credentials = + GoogleCredentials.getApplicationDefault() + .createScoped("https://www.googleapis.com/auth/cloud-platform"); + } catch (IOException e) { + throw new IOException( + "Please provide a service account or configure Application Default Credentials. To" + + " set up ADC, see" + + " https://cloud.google.com/docs/authentication/external/set-up-adc.", + e); + } + } + credentials.refreshIfExpired(); return credentials.getAccessToken().getTokenValue(); } + + @Override + public HttpExecutor createExecutor(String serviceAccountJson) { + if (isNullOrEmpty(serviceAccountJson)) { + return new DefaultHttpExecutor(); + } else { + return new DefaultHttpExecutor(serviceAccountJson); + } + } } private static final ImmutableList EXCLUDE_FIELDS = @@ -75,7 +125,11 @@ public String getToken() throws IOException { /** Constructor for Application Integration Tool for integration */ IntegrationConnectorTool( - String openApiSpec, String pathUrl, String toolName, String toolDescription) { + String openApiSpec, + String pathUrl, + String toolName, + String toolDescription, + String serviceAccountJson) { this( openApiSpec, pathUrl, @@ -84,7 +138,8 @@ public String getToken() throws IOException { null, null, null, - new DefaultHttpExecutor()); + serviceAccountJson, + new DefaultHttpExecutor().createExecutor(serviceAccountJson)); } /** @@ -98,7 +153,8 @@ public String getToken() throws IOException { String toolDescription, String connectionName, String serviceName, - String host) { + String host, + String serviceAccountJson) { this( openApiSpec, pathUrl, @@ -107,7 +163,8 @@ public String getToken() throws IOException { connectionName, serviceName, host, - new DefaultHttpExecutor()); + serviceAccountJson, + new DefaultHttpExecutor().createExecutor(serviceAccountJson)); } IntegrationConnectorTool( @@ -118,6 +175,7 @@ public String getToken() throws IOException { @Nullable String connectionName, @Nullable String serviceName, @Nullable String host, + @Nullable String serviceAccountJson, HttpExecutor httpExecutor) { super(toolName, toolDescription); this.openApiSpec = openApiSpec; @@ -182,7 +240,7 @@ public Single> runAsync(Map args, ToolContex } private String executeIntegration(Map args) throws Exception { - String url = String.format("https://integrations.googleapis.com%s", pathUrl); + String url = String.format("https://integrations.googleapis.com%s", this.pathUrl); String jsonRequestBody; try { jsonRequestBody = OBJECT_MAPPER.writeValueAsString(args); diff --git a/core/src/test/java/com/google/adk/tools/applicationintegrationtoolset/ApplicationIntegrationToolsetTest.java b/core/src/test/java/com/google/adk/tools/applicationintegrationtoolset/ApplicationIntegrationToolsetTest.java index 5c07b3a80..be80b9ed3 100644 --- a/core/src/test/java/com/google/adk/tools/applicationintegrationtoolset/ApplicationIntegrationToolsetTest.java +++ b/core/src/test/java/com/google/adk/tools/applicationintegrationtoolset/ApplicationIntegrationToolsetTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.fasterxml.jackson.core.JsonParseException; @@ -14,6 +15,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.List; +import java.util.Objects; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -27,7 +29,6 @@ public final class ApplicationIntegrationToolsetTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @Mock private HttpExecutor mockHttpExecutor; - @Mock private HttpResponse mockHttpResponse; private static final String LOCATION = "us-central1"; private static final String PROJECT = "test-project"; @@ -36,7 +37,6 @@ public final class ApplicationIntegrationToolsetTest { "projects/test-project/locations/us-central1/connections/test-conn"; @Test - @SuppressWarnings("MockitoDoSetup") public void getTools_forIntegration_success() throws Exception { ApplicationIntegrationToolset toolset = new ApplicationIntegrationToolset( @@ -49,25 +49,28 @@ public void getTools_forIntegration_success() throws Exception { null, null, null, + null, mockHttpExecutor); String mockOpenApiSpecJson = "{\"openApiSpec\":" + "\"{\\\"paths\\\":{\\\"/p1?triggerId=api_trigger/trigger-1\\\":{\\\"post\\\":{\\\"operationId\\\":\\\"trigger-1\\\"}}}," + "\\\"components\\\":{\\\"schemas\\\":{}}}\"}"; + + @SuppressWarnings("unchecked") + HttpResponse mockHttpResponse = mock(HttpResponse.class); when(mockHttpExecutor.getToken()).thenReturn(MOCK_ACCESS_TOKEN); when(mockHttpResponse.statusCode()).thenReturn(200); when(mockHttpResponse.body()).thenReturn(mockOpenApiSpecJson); doReturn(mockHttpResponse).when(mockHttpExecutor).send(any(HttpRequest.class), any()); - List tools = toolset.getTools(); + List tools = toolset.getTools(null).toList().blockingGet(); assertThat(tools).hasSize(1); assertThat(tools.get(0).name()).isEqualTo("trigger-1"); } @Test - @SuppressWarnings("MockitoDoSetup") public void getTools_forConnection_success() throws Exception { ApplicationIntegrationToolset toolset = new ApplicationIntegrationToolset( @@ -78,6 +81,7 @@ public void getTools_forConnection_success() throws Exception { CONNECTION, ImmutableMap.of("Issue", ImmutableList.of("GET")), null, + null, "Jira", "Tools for Jira", mockHttpExecutor); @@ -89,6 +93,10 @@ public void getTools_forConnection_success() throws Exception { String mockEntitySchemaJson = "{\"name\": \"op1\", \"done\": true, \"response\": {\"jsonSchema\": {}, \"operations\":" + " [\"GET\"]}}"; + + @SuppressWarnings("unchecked") + HttpResponse mockHttpResponse = mock(HttpResponse.class); + when(mockHttpExecutor.getToken()).thenReturn(MOCK_ACCESS_TOKEN); when(mockHttpResponse.statusCode()).thenReturn(200); when(mockHttpResponse.body()) @@ -96,39 +104,56 @@ public void getTools_forConnection_success() throws Exception { .thenReturn(mockEntitySchemaJson); doReturn(mockHttpResponse).when(mockHttpExecutor).send(any(HttpRequest.class), any()); - List tools = toolset.getTools(); + List tools = toolset.getTools(null).toList().blockingGet(); assertThat(tools).hasSize(1); assertThat(tools.get(0).name()).isEqualTo("Jira_get_issue"); } @Test - public void getTools_invalidArguments_throwsException() { + public void getTools_invalidArguments_emitsError() { ApplicationIntegrationToolset toolset = new ApplicationIntegrationToolset( - PROJECT, LOCATION, null, null, CONNECTION, null, null, null, null, mockHttpExecutor); - - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, toolset::getTools); - assertThat(e) - .hasMessageThat() - .contains( - "Invalid request, Either integration or (connection and" - + " (entityOperations or actions)) should be provided."); + PROJECT, + LOCATION, + null, + null, + CONNECTION, + null, + null, + null, + null, + null, + mockHttpExecutor); + + toolset + .getTools(null) + .test() + .assertError( + throwable -> + throwable instanceof IllegalArgumentException + && Objects.equals( + throwable.getMessage(), + "Invalid request, Either integration or (connection and" + + " (entityOperations or actions)) should be provided.")); } @Test - public void getTools_forConnection_noEntityOperationsOrActions_throwsException() - throws Exception { + public void getTools_forConnection_noEntityOperationsOrActions_emitsError() { ApplicationIntegrationToolset toolset = new ApplicationIntegrationToolset( - PROJECT, LOCATION, null, null, null, null, null, null, null, mockHttpExecutor); - - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, toolset::getTools); - assertThat(e) - .hasMessageThat() - .contains( - "Invalid request, Either integration or (connection and" - + " (entityOperations or actions)) should be provided."); + PROJECT, LOCATION, null, null, null, null, null, null, null, null, mockHttpExecutor); + + toolset + .getTools(null) + .test() + .assertError( + throwable -> + throwable instanceof IllegalArgumentException + && Objects.equals( + throwable.getMessage(), + "Invalid request, Either integration or (connection and" + + " (entityOperations or actions)) should be provided.")); } @Test @@ -136,7 +161,8 @@ public void getPathUrl_success() throws Exception { String openApiSpec = "{\"openApiSpec\": \"{\\\"paths\\\":{\\\"/path1\\\":{},\\\"/path2\\\":{}}}\"}"; ApplicationIntegrationToolset toolset = - new ApplicationIntegrationToolset(null, null, null, null, null, null, null, null, null); + new ApplicationIntegrationToolset( + null, null, null, null, null, null, null, null, null, null); List paths = toolset.getPathUrl(openApiSpec); assertThat(paths).containsExactly("/path1", "/path2").inOrder(); } @@ -145,7 +171,8 @@ public void getPathUrl_success() throws Exception { public void getPathUrl_invalidJson_throwsException() { String openApiSpec = "{\"openApiSpec\": \"invalid json\"}"; ApplicationIntegrationToolset toolset = - new ApplicationIntegrationToolset(null, null, null, null, null, null, null, null, null); + new ApplicationIntegrationToolset( + null, null, null, null, null, null, null, null, null, null); assertThrows(JsonParseException.class, () -> toolset.getPathUrl(openApiSpec)); } } diff --git a/core/src/test/java/com/google/adk/tools/applicationintegrationtoolset/IntegrationConnectorToolTest.java b/core/src/test/java/com/google/adk/tools/applicationintegrationtoolset/IntegrationConnectorToolTest.java index 7c6ad3135..9ed3c67da 100644 --- a/core/src/test/java/com/google/adk/tools/applicationintegrationtoolset/IntegrationConnectorToolTest.java +++ b/core/src/test/java/com/google/adk/tools/applicationintegrationtoolset/IntegrationConnectorToolTest.java @@ -41,7 +41,15 @@ public final class IntegrationConnectorToolTest { private IntegrationConnectorTool integrationTool; private IntegrationConnectorTool connectorTool; private IntegrationConnectorTool connectorToolWithAction; + private IntegrationConnectorTool connectorToolWithServiceAccount; private static final String MOCK_ACCESS_TOKEN = "test-token"; + private static final String MOCK_ACCESS_TOKEN_2 = "test-token-2"; + private static final String MOCK_SERVICE_ACCOUNT_JSON = + "{\"type\": \"service_account\",\"project_id\": \"test-project\",\"private_key_data\":" + + " \"test-private-key-data\",\"client_email\": \"test-client-email\",\"client_id\":" + + " \"test-client-id\",\"auth_uri\":" + + " \"https://accounts.google.com/o/oauth2/auth\",\"token_uri\":" + + " \"https://oauth2.googleapis.com/token\",\"refresh_token\": \"1/1234567890\"}"; private static final String FAKE_PATH_URL = "/v2/projects/test-project/locations/test-region/integrations/test:execute?triggerId=api_trigger/Trigger1"; private static final String FAKE_TOOL_NAME = "Trigger1"; @@ -72,7 +80,8 @@ public final class IntegrationConnectorToolTest { + " for Executing" + " Action\\\"},\\\"paths\\\":{\\\"/v2/projects/test-project/locations/test-region/integrations/ExecuteConnection:execute\\\":{\\\"post\\\":{\\\"summary\\\":\\\"Execute" + " a custom" - + " action\\\",\\\"operationId\\\":\\\"execute_custom_action\\\",\\\"x-action\\\":\\\"CUSTOM_ACTION\\\",\\\"x-operation\\\":\\\"EXECUTE_ACTION\\\",\\\"requestBody\\\":{\\\"required\\\":true,\\\"content\\\":{\\\"application/json\\\":{\\\"schema\\\":{\\\"$ref\\\":\\\"#/components/schemas/ActionRequest\\\"}}}}}}},\\\"components\\\":{\\\"schemas\\\":{\\\"ActionRequest\\\":{\\\"type\\\":\\\"object\\\",\\\"required\\\":[\\\"connectionName\\\",\\\"action\\\",\\\"operation\\\"],\\\"properties\\\":{\\\"connectionName\\\":{\\\"type\\\":\\\"string\\\"},\\\"serviceName\\\":{\\\"type\\\":\\\"string\\\"},\\\"host\\\":{\\\"type\\\":\\\"string\\\"},\\\"action\\\":{\\\"type\\\":\\\"string\\\"},\\\"operation\\\":{\\\"type\\\":\\\"string\\\"}}}}}}\"}"; + + " action\\\",\\\"operationId\\\":\\\"execute_custom_action\\\",\\\"x-action\\\":\\\"CUSTOM_ACTION\\\"" + + " ,\\\"x-operation\\\":\\\"EXECUTE_ACTION\\\",\\\"requestBody\\\":{\\\"required\\\":true,\\\"content\\\":{\\\"application/json\\\":{\\\"schema\\\":{\\\"$ref\\\":\\\"#/components/schemas/ActionRequest\\\"}}}}}}},\\\"components\\\":{\\\"schemas\\\":{\\\"ActionRequest\\\":{\\\"type\\\":\\\"object\\\",\\\"required\\\":[\\\"connectionName\\\",\\\"action\\\",\\\"operation\\\"],\\\"properties\\\":{\\\"connectionName\\\":{\\\"type\\\":\\\"string\\\"},\\\"serviceName\\\":{\\\"type\\\":\\\"string\\\"},\\\"host\\\":{\\\"type\\\":\\\"string\\\"},\\\"action\\\":{\\\"type\\\":\\\"string\\\"},\\\"operation\\\":{\\\"type\\\":\\\"string\\\"}}}}}}\"}"; @Before public void setUp() { @@ -85,6 +94,7 @@ public void setUp() { null, null, null, + null, mockHttpExecutor); connectorTool = @@ -96,6 +106,7 @@ public void setUp() { "test-connection", "test-service", "test-host", + null, mockHttpExecutor); connectorToolWithAction = @@ -107,6 +118,19 @@ public void setUp() { "test-connection-action", "test-service-action", "test-host-action", + null, + mockHttpExecutor); + + connectorToolWithServiceAccount = + new IntegrationConnectorTool( + EXECUTE_ACTION_SPEC, + "/v2/projects/test-project/locations/test-region/integrations/ExecuteConnection:execute", + "execute_custom_action", + "A test tool for executing an action", + "test-connection-action", + "test-service-action", + "test-host-action", + MOCK_SERVICE_ACCOUNT_JSON, mockHttpExecutor); } @@ -168,6 +192,7 @@ public void integrationTool_declaration_operationNotFound_returnsEmpty() { null, null, null, + null, mockHttpExecutor); Optional declarationOpt = badTool.declaration(); @@ -246,6 +271,56 @@ public void connectorToolWithAction_runAsync_success() throws Exception { assertThat(inputArgs).doesNotContainKey("entity"); } + @Test + @SuppressWarnings("MockitoDoSetup") + public void runAsync_serviceAccountJson_throwsPermissionDenied() throws Exception { + String errorResponse = "{\"error\":{\"message\":\"Permission denied.\"}}"; + Map inputArgs = new HashMap<>(ImmutableMap.of("username", "testuser")); + IntegrationConnectorTool spyTool = spy(connectorToolWithServiceAccount); + when(mockHttpExecutor.getToken()).thenReturn(MOCK_ACCESS_TOKEN); + + when(mockHttpResponse.statusCode()).thenReturn(403); + when(mockHttpResponse.body()).thenReturn(errorResponse); + + doReturn(mockHttpResponse).when(mockHttpExecutor).send(any(HttpRequest.class), any()); + + String expectedErrorMessage = + "Error executing integration. Status: 403 , Response: " + errorResponse; + spyTool + .runAsync(inputArgs, mockToolContext) + .test() + .assertNoErrors() + .assertValue(ImmutableMap.of("error", expectedErrorMessage)); + } + + @Test + @SuppressWarnings("MockitoDoSetup") + public void connectorToolWithServiceAccount_runAsync_success() throws Exception { + String expectedResponse = "{\"action_result\":\"success\"}"; + IntegrationConnectorTool spyTool = spy(connectorToolWithServiceAccount); + var unused = spyTool.declaration(); + + Map inputArgs = new HashMap<>(); + inputArgs.put("payload", "data"); + + when(mockHttpExecutor.getToken()).thenReturn(MOCK_ACCESS_TOKEN_2); + when(mockHttpResponse.statusCode()).thenReturn(200); + when(mockHttpResponse.body()).thenReturn(expectedResponse); + doReturn(mockHttpResponse).when(mockHttpExecutor).send(any(), any()); + + TestObserver> testObserver = + spyTool.runAsync(inputArgs, mockToolContext).test(); + testObserver.assertNoErrors().assertValue(ImmutableMap.of("result", expectedResponse)); + + assertThat(inputArgs).containsEntry("connectionName", "test-connection-action"); + assertThat(inputArgs).containsEntry("serviceName", "test-service-action"); + assertThat(inputArgs).containsEntry("host", "test-host-action"); + assertThat(inputArgs).containsEntry("action", "CUSTOM_ACTION"); + assertThat(inputArgs).containsEntry("payload", "data"); + assertThat(inputArgs).containsEntry("operation", "EXECUTE_ACTION"); + assertThat(inputArgs).doesNotContainKey("entity"); + } + @Test @SuppressWarnings("MockitoDoSetup") public void runAsync_httpError_returnsErrorMap() throws Exception {