From 54823cfbdbf60e8bb1aaa273885f3470d3118cfd Mon Sep 17 00:00:00 2001 From: alexradzin Date: Mon, 30 Oct 2023 16:36:56 +0200 Subject: [PATCH] driver that can connect to both versions 1 and 2 --- .github/workflows/integration-test.yml | 69 ++++-- build.gradle | 5 +- .../java/integration/ConnectionInfo.java | 22 +- .../java/integration/IntegrationTest.java | 4 +- .../integration/tests/ConnectionTest.java | 2 + .../integration/tests/MissingDataTest.java | 6 +- .../tests/PreparedStatementTest.java | 32 +-- .../integration/tests/SystemEngineTest.java | 1 + .../java/com/firebolt/FireboltDriver.java | 3 +- .../jdbc/client/FireboltObjectMapper.java | 2 +- .../client/account/FireboltAccountClient.java | 113 ++++++++++ .../account/FireboltAccountRetriever.java | 1 + .../response/FireboltAccountResponse.java | 2 + ...FireboltDefaultDatabaseEngineResponse.java | 2 + .../FireboltAuthenticationClient.java | 10 +- ...ldServiceAccountAuthenticationRequest.java | 30 +++ .../ServiceAccountAuthenticationRequest.java | 9 +- ...UsernamePasswordAuthenticationRequest.java | 34 +++ .../client/query/StatementClientImpl.java | 17 +- .../com/firebolt/jdbc/connection/Engine.java | 1 + .../jdbc/connection/FireboltConnection.java | 137 ++++-------- ...ConnectionServiceSecretAuthentication.java | 114 ++++++++++ ...tConnectionUserPasswordAuthentication.java | 80 +++++++ .../com/firebolt/jdbc/connection/UrlUtil.java | 2 + .../settings/FireboltProperties.java | 9 +- .../FireboltAuthenticationService.java | 21 +- .../service/FireboltEngineApiService.java | 109 ++++++++++ ...ireboltEngineInformationSchemaService.java | 77 +++++++ .../jdbc/service/FireboltEngineService.java | 78 +------ .../com/firebolt/jdbc/util/PropertyUtil.java | 37 ++-- .../java/com/firebolt/FireboltDriverTest.java | 30 ++- .../account/FireboltAccountClientTest.java | 124 +++++++++++ .../FireboltAuthenticationClientTest.java | 11 +- .../client/query/StatementClientImplTest.java | 33 ++- ...ectionServiceSecretAuthenticationTest.java | 81 ++++++++ .../connection/FireboltConnectionTest.java | 144 +++++-------- ...nectionUserPasswordAuthenticationTest.java | 65 ++++++ .../settings/FireboltPropertiesTest.java | 39 +++- .../service/FireboltEngineApiServiceTest.java | 196 ++++++++++++++++++ ...ltEngineInformationSchemaServiceTest.java} | 59 +++--- .../service/FireboltStatementServiceTest.java | 25 ++- 41 files changed, 1451 insertions(+), 385 deletions(-) create mode 100644 src/main/java/com/firebolt/jdbc/client/account/FireboltAccountClient.java create mode 100644 src/main/java/com/firebolt/jdbc/client/authentication/OldServiceAccountAuthenticationRequest.java create mode 100644 src/main/java/com/firebolt/jdbc/client/authentication/UsernamePasswordAuthenticationRequest.java create mode 100644 src/main/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecretAuthentication.java create mode 100644 src/main/java/com/firebolt/jdbc/connection/FireboltConnectionUserPasswordAuthentication.java create mode 100644 src/main/java/com/firebolt/jdbc/service/FireboltEngineApiService.java create mode 100644 src/main/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaService.java create mode 100644 src/test/java/com/firebolt/jdbc/client/account/FireboltAccountClientTest.java create mode 100644 src/test/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecretAuthenticationTest.java create mode 100644 src/test/java/com/firebolt/jdbc/connection/FireboltConnectionUserPasswordAuthenticationTest.java create mode 100644 src/test/java/com/firebolt/jdbc/service/FireboltEngineApiServiceTest.java rename src/test/java/com/firebolt/jdbc/service/{FireboltEngineServiceTest.java => FireboltEngineInformationSchemaServiceTest.java} (63%) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 8b8ad9d6d..74bd19849 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -3,12 +3,16 @@ name: Run integration tests on: workflow_dispatch: inputs: - database: - description: 'Database - a new one will be created if not provided' + database1: + description: 'Database (v1) - a new one will be created if not provided' required: false default: '' - engine: - description: 'Engine - a new one will be created if not provided' + database2: + description: 'Database (v2) - a new one will be created if not provided' + required: false + default: '' + engine2: + description: 'Engine (v2) - a new one will be created if not provided' required: false account: description: 'Account' @@ -29,7 +33,7 @@ jobs: steps: - name: Validate database and engine - if: ${{ (github.event.inputs.database == '') != (github.event.inputs.engine == '') }} + if: ${{ (github.event.inputs.database2 == '') != (github.event.inputs.engine2 == '') }} uses: actions/github-script@v3 with: script: | @@ -49,15 +53,28 @@ jobs: - name: Determine env variables run: | if [ "${{ github.event.inputs.environment }}" == 'staging' ]; then + echo "USERNAME=${{ secrets.FIREBOLT_USERNAME_STAGING }}" >> "$GITHUB_ENV" + echo "PASSWORD=${{ secrets.FIREBOLT_PASSWORD_STAGING }}" >> "$GITHUB_ENV" echo "SERVICE_ACCOUNT_ID=${{ secrets.FIREBOLT_CLIENT_ID_STG_NEW_IDN }}" >> "$GITHUB_ENV" echo "SERVICE_ACCOUNT_SECRET=${{ secrets.FIREBOLT_CLIENT_SECRET_STG_NEW_IDN }}" >> "$GITHUB_ENV" else + echo "USERNAME=${{ secrets.FIREBOLT_USERNAME_DEV }}" >> "$GITHUB_ENV" + echo "PASSWORD=${{ secrets.FIREBOLT_PASSWORD_DEV }}" >> "$GITHUB_ENV" echo "SERVICE_ACCOUNT_ID=${{ secrets.FIREBOLT_CLIENT_ID_NEW_IDN }}" >> "$GITHUB_ENV" echo "SERVICE_ACCOUNT_SECRET=${{ secrets.FIREBOLT_CLIENT_SECRET_NEW_IDN }}" >> "$GITHUB_ENV" fi - - - name: Setup database and engine - id: setup + - name: Setup database and engine (v1) + id: setup1 + if: ${{ github.event.inputs.database == '' }} + uses: firebolt-db/integration-testing-setup@v1 + with: + firebolt-username: ${{ env.USERNAME }} + firebolt-password: ${{ env.PASSWORD }} + api-endpoint: "api.${{ github.event.inputs.environment }}.firebolt.io" + region: "us-east-1" + instance-type: "B2" + - name: Setup database and engine (v2) + id: setup2 if: ${{ github.event.inputs.database == '' }} uses: firebolt-db/integration-testing-setup@v2 with: @@ -67,26 +84,38 @@ jobs: api-endpoint: "api.${{ github.event.inputs.environment }}.firebolt.io" instance-type: "B2" - - name: Determine database name - id: find-database-name + - name: Determine database (v1) name + id: find-database-name1 + run: | + if ! [[ -z "${{ github.event.inputs.database1 }}" ]]; then + echo "database_name1=${{ github.event.inputs.database1 }}" >> $GITHUB_OUTPUT + else + echo "database_name1=${{ steps.setup.outputs.database_name }}" >> $GITHUB_OUTPUT + fi + + - name: Determine database (v2) name + id: find-database-name2 run: | - if ! [[ -z "${{ github.event.inputs.database }}" ]]; then - echo "database_name=${{ github.event.inputs.database }}" >> $GITHUB_OUTPUT + if ! [[ -z "${{ github.event.inputs.database2 }}" ]]; then + echo "database_name1=${{ github.event.inputs.database2 }}" >> $GITHUB_OUTPUT else - echo "database_name=${{ steps.setup.outputs.database_name }}" >> $GITHUB_OUTPUT + echo "database_name1=${{ steps.setup.outputs.database_name2 }}" >> $GITHUB_OUTPUT fi - - name: Determine engine name - id: find-engine-name + - name: Determine engine (v2) name + id: find-engine-name2 run: | - if ! [[ -z "${{ github.event.inputs.engine }}" ]]; then - echo "engine_name=${{ github.event.inputs.engine }}" >> $GITHUB_OUTPUT + if ! [[ -z "${{ github.event.inputs.engine2 }}" ]]; then + echo "engine_name2=${{ github.event.inputs.engine2 }}" >> $GITHUB_OUTPUT else - echo "engine_name=${{ steps.setup.outputs.engine_name }}" >> $GITHUB_OUTPUT + echo "engine_name2=${{ steps.setup.outputs.engine_name2 }}" >> $GITHUB_OUTPUT fi - - name: Run integration tests - run: ./gradlew integrationTest -Ddb=${{ steps.find-database-name.outputs.database_name }} -Denv=${{ github.event.inputs.environment }} -Dclient_secret="${{ env.SERVICE_ACCOUNT_SECRET }}" -Dclient_id="${{ env.SERVICE_ACCOUNT_ID }}" -Daccount="${{ github.event.inputs.account }}" -Dengine="${{ steps.find-engine-name.outputs.engine_name }}" + - name: Run integration tests (v1) + run: ./gradlew integrationTest -Ddb=${{ steps.find-database-name.outputs.database_name1 }} -Dapi=api.${{ github.event.inputs.environment }}.firebolt.io -Dpassword="${{ env.SERVICE_ACCOUNT_SECRET }}" -Duser="${{ env.SERVICE_ACCOUNT_ID }}" -DincludeTags="common,v1" + + - name: Run integration tests (v2) + run: ./gradlew integrationTest -Ddb=${{ steps.find-database-name.outputs.database_name2 }} -Denv=${{ github.event.inputs.environment }} -Dclient_secret="${{ env.SERVICE_ACCOUNT_SECRET }}" -Dclient_id="${{ env.SERVICE_ACCOUNT_ID }}" -Daccount="${{ github.event.inputs.account }}" -Dengine="${{ steps.find-engine-name.outputs.engine_name2 }}" -DincludeTags="common,v2" - name: "Foresight: Analyze Test Results" uses: runforesight/foresight-test-kit-action@v1 diff --git a/build.gradle b/build.gradle index 020963f66..a96910f9c 100644 --- a/build.gradle +++ b/build.gradle @@ -106,7 +106,10 @@ test { tasks.register('integrationTest', Test) { description = 'Runs integration tests.' - useJUnitPlatform() + useJUnitPlatform() { + includeTags(System.getProperty("includeTags", "common").split(",")) + excludeTags(System.getProperty("excludeTags", "nothing").split(",")) + } reports { junitXml { outputPerTestCase = true // defaults to false diff --git a/src/integrationTest/java/integration/ConnectionInfo.java b/src/integrationTest/java/integration/ConnectionInfo.java index 8ce560933..81a56c10f 100644 --- a/src/integrationTest/java/integration/ConnectionInfo.java +++ b/src/integrationTest/java/integration/ConnectionInfo.java @@ -2,6 +2,7 @@ import java.util.Objects; import java.util.Optional; +import java.util.function.Supplier; import java.util.stream.Stream; import static java.lang.String.format; @@ -18,6 +19,8 @@ public class ConnectionInfo { private final String database; private final String account; private final String engine; + private final String api; + private final Supplier jdbcUrlSupplier; private ConnectionInfo() { this( @@ -26,17 +29,20 @@ private ConnectionInfo() { getProperty("env"), getProperty("db"), getProperty("account"), - getProperty("engine") + getProperty("engine"), + getProperty("api") ); } - public ConnectionInfo(String principal, String secret, String env, String database, String account, String engine) { + public ConnectionInfo(String principal, String secret, String env, String database, String account, String engine, String api) { this.principal = principal; this.secret = secret; this.env = env; this.database = database; this.account = account; this.engine = engine; + this.api = api; + jdbcUrlSupplier = api == null ? this::toJdbcUrl2 : this::toJdbcUrl1; } public static ConnectionInfo getInstance() { @@ -78,7 +84,19 @@ public String getEngine() { return engine; } + public String getApi() { + return api; + } + public String toJdbcUrl() { + return jdbcUrlSupplier.get(); + } + + private String toJdbcUrl1() { + return "jdbc:firebolt://" + api + "/" + database + (engine == null ? "" : "?engine=" + engine); + } + + private String toJdbcUrl2() { String params = Stream.of(param("env", env), param("engine", engine), param("account", account)).filter(Objects::nonNull).collect(joining("&")); if (!params.isEmpty()) { params = "?" + params; diff --git a/src/integrationTest/java/integration/IntegrationTest.java b/src/integrationTest/java/integration/IntegrationTest.java index 441a1b19e..5980f2929 100644 --- a/src/integrationTest/java/integration/IntegrationTest.java +++ b/src/integrationTest/java/integration/IntegrationTest.java @@ -3,6 +3,7 @@ import com.firebolt.jdbc.client.HttpClientConfig; import lombok.CustomLog; import lombok.SneakyThrows; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.TestInstance; import java.io.InputStream; @@ -17,6 +18,7 @@ @CustomLog @TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Tag("common") public abstract class IntegrationTest { private static final String JDBC_URL_PREFIX = "jdbc:firebolt:"; @@ -38,7 +40,7 @@ protected Connection createConnection() throws SQLException { protected Connection createConnection(String engine) throws SQLException { ConnectionInfo current = integration.ConnectionInfo.getInstance(); ConnectionInfo updated = new ConnectionInfo(current.getPrincipal(), current.getSecret(), - current.getEnv(), current.getDatabase(), current.getAccount(), engine); + current.getEnv(), current.getDatabase(), current.getAccount(), engine, current.getApi()); return DriverManager.getConnection(updated.toJdbcUrl(), integration.ConnectionInfo.getInstance().getPrincipal(), integration.ConnectionInfo.getInstance().getSecret()); diff --git a/src/integrationTest/java/integration/tests/ConnectionTest.java b/src/integrationTest/java/integration/tests/ConnectionTest.java index 2169c76e4..c26eb4e45 100644 --- a/src/integrationTest/java/integration/tests/ConnectionTest.java +++ b/src/integrationTest/java/integration/tests/ConnectionTest.java @@ -1,6 +1,7 @@ package integration.tests; import integration.ConnectionInfo; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -21,6 +22,7 @@ public class ConnectionTest { * @throws SQLException if something went wrong */ @Test + @Tag("v2") void connectionWithAdditionalProperties() throws SQLException { ConnectionInfo params = integration.ConnectionInfo.getInstance(); String url = format("jdbc:firebolt:%s?env=%s&engine=%s&account=%s&use_standard_sql=1", params.getDatabase(), params.getEnv(), params.getEngine(), params.getAccount()); diff --git a/src/integrationTest/java/integration/tests/MissingDataTest.java b/src/integrationTest/java/integration/tests/MissingDataTest.java index 497d3b9ae..f722920e6 100644 --- a/src/integrationTest/java/integration/tests/MissingDataTest.java +++ b/src/integrationTest/java/integration/tests/MissingDataTest.java @@ -2,6 +2,7 @@ import integration.ConnectionInfo; import integration.IntegrationTest; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -13,8 +14,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows; @TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class MissingDataTest extends IntegrationTest { +public class MissingDataTest { @Test + @Tag("v2") void missingAccount() throws SQLException { ConnectionInfo current = integration.ConnectionInfo.getInstance(); try (Connection good = DriverManager.getConnection(current.toJdbcUrl(), current.getPrincipal(), current.getSecret())) { @@ -22,7 +24,7 @@ void missingAccount() throws SQLException { } ConnectionInfo noAccount = new ConnectionInfo(current.getPrincipal(), current.getSecret(), - current.getEnv(), current.getDatabase(), null, current.getEngine()); + current.getEnv(), current.getDatabase(), null, current.getEngine(), current.getApi()); assertThrows(SQLException.class, () -> DriverManager.getConnection(noAccount.toJdbcUrl(), noAccount.getPrincipal(), noAccount.getSecret())); } } diff --git a/src/integrationTest/java/integration/tests/PreparedStatementTest.java b/src/integrationTest/java/integration/tests/PreparedStatementTest.java index 4a775cd20..76bf6384b 100644 --- a/src/integrationTest/java/integration/tests/PreparedStatementTest.java +++ b/src/integrationTest/java/integration/tests/PreparedStatementTest.java @@ -1,28 +1,32 @@ package integration.tests; -import static java.sql.Statement.SUCCESS_NO_INFO; -import static org.junit.jupiter.api.Assertions.*; - -import java.sql.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import com.firebolt.jdbc.QueryResult; import com.firebolt.jdbc.resultset.FireboltResultSet; import com.firebolt.jdbc.testutils.AssertionUtil; import com.firebolt.jdbc.type.FireboltDataType; - import integration.ConnectionInfo; import integration.IntegrationTest; import lombok.Builder; import lombok.CustomLog; import lombok.Value; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static java.sql.Statement.SUCCESS_NO_INFO; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; @CustomLog class PreparedStatementTest extends IntegrationTest { diff --git a/src/integrationTest/java/integration/tests/SystemEngineTest.java b/src/integrationTest/java/integration/tests/SystemEngineTest.java index eb348e22e..bded9f748 100644 --- a/src/integrationTest/java/integration/tests/SystemEngineTest.java +++ b/src/integrationTest/java/integration/tests/SystemEngineTest.java @@ -38,6 +38,7 @@ void afterAll() { } @Test + @Tag("slow") void shouldExecuteEngineManagementQueries() throws SQLException { try (Connection connection = this.createConnection(null)) { List queries = Arrays.asList(String.format("CREATE DATABASE IF NOT EXISTS %s", DATABASE_NAME), diff --git a/src/main/java/com/firebolt/FireboltDriver.java b/src/main/java/com/firebolt/FireboltDriver.java index 1794d615b..a81fcfcb7 100644 --- a/src/main/java/com/firebolt/FireboltDriver.java +++ b/src/main/java/com/firebolt/FireboltDriver.java @@ -4,6 +4,7 @@ import java.util.Properties; import java.util.logging.Logger; +import com.firebolt.jdbc.connection.FireboltConnectionServiceSecretAuthentication; import org.apache.commons.lang3.StringUtils; import com.firebolt.jdbc.util.PropertyUtil; @@ -29,7 +30,7 @@ public class FireboltDriver implements Driver { @Override public Connection connect(String url, Properties connectionSettings) throws SQLException { - return acceptsURL(url) ? new FireboltConnection(url, connectionSettings) : null; + return acceptsURL(url) ? FireboltConnection.create(url, connectionSettings) : null; } @Override diff --git a/src/main/java/com/firebolt/jdbc/client/FireboltObjectMapper.java b/src/main/java/com/firebolt/jdbc/client/FireboltObjectMapper.java index dbd2c8433..645f7e23f 100644 --- a/src/main/java/com/firebolt/jdbc/client/FireboltObjectMapper.java +++ b/src/main/java/com/firebolt/jdbc/client/FireboltObjectMapper.java @@ -10,7 +10,7 @@ public class FireboltObjectMapper { private FireboltObjectMapper() { } - public static com.fasterxml.jackson.databind.ObjectMapper getInstance() { + public static ObjectMapper getInstance() { return MAPPER; } } diff --git a/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountClient.java b/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountClient.java new file mode 100644 index 000000000..3f4aaf768 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountClient.java @@ -0,0 +1,113 @@ +package com.firebolt.jdbc.client.account; + +import java.io.IOException; + +import org.apache.commons.lang3.StringUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.firebolt.jdbc.client.FireboltClient; +import com.firebolt.jdbc.client.account.response.FireboltAccountResponse; +import com.firebolt.jdbc.client.account.response.FireboltDefaultDatabaseEngineResponse; +import com.firebolt.jdbc.client.account.response.FireboltEngineIdResponse; +import com.firebolt.jdbc.client.account.response.FireboltEngineResponse; +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.exception.ExceptionType; +import com.firebolt.jdbc.exception.FireboltException; + +import lombok.CustomLog; +import okhttp3.OkHttpClient; + +import static java.lang.String.format; + +@CustomLog +public class FireboltAccountClient extends FireboltClient { + + private static final String GET_ACCOUNT_ID_URI = "%s/iam/v2/accounts:getIdByName?accountName=%s"; + private static final String URI_SUFFIX_ENGINE_AND_ACCOUNT_ID_BY_ENGINE_NAME = "engines:getIdByName?engine_name="; + private static final String URI_SUFFIX_ACCOUNT_ENGINE_INFO_BY_ENGINE_ID = "engines/"; + private static final String URI_SUFFIX_DATABASE_INFO_URL = "engines:getURLByDatabaseName?databaseName="; + private static final String URI_PREFIX_WITH_ACCOUNT_RESOURCE = "%s/core/v1/accounts/%s/%s"; + private static final String URI_PREFIX_WITHOUT_ACCOUNT_RESOURCE = "%s/core/v1/account/%s"; + + public FireboltAccountClient(OkHttpClient httpClient, ObjectMapper objectMapper, + FireboltConnection fireboltConnection, String customDrivers, String customClients) { + super(httpClient, objectMapper, fireboltConnection, customDrivers, customClients); + } + + /** + * Returns the account + * + * @param host the host + * @param account the name of the account + * @param accessToken the access token + * @return the account + */ + public FireboltAccountResponse getAccount(String host, String account, String accessToken) + throws FireboltException, IOException { + String uri = format(GET_ACCOUNT_ID_URI, host, account); + return getResource(uri, host, accessToken, FireboltAccountResponse.class); + } + + /** + * Returns an engine + * + * @param host the host + * @param accountId the id of the account + * @param engineName the engine name + * @param engineId the engine id + * @param accessToken the access token + * @return the engine + */ + public FireboltEngineResponse getEngine(String host, String accountId, String engineName, String engineId, + String accessToken) throws FireboltException, IOException { + String uri = createAccountUri(accountId, host, URI_SUFFIX_ACCOUNT_ENGINE_INFO_BY_ENGINE_ID + engineId); + return getResource(uri, host, accessToken, FireboltEngineResponse.class, format("The address of the engine with name %s and id %s could not be found", engineName, engineId)); + } + + /** + * Returns the default engine of the database + * + * @param host the host + * @param accountId the account id + * @param dbName the name of the database + * @param accessToken the access token + * @return the default engine for the database + */ + public FireboltDefaultDatabaseEngineResponse getDefaultEngineByDatabaseName(String host, String accountId, String dbName, + String accessToken) throws FireboltException, IOException { + String uri = createAccountUri(accountId, host, URI_SUFFIX_DATABASE_INFO_URL + dbName); + return getResource(uri, host, accessToken, FireboltDefaultDatabaseEngineResponse.class, format("The database with the name %s could not be found", dbName)); + } + + /** + * Returns the engine id + * + * @param host the host + * @param accountId the account id + * @param engineName the name of the engine + * @param accessToken the access token + * @return the engine id + */ + public FireboltEngineIdResponse getEngineId(String host, String accountId, String engineName, String accessToken) + throws FireboltException, IOException { + String uri = createAccountUri(accountId, host, URI_SUFFIX_ENGINE_AND_ACCOUNT_ID_BY_ENGINE_NAME + engineName); + return getResource(uri, host, accessToken, FireboltEngineIdResponse.class, format("The engine %s could not be found", engineName)); + } + + + private R getResource(String uri, String host, String accessToken, Class responseType, String notFoundErrorMessage) throws FireboltException, IOException { + try { + return getResource(uri, host, accessToken, responseType); + } catch (FireboltException exception) { + if (exception.getType() == ExceptionType.RESOURCE_NOT_FOUND) { + throw new FireboltException(notFoundErrorMessage, exception, ExceptionType.RESOURCE_NOT_FOUND); + } + throw exception; + } + } + + private String createAccountUri(String account, String host, String suffix) { + return StringUtils.isEmpty(account) ? format(URI_PREFIX_WITHOUT_ACCOUNT_RESOURCE, host, suffix) : format(URI_PREFIX_WITH_ACCOUNT_RESOURCE, host, account, suffix); + } + +} \ No newline at end of file diff --git a/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountRetriever.java b/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountRetriever.java index c99efc6f7..f9dbbe4f9 100644 --- a/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountRetriever.java +++ b/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountRetriever.java @@ -16,6 +16,7 @@ public class FireboltAccountRetriever extends FireboltClient { private final String path; private final Class type; + @SuppressWarnings("java:S107") //Number of parameters (8) > max (7). This is the price of the immutability public FireboltAccountRetriever(OkHttpClient httpClient, ObjectMapper objectMapper, FireboltConnection connection, String customDrivers, String customClients, String host, String path, Class type) { super(httpClient, objectMapper, connection, customDrivers, customClients); this.host = host; diff --git a/src/main/java/com/firebolt/jdbc/client/account/response/FireboltAccountResponse.java b/src/main/java/com/firebolt/jdbc/client/account/response/FireboltAccountResponse.java index effb2e4cd..43a747568 100644 --- a/src/main/java/com/firebolt/jdbc/client/account/response/FireboltAccountResponse.java +++ b/src/main/java/com/firebolt/jdbc/client/account/response/FireboltAccountResponse.java @@ -2,10 +2,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Value; @Value +@AllArgsConstructor @Builder public class FireboltAccountResponse { @JsonProperty("account_id") diff --git a/src/main/java/com/firebolt/jdbc/client/account/response/FireboltDefaultDatabaseEngineResponse.java b/src/main/java/com/firebolt/jdbc/client/account/response/FireboltDefaultDatabaseEngineResponse.java index e4fa1a885..2039c4337 100644 --- a/src/main/java/com/firebolt/jdbc/client/account/response/FireboltDefaultDatabaseEngineResponse.java +++ b/src/main/java/com/firebolt/jdbc/client/account/response/FireboltDefaultDatabaseEngineResponse.java @@ -2,10 +2,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Value; @Value +@AllArgsConstructor @Builder public class FireboltDefaultDatabaseEngineResponse { @JsonProperty("engine_url") diff --git a/src/main/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClient.java b/src/main/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClient.java index f03fa373a..c72dcb101 100644 --- a/src/main/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClient.java +++ b/src/main/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClient.java @@ -14,10 +14,10 @@ import java.io.IOException; @CustomLog -public class FireboltAuthenticationClient extends FireboltClient { +public abstract class FireboltAuthenticationClient extends FireboltClient { - public FireboltAuthenticationClient(OkHttpClient httpClient, ObjectMapper objectMapper, - FireboltConnection connection, String customDrivers, String customClients) { + protected FireboltAuthenticationClient(OkHttpClient httpClient, ObjectMapper objectMapper, + FireboltConnection connection, String customDrivers, String customClients) { super(httpClient, objectMapper, connection, customDrivers, customClients); } @@ -32,7 +32,7 @@ public FireboltAuthenticationClient(OkHttpClient httpClient, ObjectMapper object */ public FireboltConnectionTokens postConnectionTokens(String host, String user, String password, String environment) throws IOException, FireboltException { - AuthenticationRequest authenticationRequest = new ServiceAccountAuthenticationRequest(user, password, environment); + AuthenticationRequest authenticationRequest = getAuthenticationRequest(user, password, host, environment); String uri = authenticationRequest.getUri(); log.debug("Creating connection with url {}", uri); Request request = this.createPostRequest(uri, authenticationRequest.getRequestBody()); @@ -63,4 +63,6 @@ private void logIfPresent(String token, String message) { log.debug(message); } } + + protected abstract AuthenticationRequest getAuthenticationRequest(String username, String password, String host, String environment); } diff --git a/src/main/java/com/firebolt/jdbc/client/authentication/OldServiceAccountAuthenticationRequest.java b/src/main/java/com/firebolt/jdbc/client/authentication/OldServiceAccountAuthenticationRequest.java new file mode 100644 index 000000000..0279dc0ec --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/client/authentication/OldServiceAccountAuthenticationRequest.java @@ -0,0 +1,30 @@ +package com.firebolt.jdbc.client.authentication; + +import lombok.AllArgsConstructor; +import okhttp3.FormBody; +import okhttp3.RequestBody; + +@AllArgsConstructor +public class OldServiceAccountAuthenticationRequest implements AuthenticationRequest { + private static final String CLIENT_CREDENTIALS = "client_credentials"; + private static final String GRAND_TYPE_FIELD_NAME = "grant_type"; + private static final String CLIENT_ID_FIELD_NAME = "client_id"; + private static final String CLIENT_SECRET_FIELD_NAME = "client_secret"; + private static final String AUTH_URL = "%s/auth/v1/token"; + private final String id; + private final String secret; + private final String host; + + public RequestBody getRequestBody() { + return new FormBody.Builder() + .addEncoded(CLIENT_ID_FIELD_NAME, id) + .addEncoded(CLIENT_SECRET_FIELD_NAME, secret) + .add(GRAND_TYPE_FIELD_NAME, CLIENT_CREDENTIALS) + .build(); + } + + @Override + public String getUri() { + return String.format(AUTH_URL, host); + } +} \ No newline at end of file diff --git a/src/main/java/com/firebolt/jdbc/client/authentication/ServiceAccountAuthenticationRequest.java b/src/main/java/com/firebolt/jdbc/client/authentication/ServiceAccountAuthenticationRequest.java index 50ca3e569..89f70aea6 100644 --- a/src/main/java/com/firebolt/jdbc/client/authentication/ServiceAccountAuthenticationRequest.java +++ b/src/main/java/com/firebolt/jdbc/client/authentication/ServiceAccountAuthenticationRequest.java @@ -3,6 +3,8 @@ import okhttp3.FormBody; import okhttp3.RequestBody; +import static java.lang.String.format; + public class ServiceAccountAuthenticationRequest implements AuthenticationRequest { private static final String AUDIENCE_FIELD_NAME = "audience"; @@ -28,12 +30,13 @@ public RequestBody getRequestBody() { return new FormBody.Builder() .add(AUDIENCE_FIELD_NAME, AUDIENCE_FIELD_VALUE) .add(GRAND_TYPE_FIELD_NAME, GRAND_TYPE_FIELD_VALUE) - .add(CLIENT_ID_FIELD_NAME, clientId) - .add(CLIENT_SECRET_FIELD_NAME, clientSecret).build(); + .addEncoded(CLIENT_ID_FIELD_NAME, clientId) + .addEncoded(CLIENT_SECRET_FIELD_NAME, clientSecret) + .build(); } @Override public String getUri() { - return String.format(AUTH_URL, environment); + return format(AUTH_URL, environment); } } diff --git a/src/main/java/com/firebolt/jdbc/client/authentication/UsernamePasswordAuthenticationRequest.java b/src/main/java/com/firebolt/jdbc/client/authentication/UsernamePasswordAuthenticationRequest.java new file mode 100644 index 000000000..1957f3146 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/client/authentication/UsernamePasswordAuthenticationRequest.java @@ -0,0 +1,34 @@ +package com.firebolt.jdbc.client.authentication; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.firebolt.jdbc.client.FireboltObjectMapper; +import lombok.AllArgsConstructor; +import okhttp3.MediaType; +import okhttp3.RequestBody; + +import java.util.Map; + +import static java.lang.String.format; + +@AllArgsConstructor +public class UsernamePasswordAuthenticationRequest implements AuthenticationRequest { + private static final String AUTH_URL = "%s/auth/v1/login"; + private static final String USERNAME_FIELD_NAME = "username"; + private static final String PASSWORD_FIELD_NAME = "password"; + private static final MediaType JSON = MediaType.parse("application/json"); + private final ObjectMapper objectMapper = FireboltObjectMapper.getInstance(); + private final String username; + private final String password; + private final String host; + + public RequestBody getRequestBody() throws JsonProcessingException { + Map loginDetailsMap = Map.of(USERNAME_FIELD_NAME, username, PASSWORD_FIELD_NAME, password); + return RequestBody.create(objectMapper.writeValueAsString(loginDetailsMap), JSON); + } + + @Override + public String getUri() { + return format(AUTH_URL, host); + } +} diff --git a/src/main/java/com/firebolt/jdbc/client/query/StatementClientImpl.java b/src/main/java/com/firebolt/jdbc/client/query/StatementClientImpl.java index 934b12b4b..c58bc9765 100644 --- a/src/main/java/com/firebolt/jdbc/client/query/StatementClientImpl.java +++ b/src/main/java/com/firebolt/jdbc/client/query/StatementClientImpl.java @@ -75,15 +75,13 @@ public InputStream executeSqlStatement(@NonNull StatementInfoWrapper statementIn return executeSqlStatementWithRetryOnUnauthorized(statementInfoWrapper, connectionProperties, formattedStatement, uri); } catch (FireboltException e) { throw e; + } catch (StreamResetException e) { + String errorMessage = format("Error executing statement with id %s: %s", statementInfoWrapper.getId(), formattedStatement); + throw new FireboltException(errorMessage, e, ExceptionType.CANCELED); } catch (Exception e) { - String errorMessage = format("Error executing statement with id %s: %s", - statementInfoWrapper.getId(), formattedStatement); - if (e instanceof StreamResetException) { - throw new FireboltException(errorMessage, e, ExceptionType.CANCELED); - } + String errorMessage = format("Error executing statement with id %s: %s", statementInfoWrapper.getId(), formattedStatement); throw new FireboltException(errorMessage, e); } - } private InputStream executeSqlStatementWithRetryOnUnauthorized(@NonNull StatementInfoWrapper statementInfoWrapper, @@ -210,7 +208,9 @@ private Map getAllParameters(FireboltProperties fireboltProperti getResponseFormatParameter(statementInfoWrapper.getType() == StatementType.QUERY, isLocalDb) .ifPresent(format -> params.put(format.getLeft(), format.getRight())); if (systemEngine) { - params.put(FireboltQueryParameterKey.ACCOUNT_ID.getKey(), fireboltProperties.getAccountId()); + if (fireboltProperties.getAccountId() != null) { + params.put(FireboltQueryParameterKey.ACCOUNT_ID.getKey(), fireboltProperties.getAccountId()); + } } else { params.put(FireboltQueryParameterKey.QUERY_ID.getKey(), statementInfoWrapper.getId()); params.put(FireboltQueryParameterKey.COMPRESS.getKey(), fireboltProperties.isCompress() ? "1" : "0"); @@ -225,7 +225,8 @@ private Map getAllParameters(FireboltProperties fireboltProperti } private Optional> getResponseFormatParameter(boolean isQuery, boolean isLocalDb) { - return isQuery ? Optional.of(Pair.of((isLocalDb ? DEFAULT_FORMAT : OUTPUT_FORMAT).getKey(), TAB_SEPARATED_WITH_NAMES_AND_TYPES_FORMAT)) : Optional.empty(); + FireboltQueryParameterKey format = isLocalDb ? DEFAULT_FORMAT : OUTPUT_FORMAT; + return isQuery ? Optional.of(Pair.of(format.getKey(), TAB_SEPARATED_WITH_NAMES_AND_TYPES_FORMAT)) : Optional.empty(); } private Map getCancelParameters(String statementId) { diff --git a/src/main/java/com/firebolt/jdbc/connection/Engine.java b/src/main/java/com/firebolt/jdbc/connection/Engine.java index 47f355b60..17b80429f 100644 --- a/src/main/java/com/firebolt/jdbc/connection/Engine.java +++ b/src/main/java/com/firebolt/jdbc/connection/Engine.java @@ -10,4 +10,5 @@ public class Engine { private final String status; private final String name; private final String database; + private final String id; } diff --git a/src/main/java/com/firebolt/jdbc/connection/FireboltConnection.java b/src/main/java/com/firebolt/jdbc/connection/FireboltConnection.java index 84234cccc..12f0740c3 100644 --- a/src/main/java/com/firebolt/jdbc/connection/FireboltConnection.java +++ b/src/main/java/com/firebolt/jdbc/connection/FireboltConnection.java @@ -5,10 +5,7 @@ import com.firebolt.jdbc.annotation.NotImplemented; import com.firebolt.jdbc.client.FireboltObjectMapper; import com.firebolt.jdbc.client.HttpClientConfig; -import com.firebolt.jdbc.client.account.FireboltAccount; -import com.firebolt.jdbc.client.account.FireboltAccountRetriever; import com.firebolt.jdbc.client.authentication.FireboltAuthenticationClient; -import com.firebolt.jdbc.client.gateway.GatewayUrlResponse; import com.firebolt.jdbc.client.query.StatementClientImpl; import com.firebolt.jdbc.connection.settings.FireboltProperties; import com.firebolt.jdbc.exception.ExceptionType; @@ -17,10 +14,7 @@ import com.firebolt.jdbc.exception.FireboltUnsupportedOperationException; import com.firebolt.jdbc.metadata.FireboltDatabaseMetadata; import com.firebolt.jdbc.metadata.FireboltSystemEngineDatabaseMetadata; -import com.firebolt.jdbc.service.FireboltAccountIdService; import com.firebolt.jdbc.service.FireboltAuthenticationService; -import com.firebolt.jdbc.service.FireboltEngineService; -import com.firebolt.jdbc.service.FireboltGatewayUrlService; import com.firebolt.jdbc.service.FireboltStatementService; import com.firebolt.jdbc.statement.FireboltStatement; import com.firebolt.jdbc.statement.preparedstatement.FireboltPreparedStatement; @@ -30,7 +24,6 @@ import lombok.CustomLog; import lombok.NonNull; import okhttp3.OkHttpClient; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import java.io.IOException; @@ -58,75 +51,78 @@ import java.util.Optional; import java.util.Properties; import java.util.concurrent.Executor; +import java.util.regex.Pattern; import static java.lang.String.format; import static java.sql.ResultSet.CLOSE_CURSORS_AT_COMMIT; import static java.sql.ResultSet.TYPE_FORWARD_ONLY; @CustomLog -public class FireboltConnection implements Connection { +public abstract class FireboltConnection implements Connection { private final FireboltAuthenticationService fireboltAuthenticationService; private final FireboltStatementService fireboltStatementService; - private final FireboltEngineService fireboltEngineService; - private final FireboltGatewayUrlService fireboltGatewayUrlService; - private final FireboltAccountIdService fireboltAccountIdService; - private final String httpConnectionUrl; + protected final String httpConnectionUrl; private final List statements; private final int connectionTimeout; - private final boolean systemEngine; private boolean closed = true; - private FireboltProperties sessionProperties; + protected FireboltProperties sessionProperties; private int networkTimeout; //Properties that are used at the beginning of the connection for authentication - private final FireboltProperties loginProperties; + protected final FireboltProperties loginProperties; - public FireboltConnection(@NonNull String url, Properties connectionSettings, - FireboltAuthenticationService fireboltAuthenticationService, - FireboltGatewayUrlService fireboltGatewayUrlService, - FireboltStatementService fireboltStatementService, - FireboltEngineService fireboltEngineService, - FireboltAccountIdService fireboltAccountIdService) throws SQLException { - this.loginProperties = this.extractFireboltProperties(url, connectionSettings); + protected FireboltConnection(@NonNull String url, + Properties connectionSettings, + FireboltAuthenticationService fireboltAuthenticationService, + FireboltStatementService fireboltStatementService) { + this.loginProperties = extractFireboltProperties(url, connectionSettings); this.fireboltAuthenticationService = fireboltAuthenticationService; - this.fireboltGatewayUrlService = fireboltGatewayUrlService; - this.httpConnectionUrl = getHttpConnectionUrl(loginProperties); + this.httpConnectionUrl = loginProperties.getHttpConnectionUrl(); this.fireboltStatementService = fireboltStatementService; this.statements = new ArrayList<>(); this.connectionTimeout = loginProperties.getConnectionTimeoutMillis(); this.networkTimeout = loginProperties.getSocketTimeoutMillis(); - this.systemEngine = loginProperties.isSystemEngine(); - this.fireboltEngineService = fireboltEngineService; - this.fireboltAccountIdService = fireboltAccountIdService; - this.connect(); } // This code duplication between constructors is done because of back reference: dependent services require reference to current instance of FireboltConnection that prevents using constructor chaining or factory method. @ExcludeFromJacocoGeneratedReport - public FireboltConnection(@NonNull String url, Properties connectionSettings) throws SQLException { + protected FireboltConnection(@NonNull String url, Properties connectionSettings) throws SQLException { this.loginProperties = extractFireboltProperties(url, connectionSettings); OkHttpClient httpClient = getHttpClient(loginProperties); ObjectMapper objectMapper = FireboltObjectMapper.getInstance(); - this.fireboltAuthenticationService = new FireboltAuthenticationService(new FireboltAuthenticationClient(httpClient, objectMapper, this, loginProperties.getUserDrivers(), loginProperties.getUserClients())); - this.fireboltGatewayUrlService = new FireboltGatewayUrlService(createFireboltAccountRetriever(httpClient, objectMapper, "engineUrl", GatewayUrlResponse.class)); - this.httpConnectionUrl = getHttpConnectionUrl(loginProperties); + this.fireboltAuthenticationService = new FireboltAuthenticationService(createFireboltAuthenticationClient(httpClient, objectMapper)); + this.httpConnectionUrl = loginProperties.getHttpConnectionUrl(); this.fireboltStatementService = new FireboltStatementService(new StatementClientImpl(httpClient, objectMapper, this, loginProperties.getUserDrivers(), loginProperties.getUserClients())); this.statements = new ArrayList<>(); this.connectionTimeout = loginProperties.getConnectionTimeoutMillis(); this.networkTimeout = loginProperties.getSocketTimeoutMillis(); - this.systemEngine = loginProperties.isSystemEngine(); - this.fireboltEngineService = new FireboltEngineService(this); - this.fireboltAccountIdService = new FireboltAccountIdService(createFireboltAccountRetriever(httpClient, objectMapper, "resolve", FireboltAccount.class)); + } + + protected abstract FireboltAuthenticationClient createFireboltAuthenticationClient(OkHttpClient httpClient, ObjectMapper objectMapper); + + public static FireboltConnection create(@NonNull String url, Properties connectionSettings) throws SQLException { + return createConnectionInstance(url, connectionSettings); + } - this.connect(); + private static FireboltConnection createConnectionInstance(@NonNull String url, Properties connectionSettings) throws SQLException { + switch(getUrlVersion(url)) { + case 1: return new FireboltConnectionUserPasswordAuthentication(url, connectionSettings); + case 2: return new FireboltConnectionServiceSecretAuthentication(url, connectionSettings); + default: throw new IllegalArgumentException(format("Cannot distinguish version from url %s", url)); + } + } + + private static int getUrlVersion(String url) { + Pattern urlWithHost = Pattern.compile("jdbc:firebolt://api\\.\\w+\\.firebolt\\.io"); + return urlWithHost.matcher(url).find() ? 1 : 2; } - private static OkHttpClient getHttpClient(FireboltProperties fireboltProperties) throws FireboltException { + protected static OkHttpClient getHttpClient(FireboltProperties fireboltProperties) throws FireboltException { try { return HttpClientConfig.getInstance() == null ? HttpClientConfig.init(fireboltProperties) : HttpClientConfig.getInstance(); } catch (GeneralSecurityException | IOException e) { @@ -134,70 +130,32 @@ private static OkHttpClient getHttpClient(FireboltProperties fireboltProperties) } } - private FireboltAccountRetriever createFireboltAccountRetriever(OkHttpClient httpClient, ObjectMapper objectMapper, String path, Class type) { - return new FireboltAccountRetriever<>(httpClient, objectMapper, this, loginProperties.getUserDrivers(), loginProperties.getUserClients(), loginProperties.getHost(), path, type); - } - - private void connect() throws SQLException { - String accessToken = this.getAccessToken(loginProperties).orElse(StringUtils.EMPTY); + protected void connect() throws SQLException { closed = false; if (!PropertyUtil.isLocalDb(loginProperties)) { - String account = loginProperties.getAccount(); - if (account == null) { - throw new FireboltException("Cannot connect: account is missing"); - } - FireboltProperties internalSystemEngineProperties = createInternalSystemEngineProperties(accessToken, account); - String accountId = fireboltAccountIdService.getValue(accessToken, account); - if (systemEngine) { - //When using system engine, the system engine properties are the same as the session properties - sessionProperties = internalSystemEngineProperties.toBuilder().accountId(accountId).build(); - } else { - sessionProperties = internalSystemEngineProperties.toBuilder() - .engine(loginProperties.getEngine()) - .systemEngine(true) - .accountId(accountId) - .build(); - sessionProperties = getSessionPropertiesForNonSystemEngine(); - } + authenticate(); } else { //When running packdb locally, the login properties are the session properties sessionProperties = loginProperties; + assertDatabaseExisting(loginProperties.getDatabase()); } - assertDatabaseExisting(sessionProperties.getDatabase()); log.debug("Connection opened"); } - private FireboltProperties getSessionPropertiesForNonSystemEngine() throws SQLException { - Engine engine = fireboltEngineService.getEngine(loginProperties.getEngine(), loginProperties.getDatabase()); - return loginProperties.toBuilder().host(engine.getEndpoint()).engine(engine.getName()).systemEngine(false).database(engine.getDatabase()).build(); - } - - private void assertDatabaseExisting(String database) throws SQLException { - if (database != null && !fireboltEngineService.doesDatabaseExist(database)) { - throw new FireboltException(format("Database %s does not exist", database)); - } - } + protected abstract void authenticate() throws SQLException; - private FireboltProperties createInternalSystemEngineProperties(String accessToken, String account) throws FireboltException { - String systemEngineEndpoint = fireboltGatewayUrlService.getUrl(accessToken, account); - return this.loginProperties - .toBuilder() - .systemEngine(true) - .additionalProperties(Map.of()) - .compress(false) - .host(UrlUtil.createUrl(systemEngineEndpoint).getHost()).database(null).build(); - } + protected abstract void assertDatabaseExisting(String database) throws SQLException; public void removeExpiredTokens() throws FireboltException { fireboltAuthenticationService.removeConnectionTokens(httpConnectionUrl, loginProperties); } public Optional getAccessToken() throws FireboltException { - return this.getAccessToken(sessionProperties); + return getAccessToken(sessionProperties); } - private Optional getAccessToken(FireboltProperties fireboltProperties) throws FireboltException { + protected Optional getAccessToken(FireboltProperties fireboltProperties) throws FireboltException { String accessToken = fireboltProperties.getAccessToken(); if (accessToken != null) { if (fireboltProperties.getPrincipal() != null || fireboltProperties.getSecret() != null) { @@ -258,10 +216,10 @@ public boolean isClosed() { @Override public DatabaseMetaData getMetaData() throws SQLException { this.validateConnectionIsNotClose(); - if (!this.systemEngine) { - return new FireboltDatabaseMetadata(this.httpConnectionUrl, this); + if (!loginProperties.isSystemEngine()) { + return new FireboltDatabaseMetadata(httpConnectionUrl, this); } else { - return new FireboltSystemEngineDatabaseMetadata(this.httpConnectionUrl, this); + return new FireboltSystemEngineDatabaseMetadata(httpConnectionUrl, this); } } @@ -348,16 +306,11 @@ public void close() { log.debug("Connection closed"); } - private FireboltProperties extractFireboltProperties(String jdbcUri, Properties connectionProperties) { + protected FireboltProperties extractFireboltProperties(String jdbcUri, Properties connectionProperties) { Properties propertiesFromUrl = UrlUtil.extractProperties(jdbcUri); return FireboltProperties.of(propertiesFromUrl, connectionProperties); } - private String getHttpConnectionUrl(FireboltProperties newSessionProperties) { - String hostAndPort = newSessionProperties.getHost() + ":" + newSessionProperties.getPort(); - return newSessionProperties.isSsl() ? "https://" + hostAndPort : "http://" + hostAndPort; - } - @Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { @@ -425,7 +378,7 @@ public boolean isValid(int timeout) throws SQLException { return false; } try { - if (!this.systemEngine) { + if (!loginProperties.isSystemEngine()) { validateConnection(this.getSessionProperties(), true); } return true; diff --git a/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecretAuthentication.java b/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecretAuthentication.java new file mode 100644 index 000000000..e08f641e9 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecretAuthentication.java @@ -0,0 +1,114 @@ +package com.firebolt.jdbc.connection; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.firebolt.jdbc.annotation.ExcludeFromJacocoGeneratedReport; +import com.firebolt.jdbc.client.FireboltObjectMapper; +import com.firebolt.jdbc.client.account.FireboltAccount; +import com.firebolt.jdbc.client.account.FireboltAccountRetriever; +import com.firebolt.jdbc.client.authentication.AuthenticationRequest; +import com.firebolt.jdbc.client.authentication.FireboltAuthenticationClient; +import com.firebolt.jdbc.client.authentication.ServiceAccountAuthenticationRequest; +import com.firebolt.jdbc.client.gateway.GatewayUrlResponse; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.exception.FireboltException; +import com.firebolt.jdbc.service.FireboltAccountIdService; +import com.firebolt.jdbc.service.FireboltAuthenticationService; +import com.firebolt.jdbc.service.FireboltEngineInformationSchemaService; +import com.firebolt.jdbc.service.FireboltEngineService; +import com.firebolt.jdbc.service.FireboltGatewayUrlService; +import com.firebolt.jdbc.service.FireboltStatementService; +import lombok.NonNull; +import okhttp3.OkHttpClient; +import org.apache.commons.lang3.StringUtils; + +import java.sql.SQLException; +import java.util.Map; +import java.util.Properties; + +import static java.lang.String.format; + +public class FireboltConnectionServiceSecretAuthentication extends FireboltConnection { + private final FireboltGatewayUrlService fireboltGatewayUrlService; + private final FireboltAccountIdService fireboltAccountIdService; + private final FireboltEngineService fireboltEngineService; + + FireboltConnectionServiceSecretAuthentication(@NonNull String url, + Properties connectionSettings, + FireboltAuthenticationService fireboltAuthenticationService, + FireboltGatewayUrlService fireboltGatewayUrlService, + FireboltStatementService fireboltStatementService, + FireboltEngineInformationSchemaService fireboltEngineService, + FireboltAccountIdService fireboltAccountIdService) throws SQLException { + super(url, connectionSettings, fireboltAuthenticationService, fireboltStatementService); + this.fireboltGatewayUrlService = fireboltGatewayUrlService; + this.fireboltAccountIdService = fireboltAccountIdService; + this.fireboltEngineService = fireboltEngineService; + connect(); + } + + @ExcludeFromJacocoGeneratedReport + FireboltConnectionServiceSecretAuthentication(@NonNull String url, Properties connectionSettings) throws SQLException { + super(url, connectionSettings); + OkHttpClient httpClient = getHttpClient(loginProperties); + ObjectMapper objectMapper = FireboltObjectMapper.getInstance(); + this.fireboltGatewayUrlService = new FireboltGatewayUrlService(createFireboltAccountRetriever(httpClient, objectMapper, "engineUrl", GatewayUrlResponse.class)); + this.fireboltAccountIdService = new FireboltAccountIdService(createFireboltAccountRetriever(httpClient, objectMapper, "resolve", FireboltAccount.class)); + this.fireboltEngineService = new FireboltEngineInformationSchemaService(this); + connect(); + } + + private FireboltAccountRetriever createFireboltAccountRetriever(OkHttpClient httpClient, ObjectMapper objectMapper, String path, Class type) { + return new FireboltAccountRetriever<>(httpClient, objectMapper, this, loginProperties.getUserDrivers(), loginProperties.getUserClients(), loginProperties.getHost(), path, type); + } + + @Override + protected void authenticate() throws SQLException { + String account = loginProperties.getAccount(); + if (account == null) { + throw new FireboltException("Cannot connect: account is missing"); + } + String accessToken = getAccessToken(loginProperties).orElse(StringUtils.EMPTY); + sessionProperties = getSessionPropertiesForSystemEngine(accessToken, account); + if (loginProperties.isSystemEngine()) { + assertDatabaseExisting(loginProperties.getDatabase()); + } else { + sessionProperties = getSessionPropertiesForNonSystemEngine(); + } + } + + private FireboltProperties getSessionPropertiesForNonSystemEngine() throws SQLException { + sessionProperties = sessionProperties.toBuilder().engine(loginProperties.getEngine()).build(); + Engine engine = fireboltEngineService.getEngine(loginProperties); + return loginProperties.toBuilder().host(engine.getEndpoint()).engine(engine.getName()).systemEngine(false).database(engine.getDatabase()).build(); + } + + @Override + protected void assertDatabaseExisting(String database) throws SQLException { + if (database != null && !fireboltEngineService.doesDatabaseExist(database)) { + throw new FireboltException(format("Database %s does not exist", database)); + } + } + + private FireboltProperties getSessionPropertiesForSystemEngine(String accessToken, String account) throws FireboltException { + String systemEngineEndpoint = fireboltGatewayUrlService.getUrl(accessToken, account); + String accountId = fireboltAccountIdService.getValue(accessToken, account); + return this.loginProperties + .toBuilder() + .systemEngine(true) + .additionalProperties(Map.of()) + .compress(false) + .accountId(accountId) + .host(UrlUtil.createUrl(systemEngineEndpoint).getHost()).database(null).build(); + } + + @Override + protected FireboltAuthenticationClient createFireboltAuthenticationClient(OkHttpClient httpClient, ObjectMapper objectMapper) { + return new FireboltAuthenticationClient(httpClient, objectMapper, this, loginProperties.getUserDrivers(), loginProperties.getUserClients()) { + @Override + public AuthenticationRequest getAuthenticationRequest(String username, String password, String host, String environment) { + return new ServiceAccountAuthenticationRequest(username, password, environment); + } + }; + } + +} diff --git a/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionUserPasswordAuthentication.java b/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionUserPasswordAuthentication.java new file mode 100644 index 000000000..15d44a281 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionUserPasswordAuthentication.java @@ -0,0 +1,80 @@ +package com.firebolt.jdbc.connection; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.firebolt.jdbc.annotation.ExcludeFromJacocoGeneratedReport; +import com.firebolt.jdbc.client.FireboltObjectMapper; +import com.firebolt.jdbc.client.account.FireboltAccountClient; +import com.firebolt.jdbc.client.authentication.AuthenticationRequest; +import com.firebolt.jdbc.client.authentication.FireboltAuthenticationClient; +import com.firebolt.jdbc.client.authentication.OldServiceAccountAuthenticationRequest; +import com.firebolt.jdbc.client.authentication.UsernamePasswordAuthenticationRequest; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.service.FireboltAuthenticationService; +import com.firebolt.jdbc.service.FireboltEngineApiService; +import com.firebolt.jdbc.service.FireboltEngineInformationSchemaService; +import com.firebolt.jdbc.service.FireboltEngineService; +import com.firebolt.jdbc.service.FireboltStatementService; +import lombok.NonNull; +import okhttp3.OkHttpClient; +import org.apache.commons.lang3.StringUtils; + +import java.sql.SQLException; +import java.util.Properties; + +public class FireboltConnectionUserPasswordAuthentication extends FireboltConnection { + private final FireboltEngineService fireboltEngineService; + + FireboltConnectionUserPasswordAuthentication(@NonNull String url, + Properties connectionSettings, + FireboltAuthenticationService fireboltAuthenticationService, + FireboltStatementService fireboltStatementService, + FireboltEngineInformationSchemaService fireboltEngineService) throws SQLException { + super(url, connectionSettings, fireboltAuthenticationService, fireboltStatementService); + this.fireboltEngineService = fireboltEngineService; + connect(); + } + + @ExcludeFromJacocoGeneratedReport + FireboltConnectionUserPasswordAuthentication(@NonNull String url, Properties connectionSettings) throws SQLException { + super(url, connectionSettings); + OkHttpClient httpClient = getHttpClient(loginProperties); + ObjectMapper objectMapper = FireboltObjectMapper.getInstance(); + this.fireboltEngineService = new FireboltEngineApiService(new FireboltAccountClient(httpClient, objectMapper, this, loginProperties.getUserDrivers(), loginProperties.getUserClients())); + connect(); + } + + @Override + protected void authenticate() throws SQLException { + String accessToken = getAccessToken(loginProperties).orElse(StringUtils.EMPTY); + FireboltProperties propertiesWithAccessToken = loginProperties.toBuilder().accessToken(accessToken).build(); + Engine engine = fireboltEngineService.getEngine(propertiesWithAccessToken); + this.sessionProperties = loginProperties.toBuilder().host(engine.getEndpoint()).database(engine.getDatabase()).engine(engine.getName()).build(); + } + + @Override + protected FireboltProperties extractFireboltProperties(String jdbcUri, Properties connectionProperties) { + FireboltProperties properties = super.extractFireboltProperties(jdbcUri, connectionProperties); + boolean systemEngine = "system".equals(properties.getEngine()); + return properties.toBuilder().systemEngine(systemEngine).build(); + } + + @Override + protected void assertDatabaseExisting(String database) throws SQLException { + // empty implementation. There is no way to validate that DB exists. Even if such API exists it is irrelevant + // because it is used for old DB that will be obsolete soon and only when using either system or local engine. + } + + @Override + protected FireboltAuthenticationClient createFireboltAuthenticationClient(OkHttpClient httpClient, ObjectMapper objectMapper) { + return new FireboltAuthenticationClient(httpClient, objectMapper, this, loginProperties.getUserDrivers(), loginProperties.getUserClients()) { + @Override + public AuthenticationRequest getAuthenticationRequest(String username, String password, String host, String environment) { + if (StringUtils.isEmpty(username) || StringUtils.contains(username, "@")) { + return new UsernamePasswordAuthenticationRequest(username, password, host); + } else { + return new OldServiceAccountAuthenticationRequest(username, password, host); + } + } + }; + } +} diff --git a/src/main/java/com/firebolt/jdbc/connection/UrlUtil.java b/src/main/java/com/firebolt/jdbc/connection/UrlUtil.java index bc32fa51e..33eb33b32 100644 --- a/src/main/java/com/firebolt/jdbc/connection/UrlUtil.java +++ b/src/main/java/com/firebolt/jdbc/connection/UrlUtil.java @@ -39,6 +39,8 @@ private static Properties parseUriQueryPart(String jdbcConnectionString) { } } Optional.ofNullable(uri.getPath()).map(p -> !StringUtils.isEmpty(p) ? StringUtils.removeEnd(p, "/") : p).ifPresent(path -> uriProperties.put(FireboltSessionProperty.PATH.getKey(), path)); + Optional.ofNullable(uri.getHost()) + .ifPresent(host -> uriProperties.put(FireboltSessionProperty.HOST.getKey(), host)); Optional.of(uri.getPort()).filter(p -> !p.equals(-1)) .ifPresent(port -> uriProperties.put(FireboltSessionProperty.PORT.getKey(), String.valueOf(port))); return uriProperties; diff --git a/src/main/java/com/firebolt/jdbc/connection/settings/FireboltProperties.java b/src/main/java/com/firebolt/jdbc/connection/settings/FireboltProperties.java index a23c0922d..76027b63d 100644 --- a/src/main/java/com/firebolt/jdbc/connection/settings/FireboltProperties.java +++ b/src/main/java/com/firebolt/jdbc/connection/settings/FireboltProperties.java @@ -176,7 +176,7 @@ private static String getDatabase(Properties properties, String path) throws Ill return null; } else { Matcher m = DB_PATH_PATTERN.matcher(path); - if (m.matches()) { + if (m.find()) { return m.group(1); } else { throw new IllegalArgumentException(format("The database provided is invalid %s", path)); @@ -248,4 +248,11 @@ public void addProperty(@NonNull String key, String value) { public void addProperty(Pair property) { this.addProperty(property.getLeft(), property.getRight()); } + + public String getHttpConnectionUrl() { + String hostAndPort = host + (port == null ? "" : ":" + port); + String protocol = isSsl() ? "https://" : "http://"; + return protocol + hostAndPort; + } + } diff --git a/src/main/java/com/firebolt/jdbc/service/FireboltAuthenticationService.java b/src/main/java/com/firebolt/jdbc/service/FireboltAuthenticationService.java index 07ed76c8f..7ff3decfa 100644 --- a/src/main/java/com/firebolt/jdbc/service/FireboltAuthenticationService.java +++ b/src/main/java/com/firebolt/jdbc/service/FireboltAuthenticationService.java @@ -12,9 +12,9 @@ import javax.xml.bind.DatatypeConverter; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Optional; import static java.lang.String.format; +import static java.util.Optional.ofNullable; import static java.util.concurrent.TimeUnit.SECONDS; import static net.jodah.expiringmap.ExpirationPolicy.CREATED; @@ -26,6 +26,8 @@ public class FireboltAuthenticationService { .variableExpiration().build(); private static final long TOKEN_EXPIRATION_OFFSET = 5L; private static final long TOKEN_TTL_THRESHOLD = 60L; + private static final String ERROR_MESSAGE = "Failed to connect to Firebolt with the error: %s, see logs for more info."; + private static final String ERROR_MESSAGE_FROM_SERVER = "Failed to connect to Firebolt with the error from the server: %s, see logs for more info."; private final FireboltAuthenticationClient fireboltAuthenticationClient; public FireboltConnectionTokens getConnectionTokens(String host, FireboltProperties loginProperties) throws FireboltException { @@ -43,16 +45,13 @@ public FireboltConnectionTokens getConnectionTokens(String host, FireboltPropert tokensMap.put(connectionParams, fireboltConnectionTokens, CREATED, durationInSeconds, SECONDS); return fireboltConnectionTokens; } + } catch (FireboltException e) { + log.error("Failed to connect to Firebolt", e); + String msg = ofNullable(e.getErrorMessageFromServer()).map(m -> format(ERROR_MESSAGE_FROM_SERVER, m)).orElse(format(ERROR_MESSAGE, e.getMessage())); + throw new FireboltException(msg, e); } catch (Exception e) { log.error("Failed to connect to Firebolt", e); - String errorMessageTemplate = "Failed to connect to Firebolt with the error%s: %s, see logs for more info."; - if (e instanceof FireboltException) { - String server = ((FireboltException) e).getErrorMessageFromServer(); - if (server != null) { - throw new FireboltException(format(errorMessageTemplate, " from the server", server), e); - } - } - throw new FireboltException(format(errorMessageTemplate, "", e.getMessage()), e); + throw new FireboltException(format(ERROR_MESSAGE, e.getMessage()), e); } } @@ -89,8 +88,8 @@ private static class ConnectParams { public ConnectParams(String fireboltHost, String principal, String secret) throws NoSuchAlgorithmException { this.fireboltHost = fireboltHost; MessageDigest sha256Instance = MessageDigest.getInstance("SHA-256"); - Optional.ofNullable(principal).map(String::getBytes).ifPresent(sha256Instance::update); - Optional.ofNullable(secret).map(String::getBytes).ifPresent(sha256Instance::update); + ofNullable(principal).map(String::getBytes).ifPresent(sha256Instance::update); + ofNullable(secret).map(String::getBytes).ifPresent(sha256Instance::update); this.credentialsHash = DatatypeConverter.printHexBinary(sha256Instance.digest()); } } diff --git a/src/main/java/com/firebolt/jdbc/service/FireboltEngineApiService.java b/src/main/java/com/firebolt/jdbc/service/FireboltEngineApiService.java new file mode 100644 index 000000000..e5dc80205 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/service/FireboltEngineApiService.java @@ -0,0 +1,109 @@ +package com.firebolt.jdbc.service; + +import com.firebolt.jdbc.client.account.FireboltAccountClient; +import com.firebolt.jdbc.client.account.response.FireboltAccountResponse; +import com.firebolt.jdbc.client.account.response.FireboltDefaultDatabaseEngineResponse; +import com.firebolt.jdbc.client.account.response.FireboltEngineIdResponse; +import com.firebolt.jdbc.client.account.response.FireboltEngineResponse; +import com.firebolt.jdbc.connection.Engine; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.exception.FireboltException; +import lombok.CustomLog; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.Optional; +import java.util.Set; + +@RequiredArgsConstructor +@CustomLog +public class FireboltEngineApiService implements FireboltEngineService { + private static final Set ENGINE_NOT_READY_STATUSES = Set.of( + "ENGINE_STATUS_PROVISIONING_STARTED", "ENGINE_STATUS_PROVISIONING_PENDING", + "ENGINE_STATUS_PROVISIONING_FINISHED", "ENGINE_STATUS_RUNNING_REVISION_STARTING"); + private static final String ERROR_NO_ENGINE_ATTACHED = "There is no Firebolt engine running on %s attached to the database %s. To connect first make sure there is a running engine and then try again."; + private static final String ERROR_NO_ENGINE_WITH_NAME = "There is no Firebolt engine running on %s with the name %s. To connect first make sure there is a running engine and then try again."; + + + private final FireboltAccountClient fireboltAccountClient; + + @Override + public Engine getEngine(FireboltProperties properties) throws SQLException { + return getEngine(properties.getHttpConnectionUrl(), properties, properties.getAccessToken()); + } + + /** + * Returns the engine + * + * @param connectionUrl the connection url + * @param loginProperties properties to login + * @param accessToken the access token + * @return the engine + */ + private Engine getEngine(String connectionUrl, FireboltProperties loginProperties, String accessToken) + throws FireboltException { + String accountId = null; + Engine engine; + try { + if (StringUtils.isNotEmpty(loginProperties.getAccount())) { + accountId = getAccountId(connectionUrl, loginProperties.getAccount(), accessToken).orElse(null); + } + if (StringUtils.isEmpty(loginProperties.getEngine())) { + engine = getDefaultEngine(connectionUrl, accountId, loginProperties.getDatabase(), accessToken); + } else { + engine = getEngineWithName(connectionUrl, accountId, loginProperties.getEngine(), accessToken); + } + } catch (FireboltException e) { + throw e; + } catch (Exception e) { + throw new FireboltException("Failed to get engine", e); + } + validateEngineIsNotStarting(engine); + return engine; + } + + private Engine getEngineWithName(String connectionUrl, String accountId, String engineName, String accessToken) + throws FireboltException, IOException { + FireboltEngineIdResponse response = fireboltAccountClient.getEngineId(connectionUrl, accountId, engineName, + accessToken); + String engineID = Optional.ofNullable(response).map(FireboltEngineIdResponse::getEngine) + .map(FireboltEngineIdResponse.Engine::getEngineId).orElseThrow(() -> new FireboltException( + "Failed to extract engine id field from the server response: the response from the server is invalid.")); + FireboltEngineResponse fireboltEngineResponse = fireboltAccountClient.getEngine(connectionUrl, accountId, + engineName, engineID, accessToken); + + return Optional.ofNullable(fireboltEngineResponse).map(FireboltEngineResponse::getEngine) + .filter(e -> StringUtils.isNotEmpty(e.getEndpoint())) + .map(e -> new Engine(e.getEndpoint(), e.getCurrentStatus(), engineName, null, engineID)) + .orElseThrow(() -> new FireboltException( + String.format(ERROR_NO_ENGINE_WITH_NAME, connectionUrl, engineName))); + } + + private Engine getDefaultEngine(String connectionUrl, String accountId, String database, String accessToken) + throws FireboltException, IOException { + FireboltDefaultDatabaseEngineResponse defaultEngine = fireboltAccountClient + .getDefaultEngineByDatabaseName(connectionUrl, accountId, database, accessToken); + + return Optional.ofNullable(defaultEngine).map(FireboltDefaultDatabaseEngineResponse::getEngineUrl) + .map(url -> new Engine(url, "running", null, database, null)).orElseThrow( + () -> new FireboltException(String.format(ERROR_NO_ENGINE_ATTACHED, connectionUrl, database))); + } + + private Optional getAccountId(String connectionUrl, String account, String accessToken) + throws FireboltException, IOException { + FireboltAccountResponse fireboltAccountResponse = fireboltAccountClient.getAccount(connectionUrl, account, + accessToken); + return Optional.ofNullable(fireboltAccountResponse).map(FireboltAccountResponse::getAccountId); + } + + private void validateEngineIsNotStarting(Engine engine) throws FireboltException { + if (StringUtils.isNotEmpty(engine.getId()) && StringUtils.isNotEmpty(engine.getStatus()) + && ENGINE_NOT_READY_STATUSES.contains(engine.getStatus())) { + throw new FireboltException(String.format( + "The engine %s is currently starting. Please wait until the engine is on and then execute the query again.", engine.getName())); + } + } + +} diff --git a/src/main/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaService.java b/src/main/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaService.java new file mode 100644 index 000000000..37569132e --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaService.java @@ -0,0 +1,77 @@ +package com.firebolt.jdbc.service; + +import com.firebolt.jdbc.connection.Engine; +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.exception.FireboltException; +import lombok.CustomLog; +import lombok.RequiredArgsConstructor; + +import javax.annotation.Nullable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import static java.lang.String.format; + +@RequiredArgsConstructor +@CustomLog +public class FireboltEngineInformationSchemaService implements FireboltEngineService { + private static final String ENGINE_URL = "url"; + private static final String STATUS_FIELD = "status"; + private static final String ENGINE_NAME_FIELD = "engine_name"; + private static final String RUNNING_STATUS = "running"; + private static final String ENGINE_QUERY = + "SELECT engs.url, engs.attached_to, dbs.database_name, engs.status, engs.engine_name " + + "FROM information_schema.engines as engs " + + "LEFT JOIN information_schema.databases as dbs ON engs.attached_to = dbs.database_name " + + "WHERE engs.engine_name = ?"; + private static final String DATABASE_QUERY = "SELECT database_name FROM information_schema.databases WHERE database_name=?"; + + private final FireboltConnection fireboltConnection; + + @Override + public boolean doesDatabaseExist(String database) throws SQLException { + try (PreparedStatement ps = fireboltConnection.prepareStatement(DATABASE_QUERY)) { + ps.setString(1, database); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } + } + + @Override + public Engine getEngine(FireboltProperties properties) throws SQLException { + return getEngine(properties.getEngine(), properties.getDatabase()); + } + + private Engine getEngine(String engine, @Nullable String database) throws SQLException { + if (engine == null) { + throw new IllegalArgumentException("Cannot retrieve engine parameters because its name is null"); + } + try (PreparedStatement ps = fireboltConnection.prepareStatement(ENGINE_QUERY)) { + ps.setString(1, engine); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) { + throw new FireboltException(format("The engine with the name %s could not be found", engine)); + } + String status = rs.getString(STATUS_FIELD); + if (!isEngineRunning(status)) { + throw new FireboltException(format("The engine with the name %s is not running. Status: %s", engine, status)); + } + String attachedDatabase = rs.getString("attached_to"); + if (attachedDatabase == null) { + throw new FireboltException(format("The engine with the name %s is not attached to any database", engine)); + } + if (database != null && !database.equals(attachedDatabase)) { + throw new FireboltException(format("The engine with the name %s is not attached to database %s", engine, database)); + } + return new Engine(rs.getString(ENGINE_URL), status, rs.getString(ENGINE_NAME_FIELD), attachedDatabase, null); + } + } + } + + private boolean isEngineRunning(String status) { + return RUNNING_STATUS.equalsIgnoreCase(status); + } +} diff --git a/src/main/java/com/firebolt/jdbc/service/FireboltEngineService.java b/src/main/java/com/firebolt/jdbc/service/FireboltEngineService.java index 7b9e35837..b50c74b5c 100644 --- a/src/main/java/com/firebolt/jdbc/service/FireboltEngineService.java +++ b/src/main/java/com/firebolt/jdbc/service/FireboltEngineService.java @@ -1,83 +1,15 @@ package com.firebolt.jdbc.service; import com.firebolt.jdbc.connection.Engine; -import com.firebolt.jdbc.connection.FireboltConnection; -import com.firebolt.jdbc.exception.FireboltException; -import lombok.CustomLog; -import lombok.RequiredArgsConstructor; +import com.firebolt.jdbc.connection.settings.FireboltProperties; -import javax.annotation.Nullable; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Optional; -import static java.lang.String.format; +public interface FireboltEngineService { + Engine getEngine(FireboltProperties properties) throws SQLException; -@RequiredArgsConstructor -@CustomLog -public class FireboltEngineService { - private static final String ENGINE_URL = "url"; - private static final String STATUS_FIELD = "status"; - private static final String ENGINE_NAME_FIELD = "engine_name"; - private static final String RUNNING_STATUS = "running"; - private static final String ENGINE_QUERY = - "SELECT engs.url, engs.attached_to, dbs.database_name, engs.status, engs.engine_name " + - "FROM information_schema.engines as engs " + - "LEFT JOIN information_schema.databases as dbs ON engs.attached_to = dbs.database_name " + - "WHERE engs.engine_name = ?"; - private static final String DATABASE_QUERY = "SELECT database_name FROM information_schema.databases WHERE database_name=?"; - - private final FireboltConnection fireboltConnection; - - /** - * Extracts the engine name from host - * - * @param engineHost engine host - * @return the engine name - */ - public String getEngineNameByHost(String engineHost) throws FireboltException { - return Optional.ofNullable(engineHost).filter(host -> host.contains(".")).map(host -> host.split("\\.")[0]) - .map(host -> host.replace("-", "_")).orElseThrow(() -> new FireboltException( - format("Could not establish the engine from the host: %s", engineHost))); - } - - public boolean doesDatabaseExist(String database) throws SQLException { - try (PreparedStatement ps = fireboltConnection.prepareStatement(DATABASE_QUERY)) { - ps.setString(1, database); - try (ResultSet rs = ps.executeQuery()) { - return rs.next(); - } - } + default boolean doesDatabaseExist(String database) throws SQLException { + return true; } - public Engine getEngine(String engine, @Nullable String database) throws SQLException { - if (engine == null) { - throw new IllegalArgumentException("Cannot retrieve engine parameters because its name is null"); - } - try (PreparedStatement ps = fireboltConnection.prepareStatement(ENGINE_QUERY)) { - ps.setString(1, engine); - try (ResultSet rs = ps.executeQuery()) { - if (!rs.next()) { - throw new FireboltException(format("The engine with the name %s could not be found", engine)); - } - String status = rs.getString(STATUS_FIELD); - if (!isEngineRunning(status)) { - throw new FireboltException(format("The engine with the name %s is not running. Status: %s", engine, status)); - } - String attachedDatabase = rs.getString("attached_to"); - if (attachedDatabase == null) { - throw new FireboltException(format("The engine with the name %s is not attached to any database", engine)); - } - if (database != null && !database.equals(attachedDatabase)) { - throw new FireboltException(format("The engine with the name %s is not attached to database %s", engine, database)); - } - return new Engine(rs.getString(ENGINE_URL), status, rs.getString(ENGINE_NAME_FIELD), attachedDatabase); - } - } - } - - private boolean isEngineRunning(String status) { - return RUNNING_STATUS.equalsIgnoreCase(status); - } } diff --git a/src/main/java/com/firebolt/jdbc/util/PropertyUtil.java b/src/main/java/com/firebolt/jdbc/util/PropertyUtil.java index 3be9bc77b..e5e0670c2 100644 --- a/src/main/java/com/firebolt/jdbc/util/PropertyUtil.java +++ b/src/main/java/com/firebolt/jdbc/util/PropertyUtil.java @@ -1,20 +1,22 @@ package com.firebolt.jdbc.util; +import com.firebolt.jdbc.connection.UrlUtil; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.connection.settings.FireboltSessionProperty; +import lombok.CustomLog; +import lombok.experimental.UtilityClass; +import org.apache.commons.lang3.StringUtils; + import java.sql.DriverPropertyInfo; +import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map.Entry; import java.util.Optional; import java.util.Properties; import java.util.stream.Collectors; - -import com.firebolt.jdbc.connection.UrlUtil; -import org.apache.commons.lang3.StringUtils; - -import com.firebolt.jdbc.connection.settings.FireboltProperties; -import com.firebolt.jdbc.connection.settings.FireboltSessionProperty; - -import lombok.CustomLog; -import lombok.experimental.UtilityClass; +import java.util.stream.Stream; @CustomLog @UtilityClass @@ -57,8 +59,8 @@ public boolean isLocalDb(FireboltProperties fireboltProperties) { private List mapProperties(List fireboltSessionProperties, Properties properties) { return fireboltSessionProperties.stream().map(fireboltProperty -> { - DriverPropertyInfo driverPropertyInfo = new DriverPropertyInfo(fireboltProperty.getKey(), - getValueForFireboltSessionProperty(properties, fireboltProperty)); + Entry property = getValueForFireboltSessionProperty(properties, fireboltProperty); + DriverPropertyInfo driverPropertyInfo = new DriverPropertyInfo(property.getKey(), property.getValue()); driverPropertyInfo.required = false; driverPropertyInfo.description = fireboltProperty.getDescription(); driverPropertyInfo.choices = fireboltProperty.getPossibleValues(); @@ -66,11 +68,12 @@ private List mapProperties(List fir }).collect(Collectors.toList()); } - private String getValueForFireboltSessionProperty(Properties properties, - FireboltSessionProperty fireboltSessionProperty) { - Optional value = Optional.ofNullable(properties.getProperty(fireboltSessionProperty.getKey())); - - return value.orElseGet(() -> Optional.ofNullable(fireboltSessionProperty.getDefaultValue()) - .map(Object::toString).orElse(null)); + private Entry getValueForFireboltSessionProperty(Properties properties, FireboltSessionProperty fireboltSessionProperty) { + String strDefaultValue = Optional.ofNullable(fireboltSessionProperty.getDefaultValue()).map(Object::toString).orElse(null); + return Stream.concat(Stream.of(fireboltSessionProperty.getKey()), Arrays.stream(fireboltSessionProperty.getAliases())) + .filter(key -> properties.getProperty(key) != null) + .map(key -> new SimpleEntry<>(key, properties.getProperty(key))) + .findFirst() + .orElseGet(() -> new SimpleEntry<>(fireboltSessionProperty.getKey(), strDefaultValue)); } } diff --git a/src/test/java/com/firebolt/FireboltDriverTest.java b/src/test/java/com/firebolt/FireboltDriverTest.java index e58924ed1..27e2ba80b 100644 --- a/src/test/java/com/firebolt/FireboltDriverTest.java +++ b/src/test/java/com/firebolt/FireboltDriverTest.java @@ -1,12 +1,14 @@ package com.firebolt; -import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.connection.FireboltConnectionServiceSecretAuthentication; +import com.firebolt.jdbc.connection.FireboltConnectionUserPasswordAuthentication; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.MockedConstruction; +import java.sql.Connection; import java.sql.DriverPropertyInfo; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; @@ -33,10 +35,19 @@ void shouldNotReturnNewConnectionWhenUrlIsInvalid(String url) throws SQLExceptio } @Test - void shouldReturnNewConnectionWhenUrlIsValid() throws SQLException { - try (MockedConstruction mocked = mockConstruction(FireboltConnection.class)) { + void shouldReturnNewConnectionWhenUrlIsValid1() throws SQLException { + shouldReturnNewConnectionWhenUrlIsValid(FireboltConnectionUserPasswordAuthentication.class, "jdbc:firebolt://api.dev.firebolt.io/db_name"); + } + + @Test + void shouldReturnNewConnectionWhenUrlIsValid2() throws SQLException { + shouldReturnNewConnectionWhenUrlIsValid(FireboltConnectionServiceSecretAuthentication.class, "jdbc:firebolt:db_name"); + } + + private void shouldReturnNewConnectionWhenUrlIsValid(Class connectionType, String jdbcUrl) throws SQLException { + try (MockedConstruction mocked = mockConstruction(connectionType)) { FireboltDriver fireboltDriver = new FireboltDriver(); - assertNotNull(fireboltDriver.connect("jdbc:firebolt://api.dev.firebolt.io/db_name", new Properties())); + assertNotNull(fireboltDriver.connect(jdbcUrl, new Properties())); assertEquals(1, mocked.constructed().size()); } } @@ -73,10 +84,13 @@ void version() { @CsvSource(value = { "jdbc:firebolt,,", - "jdbc:firebolt:db_name,,environment=app;path=db_name", - "jdbc:firebolt:db_name?account=test,,environment=app;path=db_name;account=test", - "jdbc:firebolt:db_name?account=test,client_id=usr;client_secret=pwd,environment=app;path=db_name;account=test;client_id=usr;client_secret=pwd", - "jdbc:firebolt:db_name,client_id=usr;client_secret=pwd,environment=app;path=db_name;client_id=usr;client_secret=pwd" + "jdbc:firebolt://api.dev.firebolt.io/db_name,,host=api.dev.firebolt.io;path=/db_name", + "jdbc:firebolt://api.dev.firebolt.io/db_name?account=test,,host=api.dev.firebolt.io;path=/db_name;account=test", + "jdbc:firebolt://api.dev.firebolt.io/db_name?account=test,user=usr;password=pwd,host=api.dev.firebolt.io;path=/db_name;account=test;user=usr;password=pwd", // legit:ignore-secrets + "jdbc:firebolt://api.dev.firebolt.io/db_name,user=usr;password=pwd,host=api.dev.firebolt.io;path=/db_name;user=usr;password=pwd", // legit:ignore-secrets + // TODO: add more tests with "new" URL format +// "jdbc:firebolt:db_name,,host=api.dev.firebolt.io;database=db_name", + }, delimiter = ',') void getPropertyInfo(String url, String propStr, String expectedInfoStr) throws SQLException { diff --git a/src/test/java/com/firebolt/jdbc/client/account/FireboltAccountClientTest.java b/src/test/java/com/firebolt/jdbc/client/account/FireboltAccountClientTest.java new file mode 100644 index 000000000..0d4887987 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/client/account/FireboltAccountClientTest.java @@ -0,0 +1,124 @@ +package com.firebolt.jdbc.client.account; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.firebolt.jdbc.client.account.response.FireboltAccountResponse; +import com.firebolt.jdbc.client.account.response.FireboltDefaultDatabaseEngineResponse; +import com.firebolt.jdbc.client.account.response.FireboltEngineIdResponse; +import com.firebolt.jdbc.client.account.response.FireboltEngineResponse; +import com.firebolt.jdbc.client.gateway.GatewayUrlResponse; +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.exception.ExceptionType; +import com.firebolt.jdbc.exception.FireboltException; +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import static com.firebolt.jdbc.exception.ExceptionType.RESOURCE_NOT_FOUND; +import static com.firebolt.jdbc.exception.ExceptionType.TOO_MANY_REQUESTS; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static java.net.HttpURLConnection.HTTP_OK; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FireboltAccountClientTest { + private final ObjectMapper objectMapper = new ObjectMapper(); + @Mock + private OkHttpClient httpClient; + @Mock + private FireboltConnection fireboltConnection; + private FireboltAccountClient client; + + @BeforeEach + void setUp() { + client = new FireboltAccountClient(httpClient, objectMapper, fireboltConnection, null, null); + } + + @Test + void getAccount() throws FireboltException, IOException { + String accountId = "123"; + injectMockedResponse(httpClient, HTTP_OK, new FireboltAccountResponse(accountId)); + assertEquals(accountId, client.getAccount("http://host", "account", "token").getAccountId()); + } + + @Test + void getEngine() throws IOException, FireboltException { + String endpoint = "http://engine/12345"; + FireboltEngineResponse response = FireboltEngineResponse.builder().engine(FireboltEngineResponse.Engine.builder().currentStatus("running").endpoint(endpoint).build()).build(); + injectMockedResponse(httpClient, HTTP_OK, response); + assertEquals(endpoint, client.getEngine("http://host", "account-id", "engine", "engine-id", "token").getEngine().getEndpoint()); + } + + @Test + void getDefaultEngineByDatabaseName() throws FireboltException, IOException { + String endpoint = "http://engine/12345"; + FireboltDefaultDatabaseEngineResponse response = FireboltDefaultDatabaseEngineResponse.builder().engineUrl(endpoint).build(); + injectMockedResponse(httpClient, HTTP_OK, response); + assertEquals(endpoint, client.getDefaultEngineByDatabaseName("http://host", "account-id", "db", "token").getEngineUrl()); + } + + @Test + void getEngineId() throws FireboltException, IOException { + String engineId = "456"; + FireboltEngineIdResponse response = FireboltEngineIdResponse.builder().engine(FireboltEngineIdResponse.Engine.builder().engineId(engineId).build()).build(); + injectMockedResponse(httpClient, HTTP_OK, response); + assertEquals(engineId, client.getEngineId("http://host", "account-id", "db", "token").getEngine().getEngineId()); + } + + @Test + void notFoundError() throws IOException { + injectMockedResponse(httpClient, HTTP_NOT_FOUND, null); + assertNotFoundException(assertThrows(FireboltException.class, () -> client.getEngine("http://host", "account-id", "engine", "engine-id", "token"))); + assertNotFoundException(assertThrows(FireboltException.class, () -> client.getDefaultEngineByDatabaseName("http://host", "account-id", "db", "token"))); + assertNotFoundException(assertThrows(FireboltException.class, () -> client.getEngineId("http://host", "account-id", "db", "token"))); + } + + @ParameterizedTest + @CsvSource(value = { + "404, RESOURCE_NOT_FOUND", + "400, INVALID_REQUEST", + "401, UNAUTHORIZED", + "429, TOO_MANY_REQUESTS", + "406, ERROR" + }) + void httpFailures(int httpStatus, ExceptionType type) throws IOException { + injectMockedResponse(httpClient, httpStatus, null); + assertEquals(type, assertThrows(FireboltException.class, () -> client.getEngine("http://host", "account-id", "engine", "engine-id", "token")).getType()); + assertEquals(type, assertThrows(FireboltException.class, () -> client.getDefaultEngineByDatabaseName("http://host", "account-id", "db", "token")).getType()); + assertEquals(type, assertThrows(FireboltException.class, () -> client.getEngineId("http://host", "account-id", "db", "token")).getType()); + } + + private void assertNotFoundException(FireboltException e) { + assertEquals(RESOURCE_NOT_FOUND, e.getType()); + assertTrue(e.getMessage().contains("could not be found")); + } + + private void injectMockedResponse(OkHttpClient httpClient, int code, Object payload) throws IOException { + Response response = mock(Response.class); + Call call = mock(Call.class); + when(httpClient.newCall(any())).thenReturn(call); + when(call.execute()).thenReturn(response); + ResponseBody body = mock(ResponseBody.class); + when(response.body()).thenReturn(body); + when(response.code()).thenReturn(code); + String gatewayResponse = new ObjectMapper().writeValueAsString(payload); + lenient().when(body.string()).thenReturn(gatewayResponse); + } +} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClientTest.java b/src/test/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClientTest.java index bc8f349f8..b308294ea 100644 --- a/src/test/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClientTest.java +++ b/src/test/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClientTest.java @@ -12,6 +12,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; @@ -47,8 +48,14 @@ class FireboltAuthenticationClientTest { @BeforeEach void setUp() { - fireboltAuthenticationClient = new FireboltAuthenticationClient(httpClient, objectMapper, connection, - "ConnA:1.0.9", "ConnB:2.0.9"); + fireboltAuthenticationClient = new FireboltAuthenticationClient(httpClient, objectMapper, connection, "ConnA:1.0.9", "ConnB:2.0.9") { + @Override + protected AuthenticationRequest getAuthenticationRequest(String username, String password, String host, String environment) { + AuthenticationRequest request = Mockito.mock(AuthenticationRequest.class); + when(request.getUri()).thenReturn("http://host/auth"); + return request; + } + }; } @Test diff --git a/src/test/java/com/firebolt/jdbc/client/query/StatementClientImplTest.java b/src/test/java/com/firebolt/jdbc/client/query/StatementClientImplTest.java index f951cd9ba..903538225 100644 --- a/src/test/java/com/firebolt/jdbc/client/query/StatementClientImplTest.java +++ b/src/test/java/com/firebolt/jdbc/client/query/StatementClientImplTest.java @@ -8,10 +8,16 @@ import com.firebolt.jdbc.statement.StatementInfoWrapper; import com.firebolt.jdbc.statement.StatementUtil; import lombok.NonNull; -import okhttp3.*; +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; import okio.Buffer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -29,7 +35,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class StatementClientImplTest { @@ -134,6 +144,25 @@ void shouldNotRetryNoMoreThanOnceOnUnauthorized() throws IOException, FireboltEx verify(connection, times(2)).removeExpiredTokens(); } + @ParameterizedTest + @CsvSource({ + "java.io.IOException, ERROR", + "okhttp3.internal.http2.StreamResetException, CANCELED", + "java.lang.IllegalArgumentException, ERROR", + }) + void shouldThrowIOException(Class exceptionClass, ExceptionType exceptionType) throws IOException { + FireboltProperties fireboltProperties = FireboltProperties.builder().database("db1").compress(true) + .host("firebolt1").port(555).build(); + Call call = mock(Call.class); + when(call.execute()).thenThrow(exceptionClass); + when(okHttpClient.newCall(any())).thenReturn(call); + StatementClient statementClient = new StatementClientImpl(okHttpClient, mock(ObjectMapper.class), connection, "", ""); + StatementInfoWrapper statementInfoWrapper = StatementUtil.parseToStatementInfoWrappers("select 1").get(0); + FireboltException ex = assertThrows(FireboltException.class, () -> statementClient.executeSqlStatement(statementInfoWrapper, fireboltProperties, false, 5, true)); + assertEquals(exceptionType, ex.getType()); + assertEquals(exceptionClass, ex.getCause().getClass()); + } + private Call getMockedCallWithResponse(int statusCode) throws IOException { Call call = mock(Call.class); Response response = mock(Response.class); diff --git a/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecretAuthenticationTest.java b/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecretAuthenticationTest.java new file mode 100644 index 000000000..fbba00dee --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecretAuthenticationTest.java @@ -0,0 +1,81 @@ +package com.firebolt.jdbc.connection; + +import com.firebolt.jdbc.exception.FireboltException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Properties; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +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.ArgumentMatchers.argThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +class FireboltConnectionServiceSecretAuthenticationTest extends FireboltConnectionTest { + private static final String SYSTEM_ENGINE_URL = "jdbc:firebolt:db?env=dev&account=dev"; + + @Test + void shouldNotValidateConnectionWhenCallingIsValidWhenUsingSystemEngine() throws SQLException { + Properties propertiesWithSystemEngine = new Properties(connectionProperties); + try (FireboltConnection fireboltConnection = createConnection(SYSTEM_ENGINE_URL, propertiesWithSystemEngine)) { + fireboltConnection.isValid(500); + verifyNoInteractions(fireboltStatementService); + } + } + + @Test + void shouldNotGetEngineUrlOrDefaultEngineUrlWhenUsingSystemEngine() throws SQLException { + connectionProperties.put("database", "my_db"); + when(fireboltGatewayUrlService.getUrl(any(), any())).thenReturn("http://my_endpoint"); + + try (FireboltConnection connection = createConnection(SYSTEM_ENGINE_URL, connectionProperties)) { + verify(fireboltEngineService, times(0)).getEngine(argThat(props -> "my_db".equals(props.getDatabase()))); + assertEquals("my_endpoint", connection.getSessionProperties().getHost()); + } + } + + @Test + void noEngineAndDb() throws SQLException { + when(fireboltGatewayUrlService.getUrl(any(), any())).thenReturn("http://my_endpoint"); + + try (FireboltConnection connection = createConnection("jdbc:firebolt:?env=dev&account=dev", connectionProperties)) { + assertEquals("my_endpoint", connection.getSessionProperties().getHost()); + assertNull(connection.getSessionProperties().getEngine()); + assertTrue(connection.getSessionProperties().isSystemEngine()); + } + } + + @Test + void notExistingDb() throws SQLException { + connectionProperties.put("database", "my_db"); + when(fireboltGatewayUrlService.getUrl(any(), any())).thenReturn("http://my_endpoint"); + when(fireboltEngineService.doesDatabaseExist("my_db")).thenReturn(false); + assertEquals("Database my_db does not exist", assertThrows(FireboltException.class, () -> createConnection("jdbc:firebolt:?env=dev&account=dev", connectionProperties)).getMessage()); + } + + @ParameterizedTest(name = "{0}") + @CsvSource({ + "regular engine,&engine=eng,false", + "system engine,'',true" // system engine is readonly + }) + void getMetadata(String testName, String engineParameter, boolean readOnly) throws SQLException { + try (FireboltConnection connection = createConnection(format("jdbc:firebolt:db?env=dev&account=dev%s", engineParameter), connectionProperties)) { + DatabaseMetaData dbmd = connection.getMetaData(); + assertEquals(readOnly, dbmd.isReadOnly()); + } + } + + protected FireboltConnection createConnection(String url, Properties props) throws SQLException { + return new FireboltConnectionServiceSecretAuthentication(url, props, fireboltAuthenticationService, fireboltGatewayUrlService, fireboltStatementService, fireboltEngineService, fireboltAccountIdService); + } +} diff --git a/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionTest.java b/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionTest.java index b19fc328e..3091fbcb1 100644 --- a/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionTest.java +++ b/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionTest.java @@ -4,11 +4,9 @@ import com.firebolt.jdbc.connection.settings.FireboltProperties; import com.firebolt.jdbc.exception.ExceptionType; import com.firebolt.jdbc.exception.FireboltException; -import com.firebolt.jdbc.service.FireboltAuthenticationService; -import com.firebolt.jdbc.service.FireboltEngineService; import com.firebolt.jdbc.service.FireboltAccountIdService; import com.firebolt.jdbc.service.FireboltAuthenticationService; -import com.firebolt.jdbc.service.FireboltEngineService; +import com.firebolt.jdbc.service.FireboltEngineInformationSchemaService; import com.firebolt.jdbc.service.FireboltGatewayUrlService; import com.firebolt.jdbc.service.FireboltStatementService; import com.firebolt.jdbc.statement.StatementInfoWrapper; @@ -49,16 +47,9 @@ import java.util.stream.Stream; import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.ACCESS_TOKEN; -import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.HOST; -import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.CLIENT_SECRET; import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.CLIENT_ID; -import java.sql.Statement; -import java.util.Optional; -import java.util.Properties; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - +import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.CLIENT_SECRET; +import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.HOST; import static java.sql.ResultSet.CLOSE_CURSORS_AT_COMMIT; import static java.sql.ResultSet.CONCUR_READ_ONLY; import static java.sql.ResultSet.CONCUR_UPDATABLE; @@ -73,25 +64,22 @@ 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.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.isNull; -import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class FireboltConnectionTest { +abstract class FireboltConnectionTest { private static final String URL = "jdbc:firebolt:db?env=dev&engine=eng&account=dev"; - private static final String SYSTEM_ENGINE_URL = "jdbc:firebolt:db?env=dev&account=dev"; - private static final String LOCAL_URL = "jdbc:firebolt:local_dev_db?account=dev&ssl=false&max_query_size=10000000&use_standard_sql=1&mask_internal_errors=0&firebolt_enable_beta_functions=1&firebolt_case_insensitive_identifiers=1&rest_api_pull_timeout_sec=3600&rest_api_pull_interval_millisec=5000&rest_api_retry_times=10&host=localhost"; private final FireboltConnectionTokens fireboltConnectionTokens = FireboltConnectionTokens.builder().build(); @Captor @@ -99,17 +87,17 @@ class FireboltConnectionTest { @Captor private ArgumentCaptor queryInfoWrapperArgumentCaptor; @Mock - private FireboltAuthenticationService fireboltAuthenticationService; + protected FireboltAuthenticationService fireboltAuthenticationService; @Mock - private FireboltGatewayUrlService fireboltGatewayUrlService; + protected FireboltGatewayUrlService fireboltGatewayUrlService; @Mock - private FireboltEngineService fireboltEngineService; + protected FireboltEngineInformationSchemaService fireboltEngineService; @Mock - private FireboltStatementService fireboltStatementService; + protected FireboltStatementService fireboltStatementService; @Mock - private FireboltAccountIdService fireboltAccountIdService; - private Properties connectionProperties = new Properties(); + protected FireboltAccountIdService fireboltAccountIdService; + protected Properties connectionProperties = new Properties(); private static Connection connection; private static Stream unsupported() { @@ -159,8 +147,8 @@ void init() throws SQLException { lenient().when(fireboltAuthenticationService.getConnectionTokens(eq("https://api.dev.firebolt.io:443"), any())) .thenReturn(fireboltConnectionTokens); lenient().when(fireboltGatewayUrlService.getUrl(any(), any())).thenReturn("http://foo:8080/bar"); - engine = new Engine("endpoint", "id123", "OK", "noname"); - lenient().when(fireboltEngineService.getEngine(any(), any())).thenReturn(engine); + engine = new Engine("endpoint", "id123", "OK", "noname", null); + lenient().when(fireboltEngineService.getEngine(any())).thenReturn(engine); lenient().when(fireboltEngineService.doesDatabaseExist(any())).thenReturn(true); } @@ -212,26 +200,27 @@ void shouldCloseAllStatementsOnClose() throws SQLException { @Test void createStatement() throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltGatewayUrlService, fireboltStatementService, fireboltEngineService, fireboltAccountIdService); - assertNotNull(fireboltConnection.createStatement()); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertNotNull(fireboltConnection.createStatement()); + } } @Test void createStatementWithParameters() throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltGatewayUrlService, fireboltStatementService, fireboltEngineService, fireboltAccountIdService); - assertNotNull(fireboltConnection.createStatement(TYPE_FORWARD_ONLY, CONCUR_READ_ONLY)); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertNotNull(fireboltConnection.createStatement(TYPE_FORWARD_ONLY, CONCUR_READ_ONLY)); + } } @Test void unsupportedCreateStatementWithParameters() throws SQLException { - FireboltConnection fireboltConnection = createConnection(URL, connectionProperties); - assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_READ_ONLY)); - assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_SENSITIVE, CONCUR_READ_ONLY)); - assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_UPDATABLE)); - assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_SENSITIVE, CONCUR_UPDATABLE)); - assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_FORWARD_ONLY, CONCUR_UPDATABLE)); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_READ_ONLY)); + assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_SENSITIVE, CONCUR_READ_ONLY)); + assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_UPDATABLE)); + assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_SENSITIVE, CONCUR_UPDATABLE)); + assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_FORWARD_ONLY, CONCUR_UPDATABLE)); + } } @Test @@ -276,14 +265,16 @@ void unsupportedPrepareStatement() throws SQLException { @Test void prepareStatement() throws SQLException { - FireboltConnection fireboltConnection = createConnection(URL, connectionProperties); - PreparedStatement ps = fireboltConnection.prepareStatement("select 1", ResultSet.TYPE_FORWARD_ONLY, CONCUR_READ_ONLY); - assertNotNull(ps); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + PreparedStatement ps = fireboltConnection.prepareStatement("select 1", ResultSet.TYPE_FORWARD_ONLY, CONCUR_READ_ONLY); + assertNotNull(ps); + } } private void notSupported(CheckedFunction getter) throws SQLException { - FireboltConnection fireboltConnection = createConnection(URL, connectionProperties); - assertThrows(SQLFeatureNotSupportedException.class, () -> getter.apply(fireboltConnection)); // cannot invoke this method on closed connection + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertThrows(SQLFeatureNotSupportedException.class, () -> getter.apply(fireboltConnection)); // cannot invoke this method on closed connection + } } @Test @@ -332,15 +323,6 @@ void shouldValidateConnectionWhenCallingIsValid() throws SQLException { } } - @Test - void shouldNotValidateConnectionWhenCallingIsValidWhenUsingSystemEngine() throws SQLException { - Properties propertiesWithSystemEngine = new Properties(connectionProperties); - try (FireboltConnection fireboltConnection = createConnection(SYSTEM_ENGINE_URL, propertiesWithSystemEngine)) { - fireboltConnection.isValid(500); - verifyNoInteractions(fireboltStatementService); - } - } - @Test void shouldIgnore429WhenValidatingConnection() throws SQLException { when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), anyBoolean(), any())) @@ -423,11 +405,18 @@ void shouldCloseConnectionWhenAbortingConnection() throws SQLException, Interrup try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { ExecutorService executorService = Executors.newFixedThreadPool(10); fireboltConnection.abort(executorService); - executorService.awaitTermination(1, TimeUnit.SECONDS); + assertFalse(executorService.awaitTermination(1, TimeUnit.SECONDS)); assertTrue(fireboltConnection.isClosed()); } } + @Test + void shouldThrowExceptionIfAbortingWithNullExecutor() throws SQLException, InterruptedException { + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertThrows(FireboltException.class, () -> fireboltConnection.abort(null)); + } + } + @Test void shouldRemoveExpiredToken() throws SQLException { FireboltProperties fireboltProperties = FireboltProperties.builder().host("host").path("/db").port(8080).account("dev").build(); @@ -435,7 +424,7 @@ void shouldRemoveExpiredToken() throws SQLException { when(FireboltProperties.of(any())).thenReturn(fireboltProperties); when(fireboltAuthenticationService.getConnectionTokens("http://host:8080", fireboltProperties)) .thenReturn(FireboltConnectionTokens.builder().build()); - lenient().when(fireboltEngineService.getEngine(any(), any())).thenReturn(new Engine("http://hello", null, null, null)); + lenient().when(fireboltEngineService.getEngine(any())).thenReturn(new Engine("http://hello", null, null, null, null)); try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { fireboltConnection.removeExpiredTokens(); @@ -452,7 +441,7 @@ void shouldReturnConnectionTokenWhenAvailable() throws SQLException { when(FireboltProperties.of(any())).thenReturn(fireboltProperties); FireboltConnectionTokens connectionTokens = FireboltConnectionTokens.builder().accessToken(accessToken).build(); when(fireboltAuthenticationService.getConnectionTokens(eq("http://host:8080"), any())).thenReturn(connectionTokens); - lenient().when(fireboltEngineService.getEngine(any(), any())).thenReturn(new Engine("http://engineHost", null, null, null)); + lenient().when(fireboltEngineService.getEngine(any())).thenReturn(new Engine("http://engineHost", null, null, null, null)); try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { verify(fireboltAuthenticationService).getConnectionTokens("http://host:8080", fireboltProperties); assertEquals(accessToken, fireboltConnection.getAccessToken().get()); @@ -485,9 +474,7 @@ void shouldGetConnectionTokenFromProperties(String host, String configuredAccess if (configuredAccessToken != null) { propsWithToken.setProperty(ACCESS_TOKEN.getKey(), configuredAccessToken); } - - try (FireboltConnection connection = new FireboltConnection(URL, propsWithToken, fireboltAuthenticationService, - fireboltGatewayUrlService, fireboltStatementService, fireboltEngineService, fireboltAccountIdService)) { + try (FireboltConnection connection = createConnection(URL, propsWithToken)) { assertEquals(expectedAccessToken, connection.getAccessToken().orElse(null)); Mockito.verifyNoMoreInteractions(fireboltAuthenticationService); } @@ -499,8 +486,7 @@ void shouldThrowExceptionIfBothAccessTokenAndUserPasswordAreSupplied() { propsWithToken.setProperty(ACCESS_TOKEN.getKey(), "my-token"); propsWithToken.setProperty(CLIENT_ID.getKey(), "my-client"); propsWithToken.setProperty(CLIENT_SECRET.getKey(), "my-secret"); - assertThrows(SQLException.class, () -> new FireboltConnection(URL, propsWithToken, fireboltAuthenticationService, - fireboltGatewayUrlService, fireboltStatementService, fireboltEngineService, fireboltAccountIdService)); + assertThrows(SQLException.class, () -> createConnection(URL, propsWithToken)); } @Test @@ -579,8 +565,7 @@ void shouldThrowExceptionWhenPreparingStatementWIthInvalidResultSetType() throws @Test void createArray() throws SQLException { - try (Connection connection = new FireboltConnection(URL, connectionProperties, fireboltAuthenticationService, - fireboltGatewayUrlService, fireboltStatementService, fireboltEngineService, fireboltAccountIdService)) { + try (Connection connection = createConnection(URL, connectionProperties)) { Object[] data = new Object[] {"red", "green", "blue"}; Array array = connection.createArrayOf("text", data); assertEquals(Types.VARCHAR, array.getBaseType()); @@ -591,51 +576,26 @@ void createArray() throws SQLException { @ParameterizedTest(name = "{0}") @MethodSource("unsupported") void shouldThrowSQLFeatureNotSupportedException(String name, Executable function) throws SQLException { - connection = new FireboltConnection(URL, connectionProperties, fireboltAuthenticationService, - fireboltGatewayUrlService, fireboltStatementService, fireboltEngineService, fireboltAccountIdService); + connection = createConnection(URL, connectionProperties); assertThrows(SQLFeatureNotSupportedException.class, function); } @ParameterizedTest(name = "{0}") @MethodSource("empty") void shouldReturnEmptyResult(String name, Callable function, Object expected) throws Exception { - connection = new FireboltConnection(URL, connectionProperties, fireboltAuthenticationService, - fireboltGatewayUrlService, fireboltStatementService, fireboltEngineService, fireboltAccountIdService); + connection = createConnection(URL, connectionProperties); assertEquals(expected, function.call()); } + @Test void shouldGetEngineUrlWhenEngineIsProvided() throws SQLException { connectionProperties.put("engine", "engine"); - when(fireboltEngineService.getEngine(any(), any())).thenReturn(new Engine("http://my_endpoint", null, null, null)); + when(fireboltEngineService.getEngine(any())).thenReturn(new Engine("http://my_endpoint", null, null, null, null)); try (FireboltConnection connection = createConnection(URL, connectionProperties)) { - verify(fireboltEngineService).getEngine("engine", "db"); + verify(fireboltEngineService).getEngine(argThat(props -> "engine".equals(props.getEngine()) && "db".equals(props.getDatabase()))); assertEquals("http://my_endpoint", connection.getSessionProperties().getHost()); } } - @Test - void shouldNotGetEngineUrlOrDefaultEngineUrlWhenUsingSystemEngine() throws SQLException { - connectionProperties.put("database", "my_db"); - when(fireboltGatewayUrlService.getUrl(any(), any())).thenReturn("http://my_endpoint"); - - try (FireboltConnection connection = createConnection(SYSTEM_ENGINE_URL, connectionProperties)) { - verify(fireboltEngineService, times(0)).getEngine(isNull(), eq("my_db")); - assertEquals("my_endpoint", connection.getSessionProperties().getHost()); - } - } - - @Test - void noEngineAndDb() throws SQLException { - when(fireboltGatewayUrlService.getUrl(any(), any())).thenReturn("http://my_endpoint"); - - try (FireboltConnection connection = createConnection("jdbc:firebolt:?env=dev&account=dev", connectionProperties)) { - assertEquals("my_endpoint", connection.getSessionProperties().getHost()); - assertNull(connection.getSessionProperties().getEngine()); - assertTrue(connection.getSessionProperties().isSystemEngine()); - } - } - - private FireboltConnection createConnection(String url, Properties props) throws SQLException { - return new FireboltConnection(url, props, fireboltAuthenticationService, fireboltGatewayUrlService, fireboltStatementService, fireboltEngineService, fireboltAccountIdService); - } + protected abstract FireboltConnection createConnection(String url, Properties props) throws SQLException; } diff --git a/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionUserPasswordAuthenticationTest.java b/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionUserPasswordAuthenticationTest.java new file mode 100644 index 000000000..ad1324e95 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionUserPasswordAuthenticationTest.java @@ -0,0 +1,65 @@ +package com.firebolt.jdbc.connection; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Properties; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +class FireboltConnectionUserPasswordAuthenticationTest extends FireboltConnectionTest { + private static final String SYSTEM_ENGINE_URL = "jdbc:firebolt:db?env=dev&account=dev&engine=system"; + @Test + void shouldNotValidateConnectionWhenCallingIsValidWhenUsingSystemEngine() throws SQLException { + Properties propertiesWithSystemEngine = new Properties(connectionProperties); + try (FireboltConnection fireboltConnection = createConnection(SYSTEM_ENGINE_URL, propertiesWithSystemEngine)) { + fireboltConnection.isValid(500); + verifyNoInteractions(fireboltStatementService); + } + } + + @Test + void shouldNotGetEngineUrlOrDefaultEngineUrlWhenUsingSystemEngine() throws SQLException { + connectionProperties.put("database", "my_db"); + try (FireboltConnection connection = createConnection(SYSTEM_ENGINE_URL, connectionProperties)) { + verify(fireboltEngineService, times(1)).getEngine(argThat(props -> "my_db".equals(props.getDatabase()) && "system".equals(props.getEngine()))); + assertEquals("endpoint", connection.getSessionProperties().getHost()); + } + } + + + @Test + void noEngineAndDb() throws SQLException { + try (FireboltConnection connection = createConnection("jdbc:firebolt:?env=dev&account=dev", connectionProperties)) { + assertEquals("endpoint", connection.getSessionProperties().getHost()); + assertNotNull(connection.getSessionProperties().getEngine()); + assertFalse(connection.getSessionProperties().isSystemEngine()); + } + } + + @ParameterizedTest + @CsvSource({ + "eng,false", + "system,true" // system engine is readonly + }) + void getMetadata(String engine, boolean readOnly) throws SQLException { + try (FireboltConnection connection = createConnection(format("jdbc:firebolt:db?env=dev&engine=%s&account=dev", engine), connectionProperties)) { + DatabaseMetaData dbmd = connection.getMetaData(); + assertEquals(readOnly, dbmd.isReadOnly()); + } + } + + protected FireboltConnection createConnection(String url, Properties props) throws SQLException { + return new FireboltConnectionUserPasswordAuthentication(url, props, fireboltAuthenticationService, fireboltStatementService, fireboltEngineService); + } +} diff --git a/src/test/java/com/firebolt/jdbc/connection/settings/FireboltPropertiesTest.java b/src/test/java/com/firebolt/jdbc/connection/settings/FireboltPropertiesTest.java index e2f3fa529..ad10a94e8 100644 --- a/src/test/java/com/firebolt/jdbc/connection/settings/FireboltPropertiesTest.java +++ b/src/test/java/com/firebolt/jdbc/connection/settings/FireboltPropertiesTest.java @@ -1,15 +1,20 @@ package com.firebolt.jdbc.connection.settings; -import org.junit.jupiter.api.Disabled; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import java.util.HashMap; import java.util.Map; import java.util.Properties; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class FireboltPropertiesTest { @@ -57,6 +62,15 @@ void shouldHaveAllTheSpecifiedCustomProperties() { assertEquals(expectedDefaultProperties, FireboltProperties.of(properties)); } + @Test + void shouldAddAdditionalProperties() { + FireboltProperties props = FireboltProperties.of(); + assertTrue(props.getAdditionalProperties().isEmpty()); + props.addProperty(new ImmutablePair<>("a", "1")); + props.addProperty("b", "2"); + assertEquals(Map.of("a", "1", "b", "2"), props.getAdditionalProperties()); + } + @Test void shouldUsePathParamAsDb() { Properties properties = new Properties(); @@ -66,6 +80,27 @@ void shouldUsePathParamAsDb() { assertEquals("example", FireboltProperties.of(properties).getDatabase()); } + @ParameterizedTest + @ValueSource(strings = {"$", "@"}) + void invalidDatabase(String db) { + Properties properties = new Properties(); + properties.put("path", db); + assertThrows(IllegalArgumentException.class, () -> FireboltProperties.of(properties)); + } + + @Test + void emptyCopy() { + Properties properties = new Properties(); + properties.put("path", "example"); + properties.put("host", "host"); + assertEquals(FireboltProperties.of(properties), FireboltProperties.copy(FireboltProperties.of(properties))); + } + + @Test + void notEmptyCopy() { + assertEquals(FireboltProperties.of(), FireboltProperties.copy(FireboltProperties.of())); + } + @Test void shouldSupportBooleansForBooleanProperties() { Properties properties = new Properties(); diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltEngineApiServiceTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltEngineApiServiceTest.java new file mode 100644 index 000000000..d66f9678b --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/service/FireboltEngineApiServiceTest.java @@ -0,0 +1,196 @@ +package com.firebolt.jdbc.service; + +import com.firebolt.jdbc.client.account.FireboltAccountClient; +import com.firebolt.jdbc.client.account.response.FireboltAccountResponse; +import com.firebolt.jdbc.client.account.response.FireboltDefaultDatabaseEngineResponse; +import com.firebolt.jdbc.client.account.response.FireboltEngineIdResponse; +import com.firebolt.jdbc.client.account.response.FireboltEngineResponse; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.exception.FireboltException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FireboltEngineApiServiceTest { + private static final String HOST = "host"; + private static final String URL = "http://host"; + private static final String ACCOUNT_ID = "account_id"; + private static final String DB_NAME = "dbName"; + private static final String ENGINE_NAME = "engineName"; + private static final String ENGINE_ID = "engineId"; + private static final String ACCESS_TOKEN = "token"; + + @Mock + private FireboltAccountClient fireboltAccountClient; + + @InjectMocks + private FireboltEngineApiService fireboltEngineService; + + @Test + void shouldGetDefaultDbEngineWhenEngineNameIsNullOrEmpty() throws Exception { + FireboltProperties properties = FireboltProperties.builder().host(HOST).account(ACCOUNT_ID).database(DB_NAME) + .compress(false).accessToken(ACCESS_TOKEN).build(); + + when(fireboltAccountClient.getAccount(properties.getHttpConnectionUrl(), properties.getAccount(), ACCESS_TOKEN)) + .thenReturn(FireboltAccountResponse.builder().accountId(ACCOUNT_ID).build()); + when(fireboltAccountClient.getDefaultEngineByDatabaseName(URL, ACCOUNT_ID, DB_NAME, ACCESS_TOKEN)) + .thenReturn(FireboltDefaultDatabaseEngineResponse.builder().engineUrl("URL").build()); + fireboltEngineService.getEngine(properties); + + verify(fireboltAccountClient).getAccount(URL, properties.getAccount(), ACCESS_TOKEN); + verify(fireboltAccountClient).getDefaultEngineByDatabaseName(URL, ACCOUNT_ID, DB_NAME, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } + + @Test + void shouldThrowExceptionWhenAccountThrowsException() throws Exception { + FireboltProperties properties = FireboltProperties.builder().host(HOST).account(ACCOUNT_ID).database(DB_NAME) + .compress(false).accessToken(ACCESS_TOKEN).build(); + + when(fireboltAccountClient.getAccount(properties.getHttpConnectionUrl(), properties.getAccount(), ACCESS_TOKEN)) + .thenThrow(new IOException()); + assertEquals(IOException.class, assertThrows(FireboltException.class, () ->fireboltEngineService.getEngine(properties)).getCause().getClass()); + } + + @Test + void shouldGThrowExceptionWhenGettingDefaultEngineAndTheUrlReturnedFromTheServerIsNull() throws Exception { + FireboltProperties properties = FireboltProperties.builder().host(HOST).account(ACCOUNT_ID).database(DB_NAME) + .compress(false).accessToken(ACCESS_TOKEN).build(); + + when(fireboltAccountClient.getAccount(properties.getHttpConnectionUrl(), properties.getAccount(), ACCESS_TOKEN)) + .thenReturn(FireboltAccountResponse.builder().accountId(ACCOUNT_ID).build()); + when(fireboltAccountClient.getDefaultEngineByDatabaseName(URL, ACCOUNT_ID, DB_NAME, ACCESS_TOKEN)) + .thenReturn(FireboltDefaultDatabaseEngineResponse.builder().engineUrl(null).build()); + FireboltException exception = assertThrows(FireboltException.class, + () -> fireboltEngineService.getEngine(properties)); + assertEquals( + "There is no Firebolt engine running on http://host attached to the database dbName. To connect first make sure there is a running engine and then try again.", + exception.getMessage()); + + verify(fireboltAccountClient).getAccount(properties.getHttpConnectionUrl(), properties.getAccount(), ACCESS_TOKEN); + verify(fireboltAccountClient).getDefaultEngineByDatabaseName(URL, ACCOUNT_ID, DB_NAME, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } + + @Test + void shouldGetEngineWhenEngineNameIsPresent() throws Exception { + FireboltProperties properties = FireboltProperties.builder().host(HOST).account(ACCOUNT_ID).database(DB_NAME) + .engine(ENGINE_NAME).compress(false).accessToken(ACCESS_TOKEN).build(); + when(fireboltAccountClient.getAccount(URL, properties.getAccount(), ACCESS_TOKEN)) + .thenReturn(FireboltAccountResponse.builder().accountId(ACCOUNT_ID).build()); + when(fireboltAccountClient.getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN)) + .thenReturn(FireboltEngineIdResponse.builder() + .engine(FireboltEngineIdResponse.Engine.builder().engineId(ENGINE_ID).build()).build()); + when(fireboltAccountClient.getEngine(URL, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN)) + .thenReturn(FireboltEngineResponse.builder() + .engine(FireboltEngineResponse.Engine.builder().endpoint("ANY").build()).build()); + fireboltEngineService.getEngine(properties); + + verify(fireboltAccountClient).getAccount(URL, ACCOUNT_ID, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngine(URL, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } + + @Test + void shouldNotGetAccountWhileGettingEngineIfAccountIdIsNotPresent() throws Exception { + FireboltProperties properties = FireboltProperties.builder().host(HOST).database(DB_NAME) + .engine(ENGINE_NAME).compress(false).accessToken(ACCESS_TOKEN).build(); + when(fireboltAccountClient.getEngineId(URL, null, ENGINE_NAME, ACCESS_TOKEN)) + .thenReturn(FireboltEngineIdResponse.builder() + .engine(FireboltEngineIdResponse.Engine.builder().engineId(ENGINE_ID).build()).build()); + when(fireboltAccountClient.getEngine(URL, null, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN)) + .thenReturn(FireboltEngineResponse.builder() + .engine(FireboltEngineResponse.Engine.builder().endpoint("ANY").build()).build()); + fireboltEngineService.getEngine(properties); + + verify(fireboltAccountClient, times(0)).getAccount(any(), any(), any()); + verify(fireboltAccountClient).getEngineId(URL, null, ENGINE_NAME, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngine(URL, null, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } + + @Test + void shouldThrowExceptionWhenEngineNameIsSpecifiedButUrlIsNotPresentInTheResponse() throws Exception { + FireboltProperties properties = FireboltProperties.builder().host(HOST).account(ACCOUNT_ID).database(DB_NAME) + .engine(ENGINE_NAME).compress(false).accessToken(ACCESS_TOKEN).build(); + when(fireboltAccountClient.getAccount(properties.getHttpConnectionUrl(), properties.getAccount(), ACCESS_TOKEN)) + .thenReturn(FireboltAccountResponse.builder().accountId(ACCOUNT_ID).build()); + when(fireboltAccountClient.getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN)) + .thenReturn(FireboltEngineIdResponse.builder() + .engine(FireboltEngineIdResponse.Engine.builder().engineId(ENGINE_ID).build()).build()); + when(fireboltAccountClient.getEngine(URL, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN)) + .thenReturn(FireboltEngineResponse.builder() + .engine(FireboltEngineResponse.Engine.builder().endpoint(null).build()).build()); + FireboltException exception = assertThrows(FireboltException.class, + () -> fireboltEngineService.getEngine(properties)); + assertEquals( + "There is no Firebolt engine running on http://host with the name engineName. To connect first make sure there is a running engine and then try again.", + exception.getMessage()); + + verify(fireboltAccountClient).getAccount(properties.getHttpConnectionUrl(), ACCOUNT_ID, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngine(URL, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } + + @Test + void shouldThrowExceptionWhenEngineNameIsSpecifiedButEngineIdIsNotPresentInTheServerResponse() throws Exception { + FireboltProperties properties = FireboltProperties.builder().host(HOST).account(ACCOUNT_ID).database(DB_NAME) + .engine(ENGINE_NAME).compress(false).accessToken(ACCESS_TOKEN).build(); + when(fireboltAccountClient.getAccount(properties.getHttpConnectionUrl(), properties.getAccount(), ACCESS_TOKEN)) + .thenReturn(FireboltAccountResponse.builder().accountId(ACCOUNT_ID).build()); + when(fireboltAccountClient.getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN)) + .thenReturn(FireboltEngineIdResponse.builder() + .engine(FireboltEngineIdResponse.Engine.builder().engineId(null).build()).build()); + FireboltException exception = assertThrows(FireboltException.class, + () -> fireboltEngineService.getEngine(properties)); + assertEquals( + "Failed to extract engine id field from the server response: the response from the server is invalid.", + exception.getMessage()); + verify(fireboltAccountClient).getAccount(properties.getHttpConnectionUrl(), ACCOUNT_ID, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } + + @ParameterizedTest + @ValueSource(strings = { "ENGINE_STATUS_PROVISIONING_STARTED", "ENGINE_STATUS_PROVISIONING_FINISHED", + "ENGINE_STATUS_PROVISIONING_PENDING" }) + void shouldThrowExceptionWhenEngineStatusIndicatesEngineIsStarting(String status) throws Exception { + FireboltProperties properties = FireboltProperties.builder().host(HOST).account(ACCOUNT_ID).database(DB_NAME) + .engine(ENGINE_NAME).compress(false).accessToken(ACCESS_TOKEN).build(); + when(fireboltAccountClient.getAccount(properties.getHttpConnectionUrl(), properties.getAccount(), ACCESS_TOKEN)) + .thenReturn(FireboltAccountResponse.builder().accountId(ACCOUNT_ID).build()); + when(fireboltAccountClient.getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN)) + .thenReturn(FireboltEngineIdResponse.builder() + .engine(FireboltEngineIdResponse.Engine.builder().engineId(ENGINE_ID).build()).build()); + when(fireboltAccountClient.getEngine(URL, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN)) + .thenReturn(FireboltEngineResponse.builder() + .engine(FireboltEngineResponse.Engine.builder().endpoint("ANY").currentStatus(status).build()) + .build()); + FireboltException exception = assertThrows(FireboltException.class, + () -> fireboltEngineService.getEngine(properties)); + assertEquals("The engine engineName is currently starting. Please wait until the engine is on and then execute the query again.", + exception.getMessage()); + + verify(fireboltAccountClient).getAccount(properties.getHttpConnectionUrl(), ACCOUNT_ID, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngine(URL, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } +} diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltEngineServiceTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaServiceTest.java similarity index 63% rename from src/test/java/com/firebolt/jdbc/service/FireboltEngineServiceTest.java rename to src/test/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaServiceTest.java index 4bf0cc15f..73badb592 100644 --- a/src/test/java/com/firebolt/jdbc/service/FireboltEngineServiceTest.java +++ b/src/test/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaServiceTest.java @@ -2,11 +2,14 @@ import com.firebolt.jdbc.connection.Engine; import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.connection.settings.FireboltProperties; import com.firebolt.jdbc.exception.FireboltException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; @@ -23,44 +26,34 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class FireboltEngineServiceTest { +class FireboltEngineInformationSchemaServiceTest { @InjectMocks - private FireboltEngineService fireboltEngineService; + private FireboltEngineInformationSchemaService fireboltEngineService; @Mock private FireboltConnection fireboltConnection; @Test - void shouldGetEngineNameFromEngineHost() throws SQLException { - assertEquals("myHost_345", fireboltEngineService.getEngineNameByHost("myHost-345.firebolt.io")); + void shouldThrowExceptionEngineWhenEngineNameIsNotProvided() { + FireboltProperties properties = FireboltProperties.builder().database("db").build(); + assertThrows(IllegalArgumentException.class, () -> fireboltEngineService.getEngine(properties)); } - @Test - void shouldThrowExceptionWhenThEngineCannotBeEstablishedFromTheHost() { - assertThrows(FireboltException.class, () -> fireboltEngineService.getEngineNameByHost("myHost-345")); - } - - @Test - void shouldThrowExceptionWhenThEngineCannotBeEstablishedFromNullHost() { - assertThrows(FireboltException.class, () -> fireboltEngineService.getEngineNameByHost(null)); - } - - @Test - void shouldGetDefaultEngineWhenEngineNameIsNotProvided() throws SQLException { - assertThrows(IllegalArgumentException.class, () -> fireboltEngineService.getEngine(null, "db")); - } - - @Test - void shouldGetEngineWhenEngineNameIsProvided() throws SQLException { + @ParameterizedTest + @ValueSource(strings = "db") + @NullSource + void shouldGetEngineWhenEngineNameIsProvided(String db) throws SQLException { PreparedStatement statement = mock(PreparedStatement.class); ResultSet resultSet = mockedResultSet(Map.of("status", "running", "url", "https://url", "attached_to", "db", "engine_name", "some-engine")); when(fireboltConnection.prepareStatement(anyString())).thenReturn(statement); when(statement.executeQuery()).thenReturn(resultSet); - assertEquals(new Engine("https://url", "running", "some-engine", "db"), fireboltEngineService.getEngine("some-engine", "db")); + assertEquals(new Engine("https://url", "running", "some-engine", "db", null), fireboltEngineService.getEngine(createFireboltProperties("some-engine", "db"))); } @ParameterizedTest @@ -83,13 +76,25 @@ void shouldThrowExceptionWhenSomethingIsWrong(String engineName, String db, Stri ResultSet resultSet = mockedResultSet(rsData); when(fireboltConnection.prepareStatement(Mockito.matches(Pattern.compile("SELECT.+JOIN", Pattern.MULTILINE | Pattern.DOTALL)))).thenReturn(statement); when(statement.executeQuery()).thenReturn(resultSet); - assertEquals(errorMessage, assertThrows(FireboltException.class, () -> fireboltEngineService.getEngine(engineName, db)).getMessage()); + assertEquals(errorMessage, assertThrows(FireboltException.class, () -> fireboltEngineService.getEngine(createFireboltProperties(engineName, db))).getMessage()); Mockito.verify(statement, Mockito.times(1)).setString(1, engineName); } + @ParameterizedTest + @CsvSource(value = {"mydb;'';false", "other_db;'database_name,other_db';true"}, delimiter = ';') + void doesDatabaseExist(String db, String row, boolean expected) throws SQLException { + PreparedStatement statement = mock(PreparedStatement.class); + Map rowData = row == null || row.isEmpty() ? Map.of() : Map.of(row.split(",")[0], row.split(",")[1]); + ResultSet resultSet = mockedResultSet(rowData); + when(fireboltConnection.prepareStatement("SELECT database_name FROM information_schema.databases WHERE database_name=?")).thenReturn(statement); + when(statement.executeQuery()).thenReturn(resultSet); + assertEquals(expected, fireboltEngineService.doesDatabaseExist(db)); + Mockito.verify(statement, Mockito.times(1)).setString(1, db); + } + private ResultSet mockedResultSet(Map values) throws SQLException { ResultSet resultSet = mock(ResultSet.class); - if (values == null) { + if (values == null || values.isEmpty()) { when(resultSet.next()).thenReturn(false); } else { when(resultSet.next()).thenReturn(true, false); @@ -99,4 +104,8 @@ private ResultSet mockedResultSet(Map values) throws SQLExceptio } return resultSet; } + + private FireboltProperties createFireboltProperties(String engine, String database) { + return FireboltProperties.builder().engine(engine).database(database).build(); + } } diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltStatementServiceTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltStatementServiceTest.java index baedb338a..0b0df63eb 100644 --- a/src/test/java/com/firebolt/jdbc/service/FireboltStatementServiceTest.java +++ b/src/test/java/com/firebolt/jdbc/service/FireboltStatementServiceTest.java @@ -1,14 +1,13 @@ package com.firebolt.jdbc.service; -import static org.junit.Assert.assertThrows; -import static org.mockito.Mockito.*; - import java.sql.SQLException; import java.util.Optional; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.MockedConstruction; import org.mockito.Mockito; @@ -22,6 +21,12 @@ import com.firebolt.jdbc.statement.StatementInfoWrapper; import com.firebolt.jdbc.statement.StatementUtil; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class FireboltStatementServiceTest { @@ -121,4 +126,18 @@ void shouldBeEmptyWithNonQueryStatement() throws SQLException { verify(statementClient).executeSqlStatement(statementInfoWrapper, fireboltProperties, false, -1, true); } + @Test + void abortStatementHttpRequest() throws FireboltException { + FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient); + fireboltStatementService.abortStatementHttpRequest("id"); + verify(statementClient).abortRunningHttpRequest("id"); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void isStatementRunning(boolean running) { + FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient); + when(statementClient.isStatementRunning("id")).thenReturn(running); + Assertions.assertEquals(running, fireboltStatementService.isStatementRunning("id")); + } }