From 8f57ba88531b71005c58a1fc26963fcbeb03a2c4 Mon Sep 17 00:00:00 2001 From: Aashir Siddiqui Date: Tue, 3 Sep 2024 10:04:19 +0100 Subject: [PATCH 01/13] Implemented get user's access token by loginId Signed-off-by: Aashir Siddiqui --- .../couchdb/internal/CouchdbAuthStore.java | 23 ++ .../internal/CouchdbAuthStoreValidator.java | 154 ++++++++++++- .../auth/couchdb/TestCouchdbAuthStore.java | 37 ++++ .../TestCouchdbAuthStoreValidator.java | 207 +++++++++++++++++- .../common/couchdb/CouchdbBaseValidator.java | 4 + .../common/couchdb/CouchdbStore.java | 26 +++ .../extensions/mocks/BaseHttpInteraction.java | 17 +- .../mocks/MockCloseableHttpClient.java | 2 +- .../internal/CouchdbRasRegistration.java | 1 - .../ras/couchdb/internal/CouchdbRasStore.java | 4 +- .../internal/CouchdbValidatorImpl.java | 68 +++--- .../internal/CouchdbValidatorImplTest.java | 4 +- .../internal/mocks/MockCouchdbValidator.java | 6 +- 13 files changed, 489 insertions(+), 64 deletions(-) diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java index 1df743ed..2d6657f1 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java @@ -83,6 +83,29 @@ public List getTokens() throws AuthStoreException { return tokens; } + @Override + public List getTokensByLoginId(String loginId) throws AuthStoreException { + logger.info("Retrieving tokens from CouchDB"); + List tokenDocuments = new ArrayList<>(); + List tokens = new ArrayList<>(); + + try { + // Get all of the documents in the tokens database + tokenDocuments = getAllDocsByLoginId(TOKENS_DATABASE_NAME, loginId); + + // Build up a list of all the tokens using the document IDs + for (ViewRow row : tokenDocuments) { + tokens.add(getAuthTokenFromDocument(row.key)); + } + + logger.info("Tokens retrieved from CouchDB OK"); + } catch (CouchdbException e) { + String errorMessage = ERROR_FAILED_TO_RETRIEVE_TOKENS.getMessage(e.getMessage()); + throw new AuthStoreException(errorMessage, e); + } + return tokens; + } + @Override public void shutdown() throws AuthStoreException { try { diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java index 2a2214d0..ab0eaa55 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java @@ -6,26 +6,176 @@ package dev.galasa.auth.couchdb.internal; import java.net.URI; +import java.util.Random; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import dev.galasa.extensions.common.couchdb.CouchdbBaseValidator; import dev.galasa.extensions.common.couchdb.CouchdbException; import dev.galasa.extensions.common.api.HttpRequestFactory; +import dev.galasa.framework.spi.utils.GalasaGson; -public class CouchdbAuthStoreValidator extends CouchdbBaseValidator { +public class CouchdbAuthStoreValidator extends CouchdbBaseValidator{ private final Log logger = LogFactory.getLog(getClass()); + private final GalasaGson gson = new GalasaGson(); + + @Override public void checkCouchdbDatabaseIsValid(URI couchdbUri, CloseableHttpClient httpClient, HttpRequestFactory httpRequestFactory) throws CouchdbException { + // Perform the base CouchDB checks super.checkCouchdbDatabaseIsValid(couchdbUri, httpClient, httpRequestFactory); - validateDatabasePresent(couchdbUri, CouchdbAuthStore.TOKENS_DATABASE_NAME); + checkTokensDesignDocument(httpClient, couchdbUri, 1); logger.debug("Auth Store CouchDB at " + couchdbUri.toString() + " validated"); + + } + + public void checkTokensDesignDocument( CloseableHttpClient httpClient , URI couchdbUri , int attempts) throws CouchdbException { + HttpRequestFactory requestFactory = super.getRequestFactory(); + HttpGet httpGet = requestFactory.getHttpGetRequest(couchdbUri + "/galasa_tokens/_design/docs"); + + String docJson = null; + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { + + StatusLine statusLine = response.getStatusLine(); + + docJson = EntityUtils.toString(response.getEntity()); + if (statusLine.getStatusCode() != HttpStatus.SC_OK + && statusLine.getStatusCode() != HttpStatus.SC_NOT_FOUND) { + throw new CouchdbException( + "Validation failed of database galasa_tokens design document - " + statusLine.toString()); + } + if (statusLine.getStatusCode() == HttpStatus.SC_NOT_FOUND) { + docJson = "{}"; + } + } catch (CouchdbException e) { + throw e; + } catch (Exception e) { + throw new CouchdbException("Validation failed", e); + } + + boolean updated = false; + + JsonObject doc = gson.fromJson(docJson, JsonObject.class); + doc.remove("_id"); + String rev = null; + if (doc.has("_rev")) { + rev = doc.get("_rev").getAsString(); + } + + JsonObject views = doc.getAsJsonObject("views"); + if (views == null) { + updated = true; + views = new JsonObject(); + doc.add("views", views); + } + + JsonObject requestors = views.getAsJsonObject("loginId-view"); + if (requestors == null) { + updated = true; + requestors = new JsonObject(); + views.add("loginId-view", requestors); + } + + if (checkView(requestors, "function (doc) { if (doc.owner && doc.owner.loginId) {emit(doc.owner.loginId, doc); } }", "javascript")) { + updated = true; + } + + if (updated) { + logger.info("Updating the galasa_tokens design document"); + + HttpEntity entity = new StringEntity(gson.toJson(doc), ContentType.APPLICATION_JSON); + + HttpPut httpPut = requestFactory.getHttpPutRequest(couchdbUri + "/galasa_tokens/_design/docs"); + httpPut.setEntity(entity); + + if (rev != null) { + httpPut.addHeader("ETaq", "\"" + rev + "\""); + } + + try (CloseableHttpResponse response = httpClient.execute(httpPut)) { + StatusLine statusLine = response.getStatusLine(); + int statusCode = statusLine.getStatusCode(); + if (statusCode == HttpStatus.SC_CONFLICT) { + // Someone possibly updated + attempts++; + if (attempts > 10) { + throw new CouchdbException( + "Update of galasa_token design document failed on CouchDB server due to conflicts, attempted 10 times"); + } + Thread.sleep(1000 + new Random().nextInt(3000)); + checkTokensDesignDocument(httpClient, couchdbUri, attempts); + return; + } + + if (statusCode != HttpStatus.SC_CREATED) { + EntityUtils.consumeQuietly(response.getEntity()); + throw new CouchdbException( + "Update of galasa_tokens design document failed on CouchDB server - " + statusLine.toString()); + } + + EntityUtils.consumeQuietly(response.getEntity()); + } catch (CouchdbException e) { + throw e; + } catch (Exception e) { + throw new CouchdbException("Update of galasa_tokens design document failed", e); + } + } + } + private boolean checkView(JsonObject view, String targetMap, String targetReduce) { + + boolean updated = false; + + if (checkViewString(view, "map", targetMap)) { + updated = true; + } + if (checkViewString(view, "reduce", targetReduce)) { + updated = true; + } + if (checkViewString(view, "language", "javascript")) { + updated = true; + } + + return updated; + } + + private boolean checkViewString(JsonObject view, String field, String value) { + + JsonElement element = view.get(field); + if (element == null) { + view.addProperty(field, value); + return true; + } + + if (!element.isJsonPrimitive() || !((JsonPrimitive) element).isString()) { + view.addProperty(field, value); + return true; + } + + String actualValue = element.getAsString(); + if (!value.equals(actualValue)) { + view.addProperty(field, value); + return true; + } + return false; } } diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStore.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStore.java index 59d926d0..9da9e6c1 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStore.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStore.java @@ -157,6 +157,43 @@ public void testGetTokensReturnsTokensFromCouchdbOK() throws Exception { assertThat(actualToken).usingRecursiveComparison().isEqualTo(mockToken); } + @Test + public void testGetTokensReturnsTokensByLoginIdFromCouchdbOK() throws Exception { + // Given... + URI authStoreUri = URI.create("couchdb:https://my-auth-store"); + MockLogFactory logFactory = new MockLogFactory(); + + ViewRow tokenDoc = new ViewRow(); + tokenDoc.key = "token1"; + List mockDocs = List.of(tokenDoc); + + ViewResponse mockAllDocsResponse = new ViewResponse(); + mockAllDocsResponse.rows = mockDocs; + + CouchdbAuthToken mockToken = new CouchdbAuthToken("token1", "dex-client", "my test token", Instant.now(), new CouchdbUser("johndoe", "dex-user-id")); + CouchdbAuthToken mockToken2 = new CouchdbAuthToken("token2", "dex-client", "my test token", Instant.now(), new CouchdbUser("notJohnDoe", "dex-user-id")); + List interactions = new ArrayList(); + interactions.add(new GetAllTokenDocumentsInteraction("https://my-auth-store/galasa_tokens/_design/docs/_view/loginId-view?key=johndoe", HttpStatus.SC_OK, mockAllDocsResponse)); + interactions.add(new GetTokenDocumentInteraction("https://my-auth-store/galasa_tokens/token1", HttpStatus.SC_OK, mockToken)); + interactions.add(new GetTokenDocumentInteraction("https://my-auth-store/galasa_tokens/token1", HttpStatus.SC_OK, mockToken2)); + + MockCloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + + MockHttpClientFactory httpClientFactory = new MockHttpClientFactory(mockHttpClient); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); + + CouchdbAuthStore authStore = new CouchdbAuthStore(authStoreUri, httpClientFactory, new HttpRequestFactoryImpl(), logFactory, new MockCouchdbValidator(), mockTimeService); + + // When... + List tokens = authStore.getTokensByLoginId("johndoe"); + + // Then... + assertThat(tokens).hasSize(1); + + IInternalAuthToken actualToken = tokens.get(0); + assertThat(actualToken).usingRecursiveComparison().isEqualTo(mockToken); + } + @Test public void testStoreTokenSendsRequestToCreateTokenDocumentOK() throws Exception { // Given... diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java index 78cae24f..86c7f0d6 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java @@ -17,6 +17,8 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.junit.Test; +import com.google.gson.annotations.SerializedName; + import dev.galasa.auth.couchdb.internal.CouchdbAuthStore; import dev.galasa.auth.couchdb.internal.CouchdbAuthStoreValidator; import dev.galasa.extensions.common.couchdb.CouchdbBaseValidator; @@ -76,6 +78,58 @@ public void validateRequest(HttpHost host, HttpRequest request) throws RuntimeEx } } + class GetTokensDatabaseDesignInteraction extends BaseHttpInteraction { + public GetTokensDatabaseDesignInteraction(String expectedUri, Object returnedDocument) { + this(expectedUri, returnedDocument, HttpStatus.SC_OK); + } + + public GetTokensDatabaseDesignInteraction(String expectedUri, Object returnedDocument, int expectedResponseCode) { + super(expectedUri, returnedDocument, expectedResponseCode); + } + + @Override + public void validateRequest(HttpHost host, HttpRequest request) throws RuntimeException { + super.validateRequest(host,request); + assertThat(request.getRequestLine().getMethod()).isEqualTo("GET"); + } + } + + class UpdateTokensDatabaseDesignInteraction extends BaseHttpInteraction { + public UpdateTokensDatabaseDesignInteraction(String expectedUri, String returnedDocument, int expectedResponseCode) { + super(expectedUri, returnedDocument, expectedResponseCode); + setResponsePayload(returnedDocument); + } + + @Override + public void validateRequest(HttpHost host, HttpRequest request) throws RuntimeException { + super.validateRequest(host,request); + assertThat(request.getRequestLine().getMethod()).isEqualTo("PUT"); + } + } + + + +// "_id": "_design/docs", +// "_rev": "3-9e69612124f138c029ab40c9c9072deb", +// "views": { +// "loginId-view": { +// "map": "function (doc) {\n if (doc.owner && doc.owner.loginId) {\n emit(doc.owner.loginId, doc);\n }\n}" +// } +// }, +// "language": "javascript" +// } + public static class TokensDBNameViewDesign { + TokenDBViews views; + String language; + } + public static class TokenDBViews { + @SerializedName("loginId-view") + TokenDBLoginView loginIdView; + } + public static class TokenDBLoginView { + String map; + } + @Test public void testCheckCouchdbDatabaseIsValidWithValidDatabaseIsOK() throws Exception { // Given... @@ -90,16 +144,35 @@ public void testCheckCouchdbDatabaseIsValidWithValidDatabaseIsOK() throws Except List interactions = new ArrayList<>(); interactions.add(new GetCouchdbWelcomeInteraction(couchdbUriStr, welcomeMessage)); interactions.add(new GetTokensDatabaseInteraction(couchdbUriStr + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME, HttpStatus.SC_OK)); + + + // We are expecting this to ne returned from our mock couchdb server to the code: + // "_id": "_design/docs", + // "_rev": "3-9e69612124f138c029ab40c9c9072deb", + // "views": { + // "loginId-view": { + // "map": "function (doc) {\n if (doc.owner && doc.owner.loginId) {\n emit(doc.owner.loginId, doc);\n }\n}" + // } + // }, + // "language": "javascript" + // } + TokenDBLoginView view = new TokenDBLoginView(); + view.map = "function (doc) {\n if (doc.owner && doc.owner.loginId) {\n emit(doc.owner.loginId, doc);\n }\n}"; + TokenDBViews views = new TokenDBViews(); + views.loginIdView = view; + TokensDBNameViewDesign designDocToPassBack = new TokensDBNameViewDesign(); + designDocToPassBack.language = "javascript"; + + + String tokensDesignDocUrl = couchdbUriStr + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME + "/_design/docs"; + interactions.add(new GetTokensDatabaseDesignInteraction(tokensDesignDocUrl, designDocToPassBack)); + + + interactions.add(new UpdateTokensDatabaseDesignInteraction(tokensDesignDocUrl, "", HttpStatus.SC_CREATED)); CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); // When... - Throwable thrown = catchThrowable( - () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()) - ); - - // Then... - // The validation should have passed, so no errors should have been thrown - assertThat(thrown).isNull(); + validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()); } @Test @@ -147,17 +220,27 @@ public void testCheckCouchdbDatabaseIsValidWithSuccessfulDatabaseCreationIsOK() interactions.add(new GetCouchdbWelcomeInteraction(couchdbUriStr, welcomeMessage)); interactions.add(new GetTokensDatabaseInteraction(couchdbUriStr + "/" + tokensDatabaseName, HttpStatus.SC_NOT_FOUND)); interactions.add(new CreateDatabaseInteraction(couchdbUriStr + "/" + tokensDatabaseName, HttpStatus.SC_CREATED)); + + TokenDBLoginView view = new TokenDBLoginView(); + view.map = "function (doc) {\n if (doc.owner && doc.owner.loginId) {\n emit(doc.owner.loginId, doc);\n }\n}"; + TokenDBViews views = new TokenDBViews(); + views.loginIdView = view; + TokensDBNameViewDesign designDocToPassBack = new TokensDBNameViewDesign(); + designDocToPassBack.language = "javascript"; + + + String tokensDesignDocUrl = couchdbUriStr + "/" + tokensDatabaseName + "/_design/docs"; + interactions.add(new GetTokensDatabaseDesignInteraction(tokensDesignDocUrl, designDocToPassBack)); + + + interactions.add(new UpdateTokensDatabaseDesignInteraction(tokensDesignDocUrl, "",HttpStatus.SC_CREATED)); CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); // When... - CouchdbException thrown = catchThrowableOfType( - () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()), - CouchdbException.class - ); + validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()); // Then... // The validation should have passed, so no errors should have been thrown - assertThat(thrown).isNull(); } @Test @@ -331,4 +414,104 @@ public void testCheckCouchdbDatabaseIsValidWithPatchVersionMismatchThrowsError() assertThat(thrown).isNotNull(); assertThat(thrown.getMessage()).contains("GAL6005E", "Expected version '" + CouchdbBaseValidator.COUCHDB_MIN_VERSION + "' or above"); } + + @Test + public void testCheckCouchdbDatabaseIsValidWithFailedDesignDocResponseThrowsError() throws Exception { + // Given... + String couchdbUriStr = "https://my-couchdb-server"; + URI couchdbUri = URI.create(couchdbUriStr); + CouchdbAuthStoreValidator validator = new CouchdbAuthStoreValidator(); + + Welcome welcomeMessage = new Welcome(); + welcomeMessage.couchdb = "Welcome"; + welcomeMessage.version = CouchdbBaseValidator.COUCHDB_MIN_VERSION; + + List interactions = new ArrayList<>(); + interactions.add(new GetCouchdbWelcomeInteraction(couchdbUriStr, welcomeMessage)); + interactions.add(new GetTokensDatabaseInteraction(couchdbUriStr + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME, HttpStatus.SC_OK)); + + + // We are expecting this to ne returned from our mock couchdb server to the code: + // "_id": "_design/docs", + // "_rev": "3-9e69612124f138c029ab40c9c9072deb", + // "views": { + // "loginId-view": { + // "map": "function (doc) {\n if (doc.owner && doc.owner.loginId) {\n emit(doc.owner.loginId, doc);\n }\n}" + // } + // }, + // "language": "javascript" + // } + TokenDBLoginView view = new TokenDBLoginView(); + view.map = "function (doc) {\n if (doc.owner && doc.owner.loginId) {\n emit(doc.owner.loginId, doc);\n }\n}"; + TokenDBViews views = new TokenDBViews(); + views.loginIdView = view; + TokensDBNameViewDesign designDocToPassBack = new TokensDBNameViewDesign(); + designDocToPassBack.language = "javascript"; + + + String tokensDesignDocUrl = couchdbUriStr + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME + "/_design/docs"; + interactions.add(new GetTokensDatabaseDesignInteraction(tokensDesignDocUrl, designDocToPassBack, HttpStatus.SC_INTERNAL_SERVER_ERROR)); + CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + + // When... + CouchdbException thrown = catchThrowableOfType( + () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()), + CouchdbException.class + ); + + // Then... + assertThat(thrown).isNotNull(); + assertThat(thrown.getMessage()).contains("Validation failed of database galasa_tokens design document"); + } + + @Test + public void testCheckCouchdbDatabaseIsValidWithUpdateDesignDocResponseThrowsError() throws Exception { + // Given... + String couchdbUriStr = "https://my-couchdb-server"; + URI couchdbUri = URI.create(couchdbUriStr); + CouchdbAuthStoreValidator validator = new CouchdbAuthStoreValidator(); + + Welcome welcomeMessage = new Welcome(); + welcomeMessage.couchdb = "Welcome"; + welcomeMessage.version = CouchdbBaseValidator.COUCHDB_MIN_VERSION; + + List interactions = new ArrayList<>(); + interactions.add(new GetCouchdbWelcomeInteraction(couchdbUriStr, welcomeMessage)); + interactions.add(new GetTokensDatabaseInteraction(couchdbUriStr + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME, HttpStatus.SC_OK)); + + + // We are expecting this to ne returned from our mock couchdb server to the code: + // "_id": "_design/docs", + // "_rev": "3-9e69612124f138c029ab40c9c9072deb", + // "views": { + // "loginId-view": { + // "map": "function (doc) {\n if (doc.owner && doc.owner.loginId) {\n emit(doc.owner.loginId, doc);\n }\n}" + // } + // }, + // "language": "javascript" + // } + TokenDBLoginView view = new TokenDBLoginView(); + view.map = "function (doc) {\n if (doc.owner && doc.owner.loginId) {\n emit(doc.owner.loginId, doc);\n }\n}"; + TokenDBViews views = new TokenDBViews(); + views.loginIdView = view; + TokensDBNameViewDesign designDocToPassBack = new TokensDBNameViewDesign(); + designDocToPassBack.language = "javascript"; + + + String tokensDesignDocUrl = couchdbUriStr + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME + "/_design/docs"; + interactions.add(new GetTokensDatabaseDesignInteraction(tokensDesignDocUrl, designDocToPassBack, HttpStatus.SC_OK)); + interactions.add(new UpdateTokensDatabaseDesignInteraction(tokensDesignDocUrl, "", HttpStatus.SC_INTERNAL_SERVER_ERROR)); + CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + + // When... + CouchdbException thrown = catchThrowableOfType( + () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()), + CouchdbException.class + ); + + // Then... + assertThat(thrown).isNotNull(); + assertThat(thrown.getMessage()).contains("Update of galasa_tokens design"); + } + } diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbBaseValidator.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbBaseValidator.java index 46321299..49170f9d 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbBaseValidator.java +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbBaseValidator.java @@ -73,6 +73,10 @@ public void checkCouchdbDatabaseIsValid(URI couchdbUri, CloseableHttpClient http } } + public HttpRequestFactory getRequestFactory() { + return requestFactory; + } + /** * Checks if a database with the given name exists in the CouchDB server. If not, the database is created. * diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbStore.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbStore.java index 0c6062f4..8bd5878d 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbStore.java +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbStore.java @@ -117,6 +117,32 @@ protected List getAllDocsFromDatabase(String dbName) throws CouchdbExce return viewRows; } + /** + * Sends a GET request to CouchDB's /{db}/_design/docs/_view/loginId-view?key={loginId} endpoint and returns the "rows" list in the response, + * which corresponds to the list of documents within the given database. + * + * @param dbName the name of the database to retrieve the documents of + * @param loginId the loginId of the user to retrieve the doucemnts of + * @return a list of rows corresponding to documents within the database + * @throws CouchdbException if there was a problem accessing the CouchDB store or its response + */ + protected List getAllDocsByLoginId(String dbName, String loginId) throws CouchdbException { + HttpGet getTokensDocs = httpRequestFactory.getHttpGetRequest(storeUri + "/" + dbName + "/_design/docs/_view/loginId-view?key=" + loginId); + getTokensDocs.addHeader("Content-Type", "application/json"); + + String responseEntity = sendHttpRequest(getTokensDocs, HttpStatus.SC_OK); + + ViewResponse docByLoginId = gson.fromJson(responseEntity, ViewResponse.class); + List viewRows = docByLoginId.rows; + + if (viewRows == null) { + String errorMessage = ERROR_FAILED_TO_GET_DOCUMENTS_FROM_DATABASE.getMessage(dbName); + throw new CouchdbException(errorMessage); + } + + return viewRows; + } + /** * Gets an object from a given database's document using its document ID by sending a * GET /{db}/{docid} request to the CouchDB server. diff --git a/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/BaseHttpInteraction.java b/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/BaseHttpInteraction.java index c80a42c3..3648486c 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/BaseHttpInteraction.java +++ b/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/BaseHttpInteraction.java @@ -19,27 +19,26 @@ public abstract class BaseHttpInteraction implements HttpInteraction { private GalasaGson gson = new GalasaGson(); private String expectedBaseUri ; - private String returnedDocument; private String responsePayload = ""; private int responseStatusCode = HttpStatus.SC_OK; - public BaseHttpInteraction(String expectedBaseUri, String returnedDocument ) { - this.expectedBaseUri = expectedBaseUri; - this.returnedDocument = returnedDocument; + public BaseHttpInteraction(String expectedBaseUri, Object responsePayload) { + this(expectedBaseUri, responsePayload, HttpStatus.SC_OK); } - public BaseHttpInteraction(String expectedBaseUri, int responseStatusCode) { + public BaseHttpInteraction(String expectedBaseUri, Object responsePayload, int responseStatusCode) { this.expectedBaseUri = expectedBaseUri; this.responseStatusCode = responseStatusCode; + setResponsePayload(responsePayload); } - public String getExpectedBaseUri() { - return this.expectedBaseUri; + public BaseHttpInteraction(String expectedBaseUri, int responseStatusCode) { + this(expectedBaseUri, null, responseStatusCode); } - public String getReturnedDocument() { - return this.returnedDocument; + public String getExpectedBaseUri() { + return this.expectedBaseUri; } public String getExpectedHttpContentType() { diff --git a/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/MockCloseableHttpClient.java b/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/MockCloseableHttpClient.java index b3dd3d7e..8a8cff28 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/MockCloseableHttpClient.java +++ b/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/MockCloseableHttpClient.java @@ -35,7 +35,7 @@ private void nextInteraction() { if (interactionWalker.hasNext()) { this.currentInteraction = interactionWalker.next(); } else { - this.currentInteraction = null ; + this.currentInteraction = null ; } } diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbRasRegistration.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbRasRegistration.java index e4e4b453..1c2bcbcb 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbRasRegistration.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbRasRegistration.java @@ -6,7 +6,6 @@ package dev.galasa.ras.couchdb.internal; import java.net.URI; -import java.net.URISyntaxException; import java.util.List; import javax.validation.constraints.NotNull; diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbRasStore.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbRasStore.java index 28a3aa22..dde0c98f 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbRasStore.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbRasStore.java @@ -36,6 +36,7 @@ import dev.galasa.extensions.common.api.LogFactory; import dev.galasa.extensions.common.couchdb.CouchdbException; import dev.galasa.extensions.common.couchdb.CouchdbStore; +import dev.galasa.extensions.common.couchdb.CouchdbValidator; import dev.galasa.extensions.common.couchdb.pojos.PutPostResponse; import dev.galasa.extensions.common.impl.HttpClientFactoryImpl; import dev.galasa.extensions.common.impl.HttpRequestFactoryImpl; @@ -93,7 +94,7 @@ public CouchdbRasStore(IFramework framework, URI rasUri) throws CouchdbException // Note: We use logFactory here so we can propogate it downwards during unit testing. public CouchdbRasStore(IFramework framework, URI rasUri, HttpClientFactory httpFactory , CouchdbValidator validator, LogFactory logFactory, HttpRequestFactory requestFactory - ) throws CouchdbRasException, CouchdbException { + ) throws CouchdbException { super(rasUri, requestFactory, httpFactory); this.logFactory = logFactory; this.logger = logFactory.getLog(getClass()); @@ -121,7 +122,6 @@ public CouchdbRasStore(IFramework framework, URI rasUri, HttpClientFactory httpF this.provider = new CouchdbRasFileSystemProvider(fileStore, this, this.logFactory); } - // Protected so that we can create artifact documents from elsewhere. protected void createArtifactDocument() throws CouchdbException { Artifacts artifacts = new Artifacts(); diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java index 1ad4d522..36d1f460 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java @@ -33,6 +33,8 @@ import dev.galasa.extensions.common.couchdb.pojos.Welcome; import dev.galasa.extensions.common.api.HttpRequestFactory; +import dev.galasa.extensions.common.couchdb.CouchdbException; +import dev.galasa.extensions.common.couchdb.CouchdbValidator; import dev.galasa.framework.spi.utils.GalasaGson; public class CouchdbValidatorImpl implements CouchdbValidator { @@ -41,7 +43,7 @@ public class CouchdbValidatorImpl implements CouchdbValidator { private final Log logger = LogFactory.getLog(getClass()); private HttpRequestFactory requestFactory; - public void checkCouchdbDatabaseIsValid( URI rasUri, CloseableHttpClient httpClient , HttpRequestFactory httpRequestFactory) throws CouchdbRasException { + public void checkCouchdbDatabaseIsValid( URI rasUri, CloseableHttpClient httpClient , HttpRequestFactory httpRequestFactory) throws CouchdbException { this.requestFactory = httpRequestFactory; HttpGet httpGet = requestFactory.getHttpGetRequest(rasUri.toString()); @@ -76,16 +78,16 @@ public void checkCouchdbDatabaseIsValid( URI rasUri, CloseableHttpClient httpCli checkIndex(httpClient, rasUri, 1, "galasa_run", "result"); logger.debug("RAS CouchDB at " + rasUri.toString() + " validated"); - } catch (CouchdbRasException e) { + } catch (CouchdbException e) { throw e; } catch (Exception e) { - throw new CouchdbRasException("Validation failed", e); + throw new CouchdbException("Validation failed "+ e); } } - private void checkDatabasePresent( CloseableHttpClient httpClient, URI rasUri, int attempts, String dbName) throws CouchdbRasException { + private void checkDatabasePresent( CloseableHttpClient httpClient, URI rasUri, int attempts, String dbName) throws CouchdbException { HttpHead httpHead = requestFactory.getHttpHeadRequest(rasUri + "/" + dbName); try (CloseableHttpResponse response = httpClient.execute(httpHead)) { @@ -95,13 +97,13 @@ private void checkDatabasePresent( CloseableHttpClient httpClient, URI rasUri, i return; } if (statusLine.getStatusCode() != HttpStatus.SC_NOT_FOUND) { - throw new CouchdbRasException( + throw new CouchdbException( "Validation failed of database " + dbName + " - " + statusLine.toString()); } - } catch (CouchdbRasException e) { + } catch (CouchdbException e) { throw e; } catch (Exception e) { - throw new CouchdbRasException("Validation failed", e); + throw new CouchdbException("Validation failed", e); } logger.info("CouchDB database " + dbName + " is missing, creating"); @@ -114,7 +116,7 @@ private void checkDatabasePresent( CloseableHttpClient httpClient, URI rasUri, i // Someone possibly updated attempts++; if (attempts > 10) { - throw new CouchdbRasException( + throw new CouchdbException( "Create Database " + dbName + " failed on CouchDB server due to conflicts, attempted 10 times"); } Thread.sleep(1000 + new Random().nextInt(3000)); @@ -124,19 +126,19 @@ private void checkDatabasePresent( CloseableHttpClient httpClient, URI rasUri, i if (statusLine.getStatusCode() != HttpStatus.SC_CREATED) { EntityUtils.consumeQuietly(response.getEntity()); - throw new CouchdbRasException( + throw new CouchdbException( "Create Database " + dbName + " failed on CouchDB server - " + statusLine.toString()); } EntityUtils.consumeQuietly(response.getEntity()); - } catch (CouchdbRasException e) { + } catch (CouchdbException e) { throw e; } catch (Exception e) { - throw new CouchdbRasException("Create database " + dbName + " failed", e); + throw new CouchdbException("Create database " + dbName + " failed", e); } } - private void checkRunDesignDocument( CloseableHttpClient httpClient , URI rasUri , int attempts) throws CouchdbRasException { + private void checkRunDesignDocument( CloseableHttpClient httpClient , URI rasUri , int attempts) throws CouchdbException { HttpGet httpGet = requestFactory.getHttpGetRequest(rasUri + "/galasa_run/_design/docs"); String docJson = null; @@ -145,16 +147,16 @@ private void checkRunDesignDocument( CloseableHttpClient httpClient , URI rasUri docJson = EntityUtils.toString(response.getEntity()); if (statusLine.getStatusCode() != HttpStatus.SC_OK && statusLine.getStatusCode() != HttpStatus.SC_NOT_FOUND) { - throw new CouchdbRasException( + throw new CouchdbException( "Validation failed of database galasa_run designdocument - " + statusLine.toString()); } if (statusLine.getStatusCode() == HttpStatus.SC_NOT_FOUND) { docJson = "{}"; } - } catch (CouchdbRasException e) { + } catch (CouchdbException e) { throw e; } catch (Exception e) { - throw new CouchdbRasException("Validation failed", e); + throw new CouchdbException("Validation failed", e); } boolean updated = false; @@ -236,7 +238,7 @@ private void checkRunDesignDocument( CloseableHttpClient httpClient , URI rasUri // Someone possibly updated attempts++; if (attempts > 10) { - throw new CouchdbRasException( + throw new CouchdbException( "Update of galasa_run design document failed on CouchDB server due to conflicts, attempted 10 times"); } Thread.sleep(1000 + new Random().nextInt(3000)); @@ -246,15 +248,15 @@ private void checkRunDesignDocument( CloseableHttpClient httpClient , URI rasUri if (statusCode != HttpStatus.SC_CREATED) { EntityUtils.consumeQuietly(response.getEntity()); - throw new CouchdbRasException( + throw new CouchdbException( "Update of galasa_run design document failed on CouchDB server - " + statusLine.toString()); } EntityUtils.consumeQuietly(response.getEntity()); - } catch (CouchdbRasException e) { + } catch (CouchdbException e) { throw e; } catch (Exception e) { - throw new CouchdbRasException("Update of galasa_run design document faile", e); + throw new CouchdbException("Update of galasa_run design document faile", e); } } } @@ -298,14 +300,14 @@ private boolean checkViewString(JsonObject view, String field, String value) { } private void checkVersion(String version, int minVersion, int minRelease, int minModification) - throws CouchdbRasException { + throws CouchdbException { String minVRM = minVersion + "." + minRelease + "." + minModification; Pattern vrm = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)$"); Matcher m = vrm.matcher(version); if (!m.find()) { - throw new CouchdbRasException("Invalid CouchDB version " + version); + throw new CouchdbException("Invalid CouchDB version " + version); } int actualVersion = 0; @@ -317,7 +319,7 @@ private void checkVersion(String version, int minVersion, int minRelease, int mi actualRelease = Integer.parseInt(m.group(2)); actualModification = Integer.parseInt(m.group(3)); } catch (NumberFormatException e) { - throw new CouchdbRasException("Unable to determine CouchDB version " + version, e); + throw new CouchdbException("Unable to determine CouchDB version " + version, e); } if (actualVersion > minVersion) { @@ -325,7 +327,7 @@ private void checkVersion(String version, int minVersion, int minRelease, int mi } if (actualVersion < minVersion) { - throw new CouchdbRasException("CouchDB version " + version + " is below minimum " + minVRM); + throw new CouchdbException("CouchDB version " + version + " is below minimum " + minVRM); } if (actualRelease > minRelease) { @@ -333,7 +335,7 @@ private void checkVersion(String version, int minVersion, int minRelease, int mi } if (actualRelease < minRelease) { - throw new CouchdbRasException("CouchDB version " + version + " is below minimum " + minVRM); + throw new CouchdbException("CouchDB version " + version + " is below minimum " + minVRM); } if (actualModification > minModification) { @@ -341,7 +343,7 @@ private void checkVersion(String version, int minVersion, int minRelease, int mi } if (actualModification < minModification) { - throw new CouchdbRasException("CouchDB version " + version + " is below minimum " + minVRM); + throw new CouchdbException("CouchDB version " + version + " is below minimum " + minVRM); } return; @@ -350,7 +352,7 @@ private void checkVersion(String version, int minVersion, int minRelease, int mi - private void checkIndex(CloseableHttpClient httpClient, URI rasUri , int attempts, String dbName, String field) throws CouchdbRasException { + private void checkIndex(CloseableHttpClient httpClient, URI rasUri , int attempts, String dbName, String field) throws CouchdbException { HttpGet httpGet = requestFactory.getHttpGetRequest(rasUri + "/galasa_run/_index"); String idxJson = null; @@ -359,15 +361,15 @@ private void checkIndex(CloseableHttpClient httpClient, URI rasUri , int attempt idxJson = EntityUtils.toString(response.getEntity()); if (statusLine.getStatusCode() != HttpStatus.SC_OK && statusLine.getStatusCode() != HttpStatus.SC_NOT_FOUND) { - throw new CouchdbRasException("Validation failed of database indexes - " + statusLine.toString()); + throw new CouchdbException("Validation failed of database indexes - " + statusLine.toString()); } if (statusLine.getStatusCode() == HttpStatus.SC_NOT_FOUND) { idxJson = "{}"; } - } catch (CouchdbRasException e) { + } catch (CouchdbException e) { throw e; } catch (Exception e) { - throw new CouchdbRasException("Validation failed", e); + throw new CouchdbException("Validation failed", e); } JsonObject idx = gson.fromJson(idxJson, JsonObject.class); @@ -424,7 +426,7 @@ private void checkIndex(CloseableHttpClient httpClient, URI rasUri , int attempt // Someone possibly updated attempts++; if (attempts > 10) { - throw new CouchdbRasException( + throw new CouchdbException( "Update of galasa_run index failed on CouchDB server due to conflicts, attempted 10 times"); } Thread.sleep(1000 + new Random().nextInt(3000)); @@ -433,14 +435,14 @@ private void checkIndex(CloseableHttpClient httpClient, URI rasUri , int attempt } if (statusLine.getStatusCode() != HttpStatus.SC_OK) { - throw new CouchdbRasException( + throw new CouchdbException( "Update of galasa_run index failed on CouchDB server - " + statusLine.toString()); } - } catch (CouchdbRasException e) { + } catch (CouchdbException e) { throw e; } catch (Exception e) { - throw new CouchdbRasException("Update of galasa_run index faile", e); + throw new CouchdbException("Update of galasa_run index faile", e); } } diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImplTest.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImplTest.java index 56733b3a..8a492ed3 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImplTest.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImplTest.java @@ -16,6 +16,8 @@ import dev.galasa.framework.spi.utils.GalasaGson; import dev.galasa.extensions.common.couchdb.pojos.Welcome; import dev.galasa.extensions.common.impl.HttpRequestFactoryImpl; +import dev.galasa.extensions.common.couchdb.CouchdbException; +import dev.galasa.extensions.common.couchdb.CouchdbValidator; import dev.galasa.extensions.common.api.HttpRequestFactory; import dev.galasa.extensions.mocks.*; import dev.galasa.ras.couchdb.internal.mocks.CouchdbTestFixtures; @@ -276,7 +278,7 @@ public void TestRasStoreCreateBlowsUpIfCouchDBDoesntReturnWelcomeString() throws // Then.. assertThat(thrown).isNotNull(); - assertThat(thrown).as("exception caught is of type "+thrown.getClass().toString()).isInstanceOf(CouchdbRasException.class); + assertThat(thrown).as("exception caught is of type "+thrown.getClass().toString()).isInstanceOf(CouchdbException.class); } @Test diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/mocks/MockCouchdbValidator.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/mocks/MockCouchdbValidator.java index af9a1eba..0b3629a5 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/mocks/MockCouchdbValidator.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/mocks/MockCouchdbValidator.java @@ -10,13 +10,13 @@ import org.apache.http.impl.client.CloseableHttpClient; import dev.galasa.extensions.common.api.HttpRequestFactory; -import dev.galasa.ras.couchdb.internal.CouchdbRasException; -import dev.galasa.ras.couchdb.internal.CouchdbValidator; +import dev.galasa.extensions.common.couchdb.CouchdbException; +import dev.galasa.extensions.common.couchdb.CouchdbValidator; public class MockCouchdbValidator implements CouchdbValidator { @Override - public void checkCouchdbDatabaseIsValid(URI rasUri, CloseableHttpClient httpClient, HttpRequestFactory requestFactory) throws CouchdbRasException { + public void checkCouchdbDatabaseIsValid(URI rasUri, CloseableHttpClient httpClient, HttpRequestFactory requestFactory) throws CouchdbException { // Do nothing. } From 5768aa43790dae8f89fc0f6a5af717bdaabf24c5 Mon Sep 17 00:00:00 2001 From: Aashir Siddiqui Date: Tue, 3 Sep 2024 10:11:17 +0100 Subject: [PATCH 02/13] Removed @Override annotation Signed-off-by: Aashir Siddiqui --- .../java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java | 1 - 1 file changed, 1 deletion(-) diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java index 2d6657f1..9e8ce9d4 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java @@ -83,7 +83,6 @@ public List getTokens() throws AuthStoreException { return tokens; } - @Override public List getTokensByLoginId(String loginId) throws AuthStoreException { logger.info("Retrieving tokens from CouchDB"); List tokenDocuments = new ArrayList<>(); From 768dc626e0dc92280f454c92769766ce64463cdd Mon Sep 17 00:00:00 2001 From: Aashir Siddiqui Date: Wed, 4 Sep 2024 09:44:24 +0100 Subject: [PATCH 03/13] Made java beans for couchdb Signed-off-by: Aashir Siddiqui --- .../internal/CouchdbAuthStoreValidator.java | 87 +++++++++++-------- .../internal/beans/TokenDBLoginView.java | 10 +++ .../couchdb/internal/beans/TokenDBViews.java | 13 +++ .../beans/TokensDBNameViewDesign.java | 12 +++ .../TestCouchdbAuthStoreValidator.java | 15 +--- 5 files changed, 90 insertions(+), 47 deletions(-) create mode 100644 galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokenDBLoginView.java create mode 100644 galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokenDBViews.java create mode 100644 galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokensDBNameViewDesign.java diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java index ab0eaa55..58442e96 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java @@ -50,29 +50,10 @@ public void checkCouchdbDatabaseIsValid(URI couchdbUri, CloseableHttpClient http } public void checkTokensDesignDocument( CloseableHttpClient httpClient , URI couchdbUri , int attempts) throws CouchdbException { - HttpRequestFactory requestFactory = super.getRequestFactory(); - HttpGet httpGet = requestFactory.getHttpGetRequest(couchdbUri + "/galasa_tokens/_design/docs"); - - String docJson = null; - try (CloseableHttpResponse response = httpClient.execute(httpGet)) { - - StatusLine statusLine = response.getStatusLine(); - - docJson = EntityUtils.toString(response.getEntity()); - if (statusLine.getStatusCode() != HttpStatus.SC_OK - && statusLine.getStatusCode() != HttpStatus.SC_NOT_FOUND) { - throw new CouchdbException( - "Validation failed of database galasa_tokens design document - " + statusLine.toString()); - } - if (statusLine.getStatusCode() == HttpStatus.SC_NOT_FOUND) { - docJson = "{}"; - } - } catch (CouchdbException e) { - throw e; - } catch (Exception e) { - throw new CouchdbException("Validation failed", e); - } - + + //Get the design document from couchdb + String docJson = getTokenDesignDocument(httpClient, couchdbUri, attempts); + boolean updated = false; JsonObject doc = gson.fromJson(docJson, JsonObject.class); @@ -96,12 +77,47 @@ public void checkTokensDesignDocument( CloseableHttpClient httpClient , URI couc views.add("loginId-view", requestors); } - if (checkView(requestors, "function (doc) { if (doc.owner && doc.owner.loginId) {emit(doc.owner.loginId, doc); } }", "javascript")) { + if (isViewUpdated(requestors, "function (doc) { if (doc.owner && doc.owner.loginId) {emit(doc.owner.loginId, doc); } }", "javascript")) { updated = true; } if (updated) { - logger.info("Updating the galasa_tokens design document"); + updateTokenDesignDocument(httpClient, couchdbUri, attempts, doc, rev); + } + } + + private String getTokenDesignDocument(CloseableHttpClient httpClient , URI couchdbUri , int attempts) throws CouchdbException{ + HttpRequestFactory requestFactory = super.getRequestFactory(); + HttpGet httpGet = requestFactory.getHttpGetRequest(couchdbUri + "/galasa_tokens/_design/docs"); + + String docJson = null; + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { + + StatusLine statusLine = response.getStatusLine(); + + docJson = EntityUtils.toString(response.getEntity()); + if (statusLine.getStatusCode() != HttpStatus.SC_OK + && statusLine.getStatusCode() != HttpStatus.SC_NOT_FOUND) { + throw new CouchdbException( + "Validation failed of database galasa_tokens design document - " + statusLine.toString()); + } + if (statusLine.getStatusCode() == HttpStatus.SC_NOT_FOUND) { + docJson = "{}"; + } + + return docJson; + + } catch (CouchdbException e) { + throw e; + } catch (Exception e) { + throw new CouchdbException("Validation failed", e); + } + } + + private void updateTokenDesignDocument(CloseableHttpClient httpClient , URI couchdbUri , int attempts, JsonObject doc, String rev) throws CouchdbException{ + HttpRequestFactory requestFactory = super.getRequestFactory(); + + logger.info("Updating the galasa_tokens design document"); HttpEntity entity = new StringEntity(gson.toJson(doc), ContentType.APPLICATION_JSON); @@ -127,38 +143,41 @@ public void checkTokensDesignDocument( CloseableHttpClient httpClient , URI couc return; } + EntityUtils.consumeQuietly(response.getEntity()); if (statusCode != HttpStatus.SC_CREATED) { - EntityUtils.consumeQuietly(response.getEntity()); + throw new CouchdbException( "Update of galasa_tokens design document failed on CouchDB server - " + statusLine.toString()); } - - EntityUtils.consumeQuietly(response.getEntity()); + } catch (CouchdbException e) { throw e; } catch (Exception e) { throw new CouchdbException("Update of galasa_tokens design document failed", e); } - } } - private boolean checkView(JsonObject view, String targetMap, String targetReduce) { + + /** + * @returns a boolean flag indicating if a couchdb view was updated. + */ + private boolean isViewUpdated(JsonObject view, String targetMap, String targetReduce) { boolean updated = false; - if (checkViewString(view, "map", targetMap)) { + if (isViewStringPresent(view, "map", targetMap)) { updated = true; } - if (checkViewString(view, "reduce", targetReduce)) { + if (isViewStringPresent(view, "reduce", targetReduce)) { updated = true; } - if (checkViewString(view, "language", "javascript")) { + if (isViewStringPresent(view, "language", "javascript")) { updated = true; } return updated; } - private boolean checkViewString(JsonObject view, String field, String value) { + private boolean isViewStringPresent(JsonObject view, String field, String value) { JsonElement element = view.get(field); if (element == null) { diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokenDBLoginView.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokenDBLoginView.java new file mode 100644 index 00000000..95c92afe --- /dev/null +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokenDBLoginView.java @@ -0,0 +1,10 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.auth.couchdb.internal.beans; + +public class TokenDBLoginView { + public String map; +} \ No newline at end of file diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokenDBViews.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokenDBViews.java new file mode 100644 index 00000000..d7af7ffb --- /dev/null +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokenDBViews.java @@ -0,0 +1,13 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.auth.couchdb.internal.beans; + +import com.google.gson.annotations.SerializedName; + +public class TokenDBViews { + @SerializedName("loginId-view") + public TokenDBLoginView loginIdView; +} diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokensDBNameViewDesign.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokensDBNameViewDesign.java new file mode 100644 index 00000000..158367eb --- /dev/null +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokensDBNameViewDesign.java @@ -0,0 +1,12 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.auth.couchdb.internal.beans; + +public class TokensDBNameViewDesign { + public TokenDBViews views; + public String language; +} + \ No newline at end of file diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java index 86c7f0d6..453fe5f1 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java @@ -17,10 +17,9 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.junit.Test; -import com.google.gson.annotations.SerializedName; - import dev.galasa.auth.couchdb.internal.CouchdbAuthStore; import dev.galasa.auth.couchdb.internal.CouchdbAuthStoreValidator; +import dev.galasa.auth.couchdb.internal.beans.*; import dev.galasa.extensions.common.couchdb.CouchdbBaseValidator; import dev.galasa.extensions.common.couchdb.CouchdbException; import dev.galasa.extensions.common.couchdb.pojos.PutPostResponse; @@ -118,17 +117,7 @@ public void validateRequest(HttpHost host, HttpRequest request) throws RuntimeEx // }, // "language": "javascript" // } - public static class TokensDBNameViewDesign { - TokenDBViews views; - String language; - } - public static class TokenDBViews { - @SerializedName("loginId-view") - TokenDBLoginView loginIdView; - } - public static class TokenDBLoginView { - String map; - } + @Test public void testCheckCouchdbDatabaseIsValidWithValidDatabaseIsOK() throws Exception { From bc044746ab65b552effeb2c8d43b96c9d05f0253 Mon Sep 17 00:00:00 2001 From: Aashir Siddiqui Date: Wed, 4 Sep 2024 15:01:15 +0100 Subject: [PATCH 04/13] Handling couch db version class added Signed-off-by: Aashir Siddiqui --- .../internal/CouchdbAuthStoreValidator.java | 212 +++++++++--------- ...ryableCouchdbUpdateOperationProcessor.java | 64 ++++++ .../beans/TokensDBNameViewDesign.java | 2 + .../CouchdbClashingUpdateException.java | 40 ++++ .../ras/couchdb/internal/CouchDbVersion.java | 112 +++++++++ .../couchdb/internal/CouchDbVersionTest.java | 92 ++++++++ 6 files changed, 415 insertions(+), 107 deletions(-) create mode 100644 galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java create mode 100644 galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbClashingUpdateException.java create mode 100644 galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java create mode 100644 galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java index 58442e96..e024d707 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java @@ -6,7 +6,6 @@ package dev.galasa.auth.couchdb.internal; import java.net.URI; -import java.util.Random; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -21,72 +20,117 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; import dev.galasa.extensions.common.couchdb.CouchdbBaseValidator; +import dev.galasa.extensions.common.couchdb.CouchdbClashingUpdateException; import dev.galasa.extensions.common.couchdb.CouchdbException; +import dev.galasa.auth.couchdb.internal.beans.*; import dev.galasa.extensions.common.api.HttpRequestFactory; import dev.galasa.framework.spi.utils.GalasaGson; +import dev.galasa.framework.spi.utils.ITimeService; +import dev.galasa.framework.spi.utils.SystemTimeService; -public class CouchdbAuthStoreValidator extends CouchdbBaseValidator{ +public class CouchdbAuthStoreValidator extends CouchdbBaseValidator { private final Log logger = LogFactory.getLog(getClass()); - private final GalasaGson gson = new GalasaGson(); - - + private final GalasaGson gson = new GalasaGson(); + + + public static final String DB_TABLE_TOKENS_DESIGN = "function (doc) { if (doc.owner && doc.owner.loginId) {emit(doc.owner.loginId, doc); } }"; @Override - public void checkCouchdbDatabaseIsValid(URI couchdbUri, CloseableHttpClient httpClient, HttpRequestFactory httpRequestFactory) throws CouchdbException { + public void checkCouchdbDatabaseIsValid( + URI couchdbUri, + CloseableHttpClient httpClient, + HttpRequestFactory httpRequestFactory + ) throws CouchdbException { + ITimeService timeService = new SystemTimeService(); + checkCouchdbDatabaseIsValid(couchdbUri,httpClient,httpRequestFactory,timeService); + } + protected void checkCouchdbDatabaseIsValid( + URI couchdbUri, + CloseableHttpClient httpClient, + HttpRequestFactory httpRequestFactory, + ITimeService timeService + ) throws CouchdbException { + + RetryableCouchdbUpdateOperationProcessor retryProcessor = new RetryableCouchdbUpdateOperationProcessor(timeService); + + retryProcessor.retryCouchDbUpdateOperation( + ()->{ tryToCheckAndUpdateCouchDBTokenView(couchdbUri, httpClient, httpRequestFactory); + }); + + logger.debug("Auth Store CouchDB at " + couchdbUri.toString() + " validated"); + } + + private void tryToCheckAndUpdateCouchDBTokenView(URI couchdbUri, CloseableHttpClient httpClient, + HttpRequestFactory httpRequestFactory) throws CouchdbException { // Perform the base CouchDB checks super.checkCouchdbDatabaseIsValid(couchdbUri, httpClient, httpRequestFactory); validateDatabasePresent(couchdbUri, CouchdbAuthStore.TOKENS_DATABASE_NAME); checkTokensDesignDocument(httpClient, couchdbUri, 1); + } - logger.debug("Auth Store CouchDB at " + couchdbUri.toString() + " validated"); + public void checkTokensDesignDocument(CloseableHttpClient httpClient, URI couchdbUri, int attempts) + throws CouchdbException { - } - - public void checkTokensDesignDocument( CloseableHttpClient httpClient , URI couchdbUri , int attempts) throws CouchdbException { - - //Get the design document from couchdb + // Get the design document from couchdb String docJson = getTokenDesignDocument(httpClient, couchdbUri, attempts); - - boolean updated = false; - JsonObject doc = gson.fromJson(docJson, JsonObject.class); - doc.remove("_id"); - String rev = null; - if (doc.has("_rev")) { - rev = doc.get("_rev").getAsString(); + TokensDBNameViewDesign tableDesign = parseTokenDesignFromJson(docJson); + + boolean isDesignUpdated = transformDesignToDesired(tableDesign); + + if (isDesignUpdated) { + updateTokenDesignDocument(httpClient, couchdbUri, attempts, tableDesign); } + } + + private TokensDBNameViewDesign parseTokenDesignFromJson(String docJson) throws CouchdbException { + TokensDBNameViewDesign tableDesign; + try { + tableDesign = gson.fromJson(docJson, TokensDBNameViewDesign.class); + } catch (JsonSyntaxException ex) { + throw new CouchdbException("", ex); // TODO: Throw a couchdb exception up. + } + + if (tableDesign == null) { + tableDesign = new TokensDBNameViewDesign(); + } + return tableDesign; + } - JsonObject views = doc.getAsJsonObject("views"); - if (views == null) { - updated = true; - views = new JsonObject(); - doc.add("views", views); + private boolean transformDesignToDesired(TokensDBNameViewDesign tableDesign) { + boolean isUpdated = false; + + if (tableDesign.views == null) { + isUpdated = true; + tableDesign.views = new TokenDBViews(); } - JsonObject requestors = views.getAsJsonObject("loginId-view"); - if (requestors == null) { - updated = true; - requestors = new JsonObject(); - views.add("loginId-view", requestors); + if (tableDesign.views.loginIdView == null) { + isUpdated = true; + tableDesign.views.loginIdView = new TokenDBLoginView(); } - if (isViewUpdated(requestors, "function (doc) { if (doc.owner && doc.owner.loginId) {emit(doc.owner.loginId, doc); } }", "javascript")) { - updated = true; + if (tableDesign.views.loginIdView.map == null + || !DB_TABLE_TOKENS_DESIGN.equals(tableDesign.views.loginIdView.map)) { + isUpdated = true; + tableDesign.views.loginIdView.map = DB_TABLE_TOKENS_DESIGN; } - if (updated) { - updateTokenDesignDocument(httpClient, couchdbUri, attempts, doc, rev); + if (tableDesign.language == null || !tableDesign.language.equals("javascript")) { + isUpdated = true; + tableDesign.language = "javascript"; } + + return isUpdated; } - private String getTokenDesignDocument(CloseableHttpClient httpClient , URI couchdbUri , int attempts) throws CouchdbException{ + private String getTokenDesignDocument(CloseableHttpClient httpClient, URI couchdbUri, int attempts) + throws CouchdbException { HttpRequestFactory requestFactory = super.getRequestFactory(); HttpGet httpGet = requestFactory.getHttpGetRequest(couchdbUri + "/galasa_tokens/_design/docs"); @@ -114,87 +158,41 @@ private String getTokenDesignDocument(CloseableHttpClient httpClient , URI couch } } - private void updateTokenDesignDocument(CloseableHttpClient httpClient , URI couchdbUri , int attempts, JsonObject doc, String rev) throws CouchdbException{ + private void updateTokenDesignDocument(CloseableHttpClient httpClient, URI couchdbUri, int attempts, + TokensDBNameViewDesign tokenViewDesign) throws CouchdbException { HttpRequestFactory requestFactory = super.getRequestFactory(); logger.info("Updating the galasa_tokens design document"); - HttpEntity entity = new StringEntity(gson.toJson(doc), ContentType.APPLICATION_JSON); - - HttpPut httpPut = requestFactory.getHttpPutRequest(couchdbUri + "/galasa_tokens/_design/docs"); - httpPut.setEntity(entity); + HttpEntity entity = new StringEntity(gson.toJson(tokenViewDesign), ContentType.APPLICATION_JSON); - if (rev != null) { - httpPut.addHeader("ETaq", "\"" + rev + "\""); - } + HttpPut httpPut = requestFactory.getHttpPutRequest(couchdbUri + "/galasa_tokens/_design/docs"); + httpPut.setEntity(entity); - try (CloseableHttpResponse response = httpClient.execute(httpPut)) { - StatusLine statusLine = response.getStatusLine(); - int statusCode = statusLine.getStatusCode(); - if (statusCode == HttpStatus.SC_CONFLICT) { - // Someone possibly updated - attempts++; - if (attempts > 10) { - throw new CouchdbException( - "Update of galasa_token design document failed on CouchDB server due to conflicts, attempted 10 times"); - } - Thread.sleep(1000 + new Random().nextInt(3000)); - checkTokensDesignDocument(httpClient, couchdbUri, attempts); - return; - } - - EntityUtils.consumeQuietly(response.getEntity()); - if (statusCode != HttpStatus.SC_CREATED) { - - throw new CouchdbException( - "Update of galasa_tokens design document failed on CouchDB server - " + statusLine.toString()); - } - - } catch (CouchdbException e) { - throw e; - } catch (Exception e) { - throw new CouchdbException("Update of galasa_tokens design document failed", e); - } - } - - /** - * @returns a boolean flag indicating if a couchdb view was updated. - */ - private boolean isViewUpdated(JsonObject view, String targetMap, String targetReduce) { - - boolean updated = false; - - if (isViewStringPresent(view, "map", targetMap)) { - updated = true; - } - if (isViewStringPresent(view, "reduce", targetReduce)) { - updated = true; - } - if (isViewStringPresent(view, "language", "javascript")) { - updated = true; + if (tokenViewDesign._rev != null) { + httpPut.addHeader("ETaq", "\"" + tokenViewDesign._rev + "\""); } - return updated; - } - - private boolean isViewStringPresent(JsonObject view, String field, String value) { + try (CloseableHttpResponse response = httpClient.execute(httpPut)) { + StatusLine statusLine = response.getStatusLine(); + int statusCode = statusLine.getStatusCode(); + if (statusCode == HttpStatus.SC_CONFLICT) { + // Someone possibly updated the document while we were thinking about it. + // It was probably another instance of this exact code. + throw new CouchdbClashingUpdateException(""); // TODO: Add proper message. + } - JsonElement element = view.get(field); - if (element == null) { - view.addProperty(field, value); - return true; - } + EntityUtils.consumeQuietly(response.getEntity()); + if (statusCode != HttpStatus.SC_CREATED) { - if (!element.isJsonPrimitive() || !((JsonPrimitive) element).isString()) { - view.addProperty(field, value); - return true; - } + throw new CouchdbException( + "Update of galasa_tokens design document failed on CouchDB server - " + statusLine.toString()); + } - String actualValue = element.getAsString(); - if (!value.equals(actualValue)) { - view.addProperty(field, value); - return true; + } catch (CouchdbException e) { + throw e; + } catch (Exception e) { + throw new CouchdbException("Update of galasa_tokens design document failed", e); } - return false; } } diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java new file mode 100644 index 00000000..ec6f3657 --- /dev/null +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java @@ -0,0 +1,64 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.auth.couchdb.internal; + +import java.util.Random; + +import dev.galasa.extensions.common.couchdb.CouchdbClashingUpdateException; +import dev.galasa.extensions.common.couchdb.CouchdbException; +import dev.galasa.framework.spi.utils.ITimeService; + +/** + * Allows a lambda function to be used, and that function will be retried a number of times before giving up. + */ +public class RetryableCouchdbUpdateOperationProcessor { + + public int MAX_ATTEMPTS_TO_GO_BEFORE_GIVE_UP = 10; + private ITimeService timeService ; + + public interface RetryableCouchdbUpdateOperation { + public void tryToUpdateCouchDb() throws CouchdbException; + } + + + public RetryableCouchdbUpdateOperationProcessor(ITimeService timeService) { + this.timeService = timeService; + } + + public void retryCouchDbUpdateOperation(RetryableCouchdbUpdateOperation retryableOperation) throws CouchdbException{ + int attemptsToGoBeforeGiveUp = MAX_ATTEMPTS_TO_GO_BEFORE_GIVE_UP; + boolean isDone = false; + + while (!isDone) { + + try { + retryableOperation.tryToUpdateCouchDb(); + + isDone = true; + } catch (CouchdbClashingUpdateException updateClashedEx) { + + waitForBackoffDelay(); + + // TODO: Log it + attemptsToGoBeforeGiveUp -= 1; + if (attemptsToGoBeforeGiveUp == 0) { + throw new CouchdbException("tried x times and could not update doc...", updateClashedEx); + } + } + } + } + + private void waitForBackoffDelay() { + Long delayMilliSecs = 1000L + new Random().nextInt(3000); + + try { + timeService.wait(delayMilliSecs); + } catch(InterruptedException ex ) { + // TODO: Log it and continue. + } + + } +} \ No newline at end of file diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokensDBNameViewDesign.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokensDBNameViewDesign.java index 158367eb..0308a7e4 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokensDBNameViewDesign.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokensDBNameViewDesign.java @@ -6,6 +6,8 @@ package dev.galasa.auth.couchdb.internal.beans; public class TokensDBNameViewDesign { + public String _rev; + public String _id; public TokenDBViews views; public String language; } diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbClashingUpdateException.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbClashingUpdateException.java new file mode 100644 index 00000000..b1001a6c --- /dev/null +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbClashingUpdateException.java @@ -0,0 +1,40 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.extensions.common.couchdb; + +public class CouchdbClashingUpdateException extends CouchdbException { + + private static final long serialVersionUID = 1L; + + /** + * {@inheritDoc} + */ + public CouchdbClashingUpdateException() { + super(); + } + + /** + * {@inheritDoc} + */ + public CouchdbClashingUpdateException(String message) { + super(message); + } + + /** + * {@inheritDoc} + */ + public CouchdbClashingUpdateException(Throwable cause) { + super(cause); + } + + /** + * {@inheritDoc} + */ + public CouchdbClashingUpdateException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java new file mode 100644 index 00000000..d9f3a9e4 --- /dev/null +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java @@ -0,0 +1,112 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.ras.couchdb.internal; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; + +import java.net.URI; +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import dev.galasa.extensions.common.couchdb.pojos.Welcome; +import dev.galasa.extensions.common.api.HttpRequestFactory; +import dev.galasa.extensions.common.couchdb.CouchdbException; +import dev.galasa.extensions.common.couchdb.CouchdbValidator; +import dev.galasa.framework.spi.utils.GalasaGson; + + +public class CouchDbVersion { + + private int version ; + private int release; + private int modification; + + public CouchDbVersion(int version, int release, int modification) { + this.version = version ; + this.release = release ; + this.modification = modification; + } + + public CouchDbVersion(String dotSeparatedVersion ) throws CouchdbException { + Pattern vrm = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)$"); + Matcher m = vrm.matcher(dotSeparatedVersion); + + if (!m.find()) { + throw new CouchdbException("Invalid CouchDB version " + dotSeparatedVersion); // TODO: Make this error msg better. + } + + try { + this.version = Integer.parseInt(m.group(1)); + this.release = Integer.parseInt(m.group(2)); + this.modification = Integer.parseInt(m.group(3)); + } catch (NumberFormatException e) { + throw new CouchdbException("Unable to determine CouchDB version " + dotSeparatedVersion, e); + } + } + + public int getVersion() { + return this.version ; + } + + public int getRelease() { + return this.release; + } + + public int getModification() { + return this.modification; + } + + @Override + public boolean equals(Object other) { + boolean isSame = false ; + + if( other != null ) { + if( other instanceof CouchDbVersion) { + CouchDbVersion otherVersion = (CouchDbVersion)other; + if(otherVersion.version == this.version){ + if (otherVersion.release == this.release) { + if( otherVersion.modification == this.modification) { + isSame = true; + } + } + } + } + } + + return isSame; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 31 * hash + (int) this.version; + hash = 31 * hash + (int) this.release; + hash = 31 * hash + (int) this.modification; + return hash; + } + + +} \ No newline at end of file diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java new file mode 100644 index 00000000..15fd67bf --- /dev/null +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java @@ -0,0 +1,92 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.ras.couchdb.internal; + +import java.util.*; +import static org.assertj.core.api.Assertions.*; + +import org.apache.http.*; +import org.apache.http.client.methods.HttpPost; +import org.junit.*; +import org.junit.rules.TestName; + +import dev.galasa.framework.spi.utils.GalasaGson; +import dev.galasa.extensions.common.couchdb.pojos.Welcome; +import dev.galasa.extensions.common.impl.HttpRequestFactoryImpl; +import dev.galasa.extensions.common.couchdb.CouchdbException; +import dev.galasa.extensions.common.couchdb.CouchdbValidator; +import dev.galasa.extensions.common.api.HttpRequestFactory; +import dev.galasa.extensions.mocks.*; +import dev.galasa.ras.couchdb.internal.mocks.CouchdbTestFixtures; +import dev.galasa.ras.couchdb.internal.mocks.CouchdbTestFixtures.BaseHttpInteraction;; + + +public class CouchDbVersionTest { + + @Test + public void testCanCreateAVersion() { + CouchDbVersion version = new CouchDbVersion(1,2,3); + assertThat(version.getVersion()).isEqualTo(1); + assertThat(version.getRelease()).isEqualTo(2); + assertThat(version.getModification()).isEqualTo(3); + } + + @Test + public void testCanCreateAVersionFromAString() throws Exception { + CouchDbVersion version = new CouchDbVersion("1.2.3"); + assertThat(version.getVersion()).isEqualTo(1); + assertThat(version.getRelease()).isEqualTo(2); + assertThat(version.getModification()).isEqualTo(3); + } + + @Test + public void testInvalidVersionStringThrowsParsingError() throws Exception { + CouchdbException ex = catchThrowableOfType( ()->{ new CouchDbVersion("1.2..3"); }, + CouchdbException.class ); + assertThat(ex).hasMessageContaining("1.2..3"); + // TODO: Assert that more of the message is in here. + } + + @Test + public void testInvalidLeadingDotVersionStringThrowsParsingError() throws Exception { + CouchdbException ex = catchThrowableOfType( ()->{ new CouchDbVersion(".1.2.3"); }, + CouchdbException.class ); + assertThat(ex).hasMessageContaining(".1.2.3"); + // TODO: Assert that more of the message is in here. + } + + + @Test + public void testCanICompareTheSameThingIsEqualToItself() throws Exception { + CouchDbVersion version = new CouchDbVersion("1.2.3"); + assertThat(version.equals(version)).as("Could not compare the version with itself.").isTrue(); + } + + @Test + public void testHashCodeOfTwoSameVersionsIsTheSame() throws Exception { + CouchDbVersion version1 = new CouchDbVersion("1.2.3"); + CouchDbVersion version2 = new CouchDbVersion("1.2.3"); + assertThat(version1.hashCode()).isEqualTo(version2.hashCode()); + } + + @Test + public void testTwoSameVersionsDifferentObjectsAreTheSame() throws Exception { + CouchDbVersion version1 = new CouchDbVersion("1.2.3"); + CouchDbVersion version2 = new CouchDbVersion("1.2.3"); + assertThat(version1).isEqualTo(version2); + } + + @Test + public void testAddingTwoSameVersionsDifferentObjectsIntoASetResultInOneObjectInTheSet() throws Exception { + CouchDbVersion version1 = new CouchDbVersion("1.2.3"); + CouchDbVersion version2 = new CouchDbVersion("1.2.3"); + Set mySet = new HashSet<>(); + mySet.add(version1); + mySet.add(version2); + + assertThat(mySet).hasSize(1); + } +} \ No newline at end of file From 12411ef1124ad94a0a8436f8383575dcf18038d2 Mon Sep 17 00:00:00 2001 From: Aashir Siddiqui Date: Wed, 4 Sep 2024 15:37:18 +0100 Subject: [PATCH 05/13] Added comparable Signed-off-by: Aashir Siddiqui --- .../ras/couchdb/internal/CouchDbVersion.java | 23 +++++- .../couchdb/internal/CouchDbVersionTest.java | 71 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java index d9f3a9e4..5ba6e2e2 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java @@ -38,7 +38,7 @@ import dev.galasa.framework.spi.utils.GalasaGson; -public class CouchDbVersion { +public class CouchDbVersion implements Comparable { private int version ; private int release; @@ -108,5 +108,26 @@ public int hashCode() { return hash; } + @Override + public int compareTo(CouchDbVersion other) { + int result ; + if (this.version > other.version) { + result = +1; + } else if (this.version < other.version) { + result = -1; + } else if (this.release > other.release) { + result = +1; + } else if (this.release < other.release) { + result = -1; + } else if (this.modification > other.modification) { + result = +1; + } else if (this.modification < other.modification) { + result = -1; + } else { + result = 0; + } + return result; + } + } \ No newline at end of file diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java index 15fd67bf..49a2c1d6 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java @@ -89,4 +89,75 @@ public void testAddingTwoSameVersionsDifferentObjectsIntoASetResultInOneObjectIn assertThat(mySet).hasSize(1); } + + @Test + public void testTwoDifferentVersionsAreNotComparable() throws Exception { + CouchDbVersion version1 = new CouchDbVersion("1.2.3"); + CouchDbVersion version2 = new CouchDbVersion("1.2.4"); + + int result = version1.compareTo(version2); + + assertThat(result).isNotEqualTo(0); + assertThat(result).isEqualTo(-1); + } + + + public static class CouchDbVersionComparator implements Comparator { + @Override + public int compare(CouchDbVersion v1, CouchDbVersion v2) { + return v1.compareTo(v2); + } + } + + @Test + public void testToVersionsGetsSortedInCorrectOrderSmallestFirst() throws Exception { + CouchDbVersion smallerVersion = new CouchDbVersion("1.2.3"); + CouchDbVersion biggerVersion = new CouchDbVersion("1.2.4"); + List versions = new ArrayList<>(); + versions.addAll( List.of(smallerVersion, biggerVersion )); + + CouchDbVersionComparator comparator = new CouchDbVersionComparator(); + Collections.sort(versions, comparator); + + assertThat(versions.get(0)).isEqualTo(smallerVersion); + assertThat(versions.get(1)).isEqualTo(biggerVersion); + } + + @Test + public void testToVersionsGetsSortedInCorrectOrderSmallestSecond() throws Exception { + CouchDbVersion smallerVersion = new CouchDbVersion("1.2.3"); + CouchDbVersion biggerVersion = new CouchDbVersion("1.2.4"); + List versions = new ArrayList<>(); + versions.addAll( List.of(biggerVersion, smallerVersion )); + + CouchDbVersionComparator comparator = new CouchDbVersionComparator(); + Collections.sort(versions, comparator); + + assertThat(versions.get(0)).isEqualTo(smallerVersion); + assertThat(versions.get(1)).isEqualTo(biggerVersion); + } + + @Test + public void testCanCompareVersionsDirectly() throws Exception{ + CouchDbVersion smallerVersion = new CouchDbVersion("1.2.3"); + CouchDbVersion biggerVersion = new CouchDbVersion("1.2.4"); + assertThat( smallerVersion).isLessThan(biggerVersion); + assertThat( biggerVersion).isGreaterThan(smallerVersion); + } + + @Test + public void testCanCompareVersionsWhereVersionDiffers() throws Exception{ + CouchDbVersion smallerVersion = new CouchDbVersion("1.2.3"); + CouchDbVersion biggerVersion = new CouchDbVersion("2.2.3"); + assertThat( smallerVersion).isLessThan(biggerVersion); + assertThat( biggerVersion).isGreaterThan(smallerVersion); + } + + @Test + public void testCanCompareVersionsWhereReleaseDiffers() throws Exception{ + CouchDbVersion smallerVersion = new CouchDbVersion("1.2.3"); + CouchDbVersion biggerVersion = new CouchDbVersion("1.3.3"); + assertThat( smallerVersion).isLessThan(biggerVersion); + assertThat( biggerVersion).isGreaterThan(smallerVersion); + } } \ No newline at end of file From ac3028b5fa536e950b67b97d14220a33e3756445 Mon Sep 17 00:00:00 2001 From: Aashir Siddiqui Date: Wed, 4 Sep 2024 15:49:23 +0100 Subject: [PATCH 06/13] Used couchdb version inside couchdb validator Signed-off-by: Aashir Siddiqui --- .../ras/couchdb/internal/CouchDbVersion.java | 39 +++++--------- .../internal/CouchdbValidatorImpl.java | 51 +++---------------- .../couchdb/internal/CouchDbVersionTest.java | 28 +++++----- 3 files changed, 35 insertions(+), 83 deletions(-) diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java index 5ba6e2e2..09934def 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java @@ -5,37 +5,10 @@ */ package dev.galasa.ras.couchdb.internal; -import org.apache.http.HttpEntity; -import org.apache.http.HttpStatus; -import org.apache.http.StatusLine; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.util.EntityUtils; - -import java.net.URI; -import java.util.Random; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.http.client.methods.HttpHead; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; - -import dev.galasa.extensions.common.couchdb.pojos.Welcome; -import dev.galasa.extensions.common.api.HttpRequestFactory; import dev.galasa.extensions.common.couchdb.CouchdbException; -import dev.galasa.extensions.common.couchdb.CouchdbValidator; -import dev.galasa.framework.spi.utils.GalasaGson; public class CouchDbVersion implements Comparable { @@ -129,5 +102,17 @@ public int compareTo(CouchDbVersion other) { return result; } + @Override + public String toString() { + StringBuilder buffer = new StringBuilder(); + + buffer.append(this.version); + buffer.append('.'); + buffer.append(this.release); + buffer.append('.'); + buffer.append(this.modification); + + return buffer.toString(); + } } \ No newline at end of file diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java index 36d1f460..443d8b4e 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java @@ -43,6 +43,8 @@ public class CouchdbValidatorImpl implements CouchdbValidator { private final Log logger = LogFactory.getLog(getClass()); private HttpRequestFactory requestFactory; + private static final CouchDbVersion minCouchDbVersion = new CouchDbVersion(3,3,3); + public void checkCouchdbDatabaseIsValid( URI rasUri, CloseableHttpClient httpClient , HttpRequestFactory httpRequestFactory) throws CouchdbException { this.requestFactory = httpRequestFactory; HttpGet httpGet = requestFactory.getHttpGetRequest(rasUri.toString()); @@ -61,7 +63,7 @@ public void checkCouchdbDatabaseIsValid( URI rasUri, CloseableHttpClient httpCli throw new CouchdbRasException("Validation failed to CouchDB server - invalid json response"); } - checkVersion(welcome.version, 3, 3, 3); + checkVersion(welcome.version, minCouchDbVersion); checkDatabasePresent(httpClient, rasUri, 1, "galasa_run"); checkDatabasePresent(httpClient, rasUri, 1, "galasa_log"); checkDatabasePresent(httpClient, rasUri, 1, "galasa_artifacts"); @@ -299,55 +301,16 @@ private boolean checkViewString(JsonObject view, String field, String value) { return false; } - private void checkVersion(String version, int minVersion, int minRelease, int minModification) + private void checkVersion(String version, CouchDbVersion minVersion) throws CouchdbException { - String minVRM = minVersion + "." + minRelease + "." + minModification; - Pattern vrm = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)$"); - Matcher m = vrm.matcher(version); + CouchDbVersion actualCouchVersion = new CouchDbVersion(version); - if (!m.find()) { - throw new CouchdbException("Invalid CouchDB version " + version); - } - - int actualVersion = 0; - int actualRelease = 0; - int actualModification = 0; - - try { - actualVersion = Integer.parseInt(m.group(1)); - actualRelease = Integer.parseInt(m.group(2)); - actualModification = Integer.parseInt(m.group(3)); - } catch (NumberFormatException e) { - throw new CouchdbException("Unable to determine CouchDB version " + version, e); - } - - if (actualVersion > minVersion) { - return; - } - - if (actualVersion < minVersion) { - throw new CouchdbException("CouchDB version " + version + " is below minimum " + minVRM); - } - - if (actualRelease > minRelease) { - return; - } - - if (actualRelease < minRelease) { - throw new CouchdbException("CouchDB version " + version + " is below minimum " + minVRM); - } - - if (actualModification > minModification) { - return; - } - - if (actualModification < minModification) { - throw new CouchdbException("CouchDB version " + version + " is below minimum " + minVRM); + if ( actualCouchVersion.compareTo(minVersion) < 0) { + throw new CouchdbException("CouchDB version " + version + " is below minimum " + minVersion); } return; - } diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java index 49a2c1d6..f3edc39f 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java @@ -8,20 +8,8 @@ import java.util.*; import static org.assertj.core.api.Assertions.*; -import org.apache.http.*; -import org.apache.http.client.methods.HttpPost; import org.junit.*; -import org.junit.rules.TestName; - -import dev.galasa.framework.spi.utils.GalasaGson; -import dev.galasa.extensions.common.couchdb.pojos.Welcome; -import dev.galasa.extensions.common.impl.HttpRequestFactoryImpl; import dev.galasa.extensions.common.couchdb.CouchdbException; -import dev.galasa.extensions.common.couchdb.CouchdbValidator; -import dev.galasa.extensions.common.api.HttpRequestFactory; -import dev.galasa.extensions.mocks.*; -import dev.galasa.ras.couchdb.internal.mocks.CouchdbTestFixtures; -import dev.galasa.ras.couchdb.internal.mocks.CouchdbTestFixtures.BaseHttpInteraction;; public class CouchDbVersionTest { @@ -160,4 +148,20 @@ public void testCanCompareVersionsWhereReleaseDiffers() throws Exception{ assertThat( smallerVersion).isLessThan(biggerVersion); assertThat( biggerVersion).isGreaterThan(smallerVersion); } + + @Test + public void testToStringOfVersionIsCorrect() throws Exception { + CouchDbVersion version = new CouchDbVersion("1.2.3"); + assertThat(version.toString()).isEqualTo("1.2.3"); + + CouchDbVersion version2 = new CouchDbVersion(1,2,3); + assertThat(version2.toString()).isEqualTo("1.2.3"); + } + + @Test + public void testToStringMethodGetsCalledImplicitly() throws Exception { + CouchDbVersion version = new CouchDbVersion(1,2,3); + String s = "something "+version; + assertThat(s).isEqualTo("something 1.2.3"); + } } \ No newline at end of file From f457ae6401313655059c3f66d0a865c31b2b2f35 Mon Sep 17 00:00:00 2001 From: Aashir Siddiqui Date: Thu, 5 Sep 2024 10:52:49 +0100 Subject: [PATCH 07/13] Added error and logging messages Signed-off-by: Aashir Siddiqui --- .../internal/CouchdbAuthStoreValidator.java | 12 ++- .../galasa/auth/couchdb/internal/Errors.java | 83 +++++++++++++++++++ ...ryableCouchdbUpdateOperationProcessor.java | 15 ++-- .../TestCouchdbAuthStoreValidator.java | 27 +++--- .../common/couchdb}/CouchDbVersion.java | 12 ++- .../common/couchdb/CouchdbBaseValidator.java | 34 ++------ .../common/couchdb}/CouchDbVersionTest.java | 18 ++-- .../internal/CouchdbValidatorImpl.java | 3 +- 8 files changed, 142 insertions(+), 62 deletions(-) create mode 100644 galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/Errors.java rename galasa-extensions-parent/{dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal => dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb}/CouchDbVersion.java (85%) rename galasa-extensions-parent/{dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal => dev.galasa.extensions.common/src/test/java/dev/galasa/extensions/common/couchdb}/CouchDbVersionTest.java (92%) diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java index e024d707..23db9d92 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java @@ -31,6 +31,8 @@ import dev.galasa.framework.spi.utils.ITimeService; import dev.galasa.framework.spi.utils.SystemTimeService; +import static dev.galasa.auth.couchdb.internal.Errors.*; + public class CouchdbAuthStoreValidator extends CouchdbBaseValidator { private final Log logger = LogFactory.getLog(getClass()); @@ -45,7 +47,13 @@ public void checkCouchdbDatabaseIsValid( CloseableHttpClient httpClient, HttpRequestFactory httpRequestFactory ) throws CouchdbException { + ITimeService timeService = new SystemTimeService(); + + // Do some generic checks against the auth DB. + // super.checkCouchdbDatabaseIsValid(couchdbUri,httpClient,httpRequestFactory); TODO: Why can't we do this ? Should we ? + + // Check specifics which make the auth db different from other couchdb databases. checkCouchdbDatabaseIsValid(couchdbUri,httpClient,httpRequestFactory,timeService); } @@ -93,7 +101,7 @@ private TokensDBNameViewDesign parseTokenDesignFromJson(String docJson) throws C try { tableDesign = gson.fromJson(docJson, TokensDBNameViewDesign.class); } catch (JsonSyntaxException ex) { - throw new CouchdbException("", ex); // TODO: Throw a couchdb exception up. + throw new CouchdbException(ERROR_FAILED_TO_PARSE_COUCHDB_DESIGN_DOC.getMessage(ex.getMessage()), ex); } if (tableDesign == null) { @@ -179,7 +187,7 @@ private void updateTokenDesignDocument(CloseableHttpClient httpClient, URI couch if (statusCode == HttpStatus.SC_CONFLICT) { // Someone possibly updated the document while we were thinking about it. // It was probably another instance of this exact code. - throw new CouchdbClashingUpdateException(""); // TODO: Add proper message. + throw new CouchdbClashingUpdateException(ERROR_FAILED_TO_UPDATE_COUCHDB_DESING_DOC_CONFLICT.toString()); } EntityUtils.consumeQuietly(response.getEntity()); diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/Errors.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/Errors.java new file mode 100644 index 00000000..d3fc6b6f --- /dev/null +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/Errors.java @@ -0,0 +1,83 @@ +package dev.galasa.auth.couchdb.internal; + + +import java.text.MessageFormat; + +public enum Errors { + + ERROR_FAILED_TO_PARSE_COUCHDB_DESIGN_DOC (7500, + "GAL7500E: The Galasa auth extension could not check that couchdb has the correct definition for the dababase in which access tokens are stored."+ + "The design of the database could not be parsed. Please report this error to your Galasa system administrator. Detailed cause of this problem: {}"), + ERROR_FAILED_TO_UPDATE_COUCHDB_DESING_DOC_CONFLICT (7501, + "GAL7501E: The Galasa auth extension could not upgrade the definition of the couchdb database in which access tokens are stored."+ + "The design of the database could not be updated due to clashing updates. Please report this error to your Galasa system administrator."), + ; + + private String template; + private int expectedParameterCount; + private Errors(int ordinal, String template ) { + this.template = template ; + this.expectedParameterCount = this.template.split("[{]").length-1; + } + + public String getMessage() { + String msg ; + int actualParameterCount = 0; + + if (actualParameterCount!= this.expectedParameterCount) { + msg = getWrongNumberOfParametersErrorMessage(actualParameterCount,expectedParameterCount); + } else { + msg = this.template; + } + + return msg; + } + + public String getMessage(Object o1) { + + String msg ; + int actualParameterCount = 1; + + if (actualParameterCount!= this.expectedParameterCount) { + msg = getWrongNumberOfParametersErrorMessage(actualParameterCount,expectedParameterCount); + } else { + msg = MessageFormat.format(this.template,o1); + } + + return msg; + } + + public String getMessage(Object o1, Object o2) { + + String msg ; + int actualParameterCount = 2; + + if (actualParameterCount!= this.expectedParameterCount) { + msg = getWrongNumberOfParametersErrorMessage(actualParameterCount,expectedParameterCount); + } else { + msg = MessageFormat.format(this.template,o1,o2); + } + + return msg; + } + + public String getMessage(Object o1, Object o2, Object o3) { + + String msg ; + int actualParameterCount = 3; + + if (actualParameterCount!= this.expectedParameterCount) { + msg = getWrongNumberOfParametersErrorMessage(actualParameterCount,expectedParameterCount); + } else { + msg = MessageFormat.format(this.template,o1,o2,o3); + } + + return msg; + } + + private String getWrongNumberOfParametersErrorMessage(int actualParameterCount,int expectedParameterCount) { + String template = "Failed to render message template. Not the expected number of parameters. Got ''{0}''. Expected ''{1}''"; + String msg = MessageFormat.format(template,actualParameterCount, this.expectedParameterCount); + return msg ; + } +} diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java index ec6f3657..98d12277 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java @@ -11,6 +11,8 @@ import dev.galasa.extensions.common.couchdb.CouchdbException; import dev.galasa.framework.spi.utils.ITimeService; +import org.apache.commons.logging.Log; + /** * Allows a lambda function to be used, and that function will be retried a number of times before giving up. */ @@ -18,7 +20,7 @@ public class RetryableCouchdbUpdateOperationProcessor { public int MAX_ATTEMPTS_TO_GO_BEFORE_GIVE_UP = 10; private ITimeService timeService ; - + private Log logger; public interface RetryableCouchdbUpdateOperation { public void tryToUpdateCouchDb() throws CouchdbException; } @@ -40,12 +42,15 @@ public void retryCouchDbUpdateOperation(RetryableCouchdbUpdateOperation retryabl isDone = true; } catch (CouchdbClashingUpdateException updateClashedEx) { + logger.info("Clashing update detected. Backing off for a short time to avoid another clash immediately. "); + waitForBackoffDelay(); - // TODO: Log it attemptsToGoBeforeGiveUp -= 1; if (attemptsToGoBeforeGiveUp == 0) { - throw new CouchdbException("tried x times and could not update doc...", updateClashedEx); + throw new CouchdbException("Failed after " + Integer.toString(MAX_ATTEMPTS_TO_GO_BEFORE_GIVE_UP) + " attempts to update the design document in CouchDB due to conflicts.", updateClashedEx); + } else { + logger.info("Failed to update CouchDB design document, retrying..."); } } } @@ -55,10 +60,10 @@ private void waitForBackoffDelay() { Long delayMilliSecs = 1000L + new Random().nextInt(3000); try { + logger.info("Waiting "+delayMilliSecs+" during a back-off delay. starting now."); timeService.wait(delayMilliSecs); } catch(InterruptedException ex ) { - // TODO: Log it and continue. + logger.info("Interrupted from waiting during a back-off delay. Ignoring this, but cutting our wait short."); } - } } \ No newline at end of file diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java index 453fe5f1..4d03695b 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java @@ -20,7 +20,6 @@ import dev.galasa.auth.couchdb.internal.CouchdbAuthStore; import dev.galasa.auth.couchdb.internal.CouchdbAuthStoreValidator; import dev.galasa.auth.couchdb.internal.beans.*; -import dev.galasa.extensions.common.couchdb.CouchdbBaseValidator; import dev.galasa.extensions.common.couchdb.CouchdbException; import dev.galasa.extensions.common.couchdb.pojos.PutPostResponse; import dev.galasa.extensions.common.couchdb.pojos.Welcome; @@ -29,6 +28,8 @@ import dev.galasa.extensions.mocks.HttpInteraction; import dev.galasa.extensions.mocks.MockCloseableHttpClient; +import dev.galasa.extensions.common.couchdb.CouchDbVersion; + public class TestCouchdbAuthStoreValidator { class CreateDatabaseInteraction extends BaseHttpInteraction { @@ -128,7 +129,7 @@ public void testCheckCouchdbDatabaseIsValidWithValidDatabaseIsOK() throws Except Welcome welcomeMessage = new Welcome(); welcomeMessage.couchdb = "Welcome"; - welcomeMessage.version = CouchdbBaseValidator.COUCHDB_MIN_VERSION; + welcomeMessage.version = CouchDbVersion.COUCHDB_MIN_VERSION.toString(); List interactions = new ArrayList<>(); interactions.add(new GetCouchdbWelcomeInteraction(couchdbUriStr, welcomeMessage)); @@ -173,7 +174,7 @@ public void testCheckCouchdbDatabaseIsValidWithFailingDatabaseCreationReturnsErr Welcome welcomeMessage = new Welcome(); welcomeMessage.couchdb = "Welcome"; - welcomeMessage.version = CouchdbBaseValidator.COUCHDB_MIN_VERSION; + welcomeMessage.version = CouchDbVersion.COUCHDB_MIN_VERSION.toString(); String tokensDatabaseName = CouchdbAuthStore.TOKENS_DATABASE_NAME; List interactions = new ArrayList<>(); @@ -202,7 +203,7 @@ public void testCheckCouchdbDatabaseIsValidWithSuccessfulDatabaseCreationIsOK() Welcome welcomeMessage = new Welcome(); welcomeMessage.couchdb = "Welcome"; - welcomeMessage.version = CouchdbBaseValidator.COUCHDB_MIN_VERSION; + welcomeMessage.version = CouchDbVersion.COUCHDB_MIN_VERSION.toString(); String tokensDatabaseName = CouchdbAuthStore.TOKENS_DATABASE_NAME; List interactions = new ArrayList<>(); @@ -239,7 +240,7 @@ public void testCheckCouchdbDatabaseIsValidWithNewerCouchdbVersionIsOK() throws URI couchdbUri = URI.create(couchdbUriStr); CouchdbAuthStoreValidator validator = new CouchdbAuthStoreValidator(); - String[] versionParts = CouchdbBaseValidator.COUCHDB_MIN_VERSION.split("\\."); + String[] versionParts = CouchDbVersion.COUCHDB_MIN_VERSION.toString().split("\\."); int majorVersion = Integer.parseInt(versionParts[0]); int minorVersion = Integer.parseInt(versionParts[1]); int patchVersion = Integer.parseInt(versionParts[2]); @@ -274,7 +275,7 @@ public void testCheckCouchdbDatabaseIsValidWithInvalidWelcomeMessageThrowsError( Welcome welcomeMessage = new Welcome(); welcomeMessage.couchdb = "not welcome"; - welcomeMessage.version = CouchdbBaseValidator.COUCHDB_MIN_VERSION; + welcomeMessage.version = CouchDbVersion.COUCHDB_MIN_VERSION.toString(); List interactions = new ArrayList<>(); interactions.add(new GetCouchdbWelcomeInteraction(couchdbUriStr, welcomeMessage)); @@ -316,7 +317,7 @@ public void testCheckCouchdbDatabaseIsValidWithMajorVersionMismatchThrowsError() // Then... assertThat(thrown).isNotNull(); - assertThat(thrown.getMessage()).contains("GAL6005E", "Expected version '" + CouchdbBaseValidator.COUCHDB_MIN_VERSION + "' or above"); + assertThat(thrown.getMessage()).contains("GAL6005E", "Expected version '" + CouchDbVersion.COUCHDB_MIN_VERSION.toString() + "' or above"); } @Test @@ -353,7 +354,7 @@ public void testCheckCouchdbDatabaseIsValidWithMinorVersionMismatchThrowsError() URI couchdbUri = URI.create(couchdbUriStr); CouchdbAuthStoreValidator validator = new CouchdbAuthStoreValidator(); - String majorVersion = CouchdbBaseValidator.COUCHDB_MIN_VERSION.split("\\.")[0]; + String majorVersion = CouchDbVersion.COUCHDB_MIN_VERSION.toString().split("\\.")[0]; Welcome welcomeMessage = new Welcome(); welcomeMessage.couchdb = "Welcome"; @@ -372,7 +373,7 @@ public void testCheckCouchdbDatabaseIsValidWithMinorVersionMismatchThrowsError() // Then... assertThat(thrown).isNotNull(); - assertThat(thrown.getMessage()).contains("GAL6005E", "Expected version '" + CouchdbBaseValidator.COUCHDB_MIN_VERSION + "' or above"); + assertThat(thrown.getMessage()).contains("GAL6005E", "Expected version '" + CouchDbVersion.COUCHDB_MIN_VERSION.toString() + "' or above"); } @Test @@ -382,7 +383,7 @@ public void testCheckCouchdbDatabaseIsValidWithPatchVersionMismatchThrowsError() URI couchdbUri = URI.create(couchdbUriStr); CouchdbAuthStoreValidator validator = new CouchdbAuthStoreValidator(); - String[] minVersionParts = CouchdbBaseValidator.COUCHDB_MIN_VERSION.split("\\."); + String[] minVersionParts = CouchDbVersion.COUCHDB_MIN_VERSION.toString().split("\\."); Welcome welcomeMessage = new Welcome(); welcomeMessage.couchdb = "Welcome"; @@ -401,7 +402,7 @@ public void testCheckCouchdbDatabaseIsValidWithPatchVersionMismatchThrowsError() // Then... assertThat(thrown).isNotNull(); - assertThat(thrown.getMessage()).contains("GAL6005E", "Expected version '" + CouchdbBaseValidator.COUCHDB_MIN_VERSION + "' or above"); + assertThat(thrown.getMessage()).contains("GAL6005E", "Expected version '" + CouchDbVersion.COUCHDB_MIN_VERSION.toString() + "' or above"); } @Test @@ -413,7 +414,7 @@ public void testCheckCouchdbDatabaseIsValidWithFailedDesignDocResponseThrowsErro Welcome welcomeMessage = new Welcome(); welcomeMessage.couchdb = "Welcome"; - welcomeMessage.version = CouchdbBaseValidator.COUCHDB_MIN_VERSION; + welcomeMessage.version = CouchDbVersion.COUCHDB_MIN_VERSION.toString(); List interactions = new ArrayList<>(); interactions.add(new GetCouchdbWelcomeInteraction(couchdbUriStr, welcomeMessage)); @@ -462,7 +463,7 @@ public void testCheckCouchdbDatabaseIsValidWithUpdateDesignDocResponseThrowsErro Welcome welcomeMessage = new Welcome(); welcomeMessage.couchdb = "Welcome"; - welcomeMessage.version = CouchdbBaseValidator.COUCHDB_MIN_VERSION; + welcomeMessage.version = CouchDbVersion.COUCHDB_MIN_VERSION.toString(); List interactions = new ArrayList<>(); interactions.add(new GetCouchdbWelcomeInteraction(couchdbUriStr, welcomeMessage)); diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchDbVersion.java similarity index 85% rename from galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java rename to galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchDbVersion.java index 09934def..7c947d44 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchDbVersion.java +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchDbVersion.java @@ -3,19 +3,22 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package dev.galasa.ras.couchdb.internal; +package dev.galasa.extensions.common.couchdb; import java.util.regex.Matcher; import java.util.regex.Pattern; -import dev.galasa.extensions.common.couchdb.CouchdbException; +import static dev.galasa.extensions.common.Errors.*; public class CouchDbVersion implements Comparable { + private int version ; private int release; private int modification; + + public static final CouchDbVersion COUCHDB_MIN_VERSION = new CouchDbVersion(3,3,3); public CouchDbVersion(int version, int release, int modification) { this.version = version ; @@ -28,7 +31,8 @@ public CouchDbVersion(String dotSeparatedVersion ) throws CouchdbException { Matcher m = vrm.matcher(dotSeparatedVersion); if (!m.find()) { - throw new CouchdbException("Invalid CouchDB version " + dotSeparatedVersion); // TODO: Make this error msg better. + String errorMessage = ERROR_INVALID_COUCHDB_VERSION_FORMAT.getMessage(dotSeparatedVersion, COUCHDB_MIN_VERSION); + throw new CouchdbException(errorMessage); } try { @@ -36,7 +40,7 @@ public CouchDbVersion(String dotSeparatedVersion ) throws CouchdbException { this.release = Integer.parseInt(m.group(2)); this.modification = Integer.parseInt(m.group(3)); } catch (NumberFormatException e) { - throw new CouchdbException("Unable to determine CouchDB version " + dotSeparatedVersion, e); + throw new CouchdbException(ERROR_INVALID_COUCHDB_VERSION_FORMAT.getMessage(dotSeparatedVersion), e); // TODO: Common error. } } diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbBaseValidator.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbBaseValidator.java index 49170f9d..146d5ce6 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbBaseValidator.java +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbBaseValidator.java @@ -20,9 +20,6 @@ import java.io.IOException; import java.net.URI; -import java.util.Arrays; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -33,8 +30,6 @@ public abstract class CouchdbBaseValidator implements CouchdbValidator { - public static final String COUCHDB_MIN_VERSION = "3.3.3"; - private final GalasaGson gson = new GalasaGson(); private final Log logger = LogFactory.getLog(getClass()); @@ -156,36 +151,17 @@ private synchronized void createDatabase(URI couchdbUri, String dbName) throws C * @throws CouchdbException if the version of CouchDB is older than the minimum required version */ private void checkVersion(String actualVersion) throws CouchdbException { - Pattern versionPattern = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)$"); - Matcher versionMatcher = versionPattern.matcher(actualVersion); - // Make sure the given version follows semantic versioning rules (i.e. major.minor.patch) - if (!versionMatcher.find()) { - String errorMessage = ERROR_INVALID_COUCHDB_VERSION_FORMAT.getMessage(actualVersion, COUCHDB_MIN_VERSION); - throw new CouchdbException(errorMessage); - } - - int[] actualVersionParts = getVersionStringAsArray(actualVersion); - int[] minVersionParts = getVersionStringAsArray(COUCHDB_MIN_VERSION); + CouchDbVersion actualCouchDbVersion = new CouchDbVersion(actualVersion); // Check if the actual version is older than the minimum version // If the actual version matches the minimum version, then this loop will continue until // all parts of the versions have been compared - for (int i = 0; i < minVersionParts.length; i++) { - if (actualVersionParts[i] < minVersionParts[i]) { - - // The minimum CouchDB version is later than the actual version, so throw an error - String errorMessage = ERROR_OUTDATED_COUCHDB_VERSION.getMessage(actualVersion, COUCHDB_MIN_VERSION); - throw new CouchdbException(errorMessage); + if ( actualCouchDbVersion.compareTo(CouchDbVersion.COUCHDB_MIN_VERSION) < 0) { - } else if (actualVersionParts[i] > minVersionParts[i]) { - // The minimum CouchDB version is older than the actual version, this is fine - break; - } + // The minimum CouchDB version is later than the actual version, so throw an error + String errorMessage = ERROR_OUTDATED_COUCHDB_VERSION.getMessage(actualVersion, CouchDbVersion.COUCHDB_MIN_VERSION); + throw new CouchdbException(errorMessage); } } - - private int[] getVersionStringAsArray(String versionStr) { - return Arrays.stream(versionStr.split("\\.")).mapToInt(Integer::parseInt).toArray(); - } } diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/test/java/dev/galasa/extensions/common/couchdb/CouchDbVersionTest.java similarity index 92% rename from galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java rename to galasa-extensions-parent/dev.galasa.extensions.common/src/test/java/dev/galasa/extensions/common/couchdb/CouchDbVersionTest.java index f3edc39f..287e9384 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchDbVersionTest.java +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/test/java/dev/galasa/extensions/common/couchdb/CouchDbVersionTest.java @@ -3,14 +3,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package dev.galasa.ras.couchdb.internal; +package dev.galasa.extensions.common.couchdb; import java.util.*; import static org.assertj.core.api.Assertions.*; import org.junit.*; -import dev.galasa.extensions.common.couchdb.CouchdbException; - public class CouchDbVersionTest { @@ -32,17 +30,23 @@ public void testCanCreateAVersionFromAString() throws Exception { @Test public void testInvalidVersionStringThrowsParsingError() throws Exception { - CouchdbException ex = catchThrowableOfType( ()->{ new CouchDbVersion("1.2..3"); }, + + String invalidVersion = "1.2..3"; + + CouchdbException ex = catchThrowableOfType( ()->{ new CouchDbVersion(invalidVersion); }, CouchdbException.class ); - assertThat(ex).hasMessageContaining("1.2..3"); + assertThat(ex).hasMessageContaining("GAL6010E: Invalid CouchDB server version format detected. The CouchDB version '" + invalidVersion + "'"); // TODO: Assert that more of the message is in here. } @Test public void testInvalidLeadingDotVersionStringThrowsParsingError() throws Exception { - CouchdbException ex = catchThrowableOfType( ()->{ new CouchDbVersion(".1.2.3"); }, + + String invalidVersion = ".1.2.3"; + + CouchdbException ex = catchThrowableOfType( ()->{ new CouchDbVersion(invalidVersion); }, CouchdbException.class ); - assertThat(ex).hasMessageContaining(".1.2.3"); + assertThat(ex).hasMessageContaining("GAL6010E: Invalid CouchDB server version format detected. The CouchDB version '" + invalidVersion + "'"); // TODO: Assert that more of the message is in here. } diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java index 443d8b4e..697c2655 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java @@ -15,8 +15,6 @@ import java.net.URI; import java.util.Random; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -33,6 +31,7 @@ import dev.galasa.extensions.common.couchdb.pojos.Welcome; import dev.galasa.extensions.common.api.HttpRequestFactory; +import dev.galasa.extensions.common.couchdb.CouchDbVersion; import dev.galasa.extensions.common.couchdb.CouchdbException; import dev.galasa.extensions.common.couchdb.CouchdbValidator; import dev.galasa.framework.spi.utils.GalasaGson; From f4e6717a0dee5309ea15113df65bf442adb1c353 Mon Sep 17 00:00:00 2001 From: Aashir Siddiqui Date: Thu, 5 Sep 2024 11:20:15 +0100 Subject: [PATCH 08/13] Added galasa copyright text Signed-off-by: Aashir Siddiqui --- .../main/java/dev/galasa/auth/couchdb/internal/Errors.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/Errors.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/Errors.java index d3fc6b6f..13634e94 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/Errors.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/Errors.java @@ -1,3 +1,9 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ + package dev.galasa.auth.couchdb.internal; From bf8ad4211f676f7f8699f7fac4c7c7e9e84276e6 Mon Sep 17 00:00:00 2001 From: Aashir Siddiqui Date: Thu, 5 Sep 2024 14:14:41 +0100 Subject: [PATCH 09/13] Removed redundant mock couchdb validator Signed-off-by: Aashir Siddiqui --- .../internal/CouchdbValidatorImplTest.java | 3 ++- .../internal/mocks/CouchdbTestFixtures.java | 1 + .../internal/mocks/MockCouchdbValidator.java | 23 ------------------- 3 files changed, 3 insertions(+), 24 deletions(-) delete mode 100644 galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/mocks/MockCouchdbValidator.java diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImplTest.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImplTest.java index 8a492ed3..6b09835b 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImplTest.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImplTest.java @@ -13,6 +13,7 @@ import org.junit.*; import org.junit.rules.TestName; + import dev.galasa.framework.spi.utils.GalasaGson; import dev.galasa.extensions.common.couchdb.pojos.Welcome; import dev.galasa.extensions.common.impl.HttpRequestFactoryImpl; @@ -21,7 +22,7 @@ import dev.galasa.extensions.common.api.HttpRequestFactory; import dev.galasa.extensions.mocks.*; import dev.galasa.ras.couchdb.internal.mocks.CouchdbTestFixtures; -import dev.galasa.ras.couchdb.internal.mocks.CouchdbTestFixtures.BaseHttpInteraction;; +import dev.galasa.ras.couchdb.internal.mocks.CouchdbTestFixtures.BaseHttpInteraction; public class CouchdbValidatorImplTest { diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/mocks/CouchdbTestFixtures.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/mocks/CouchdbTestFixtures.java index ff5fff8f..8c8a72d1 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/mocks/CouchdbTestFixtures.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/mocks/CouchdbTestFixtures.java @@ -35,6 +35,7 @@ import dev.galasa.framework.spi.IRun; import dev.galasa.framework.spi.utils.GalasaGson; import dev.galasa.ras.couchdb.internal.CouchdbRasStore; +import dev.galasa.extensions.mocks.couchdb.MockCouchdbValidator; public class CouchdbTestFixtures { diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/mocks/MockCouchdbValidator.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/mocks/MockCouchdbValidator.java deleted file mode 100644 index 0b3629a5..00000000 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/mocks/MockCouchdbValidator.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright contributors to the Galasa project - * - * SPDX-License-Identifier: EPL-2.0 - */ -package dev.galasa.ras.couchdb.internal.mocks; - -import java.net.URI; - -import org.apache.http.impl.client.CloseableHttpClient; - -import dev.galasa.extensions.common.api.HttpRequestFactory; -import dev.galasa.extensions.common.couchdb.CouchdbException; -import dev.galasa.extensions.common.couchdb.CouchdbValidator; - -public class MockCouchdbValidator implements CouchdbValidator { - - @Override - public void checkCouchdbDatabaseIsValid(URI rasUri, CloseableHttpClient httpClient, HttpRequestFactory requestFactory) throws CouchdbException { - // Do nothing. - } - -} From 10578a571cf1986b6769bf52acabefc980f9d3c0 Mon Sep 17 00:00:00 2001 From: Aashir Siddiqui Date: Thu, 5 Sep 2024 14:26:33 +0100 Subject: [PATCH 10/13] Time Service passed to couchdb database validators Signed-off-by: Aashir Siddiqui --- .../couchdb/internal/CouchdbAuthStore.java | 2 +- .../internal/CouchdbAuthStoreValidator.java | 22 ++------- .../TestCouchdbAuthStoreValidator.java | 46 ++++++++++++++----- .../common/couchdb/CouchdbBaseValidator.java | 3 +- .../common/couchdb/CouchdbValidator.java | 3 +- .../mocks/couchdb/MockCouchdbValidator.java | 3 +- .../ras/couchdb/internal/CouchdbRasStore.java | 6 ++- .../internal/CouchdbValidatorImpl.java | 3 +- .../internal/CouchdbValidatorImplTest.java | 14 ++++-- 9 files changed, 61 insertions(+), 41 deletions(-) diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java index 9e8ce9d4..95093c62 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java @@ -57,7 +57,7 @@ public CouchdbAuthStore( this.logger = logFactory.getLog(getClass()); this.timeService = timeService; - validator.checkCouchdbDatabaseIsValid(this.storeUri, this.httpClient, this.httpRequestFactory); + validator.checkCouchdbDatabaseIsValid(this.storeUri, this.httpClient, this.httpRequestFactory, timeService); } @Override diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java index 23db9d92..746fab82 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java @@ -29,7 +29,6 @@ import dev.galasa.extensions.common.api.HttpRequestFactory; import dev.galasa.framework.spi.utils.GalasaGson; import dev.galasa.framework.spi.utils.ITimeService; -import dev.galasa.framework.spi.utils.SystemTimeService; import static dev.galasa.auth.couchdb.internal.Errors.*; @@ -43,27 +42,15 @@ public class CouchdbAuthStoreValidator extends CouchdbBaseValidator { @Override public void checkCouchdbDatabaseIsValid( - URI couchdbUri, - CloseableHttpClient httpClient, - HttpRequestFactory httpRequestFactory - ) throws CouchdbException { - - ITimeService timeService = new SystemTimeService(); - - // Do some generic checks against the auth DB. - // super.checkCouchdbDatabaseIsValid(couchdbUri,httpClient,httpRequestFactory); TODO: Why can't we do this ? Should we ? - - // Check specifics which make the auth db different from other couchdb databases. - checkCouchdbDatabaseIsValid(couchdbUri,httpClient,httpRequestFactory,timeService); - } - - protected void checkCouchdbDatabaseIsValid( URI couchdbUri, CloseableHttpClient httpClient, HttpRequestFactory httpRequestFactory, ITimeService timeService ) throws CouchdbException { + // Perform the base CouchDB checks + super.checkCouchdbDatabaseIsValid(couchdbUri, httpClient, httpRequestFactory, timeService); + RetryableCouchdbUpdateOperationProcessor retryProcessor = new RetryableCouchdbUpdateOperationProcessor(timeService); retryProcessor.retryCouchDbUpdateOperation( @@ -75,8 +62,7 @@ protected void checkCouchdbDatabaseIsValid( private void tryToCheckAndUpdateCouchDBTokenView(URI couchdbUri, CloseableHttpClient httpClient, HttpRequestFactory httpRequestFactory) throws CouchdbException { - // Perform the base CouchDB checks - super.checkCouchdbDatabaseIsValid(couchdbUri, httpClient, httpRequestFactory); + validateDatabasePresent(couchdbUri, CouchdbAuthStore.TOKENS_DATABASE_NAME); checkTokensDesignDocument(httpClient, couchdbUri, 1); } diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java index 4d03695b..33b1967e 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java @@ -10,6 +10,7 @@ import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.time.Instant; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; @@ -27,7 +28,7 @@ import dev.galasa.extensions.mocks.BaseHttpInteraction; import dev.galasa.extensions.mocks.HttpInteraction; import dev.galasa.extensions.mocks.MockCloseableHttpClient; - +import dev.galasa.extensions.mocks.MockTimeService; import dev.galasa.extensions.common.couchdb.CouchDbVersion; public class TestCouchdbAuthStoreValidator { @@ -152,6 +153,7 @@ public void testCheckCouchdbDatabaseIsValidWithValidDatabaseIsOK() throws Except views.loginIdView = view; TokensDBNameViewDesign designDocToPassBack = new TokensDBNameViewDesign(); designDocToPassBack.language = "javascript"; + String tokensDesignDocUrl = couchdbUriStr + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME + "/_design/docs"; @@ -160,9 +162,10 @@ public void testCheckCouchdbDatabaseIsValidWithValidDatabaseIsOK() throws Except interactions.add(new UpdateTokensDatabaseDesignInteraction(tokensDesignDocUrl, "", HttpStatus.SC_CREATED)); CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); // When... - validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()); + validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl(), mockTimeService); } @Test @@ -182,10 +185,11 @@ public void testCheckCouchdbDatabaseIsValidWithFailingDatabaseCreationReturnsErr interactions.add(new GetTokensDatabaseInteraction(couchdbUriStr + "/" + tokensDatabaseName, HttpStatus.SC_NOT_FOUND)); interactions.add(new CreateDatabaseInteraction(couchdbUriStr + "/" + tokensDatabaseName, HttpStatus.SC_INTERNAL_SERVER_ERROR)); CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); // When... CouchdbException thrown = catchThrowableOfType( - () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()), + () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl(), mockTimeService), CouchdbException.class ); @@ -222,12 +226,13 @@ public void testCheckCouchdbDatabaseIsValidWithSuccessfulDatabaseCreationIsOK() String tokensDesignDocUrl = couchdbUriStr + "/" + tokensDatabaseName + "/_design/docs"; interactions.add(new GetTokensDatabaseDesignInteraction(tokensDesignDocUrl, designDocToPassBack)); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); interactions.add(new UpdateTokensDatabaseDesignInteraction(tokensDesignDocUrl, "",HttpStatus.SC_CREATED)); CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); // When... - validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()); + validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl(),mockTimeService); // Then... // The validation should have passed, so no errors should have been thrown @@ -255,9 +260,11 @@ public void testCheckCouchdbDatabaseIsValidWithNewerCouchdbVersionIsOK() throws interactions.add(new GetTokensDatabaseInteraction(couchdbUriStr + "/" + tokensDatabaseName, HttpStatus.SC_INTERNAL_SERVER_ERROR)); CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); + // When... CouchdbException thrown = catchThrowableOfType( - () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()), + () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl(), mockTimeService), CouchdbException.class ); @@ -282,9 +289,11 @@ public void testCheckCouchdbDatabaseIsValidWithInvalidWelcomeMessageThrowsError( interactions.add(new GetTokensDatabaseInteraction(couchdbUriStr + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME, HttpStatus.SC_OK)); CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); + // When... CouchdbException thrown = catchThrowableOfType( - () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()), + () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl(), mockTimeService), CouchdbException.class ); @@ -309,9 +318,11 @@ public void testCheckCouchdbDatabaseIsValidWithMajorVersionMismatchThrowsError() interactions.add(new GetTokensDatabaseInteraction(couchdbUriStr + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME, HttpStatus.SC_OK)); CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); + // When... CouchdbException thrown = catchThrowableOfType( - () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()), + () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl(),mockTimeService), CouchdbException.class ); @@ -336,9 +347,11 @@ public void testCheckCouchdbDatabaseIsValidWithInvalidVersionThrowsError() throw interactions.add(new GetTokensDatabaseInteraction(couchdbUriStr + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME, HttpStatus.SC_OK)); CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); + // When... CouchdbException thrown = catchThrowableOfType( - () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()), + () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl(),mockTimeService), CouchdbException.class ); @@ -365,9 +378,12 @@ public void testCheckCouchdbDatabaseIsValidWithMinorVersionMismatchThrowsError() interactions.add(new GetTokensDatabaseInteraction(couchdbUriStr + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME, HttpStatus.SC_OK)); CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + + MockTimeService mockTimeService = new MockTimeService(Instant.now()); + // When... CouchdbException thrown = catchThrowableOfType( - () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()), + () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl(),mockTimeService), CouchdbException.class ); @@ -394,9 +410,11 @@ public void testCheckCouchdbDatabaseIsValidWithPatchVersionMismatchThrowsError() interactions.add(new GetTokensDatabaseInteraction(couchdbUriStr + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME, HttpStatus.SC_OK)); CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); + // When... CouchdbException thrown = catchThrowableOfType( - () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()), + () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl(), mockTimeService), CouchdbException.class ); @@ -443,9 +461,11 @@ public void testCheckCouchdbDatabaseIsValidWithFailedDesignDocResponseThrowsErro interactions.add(new GetTokensDatabaseDesignInteraction(tokensDesignDocUrl, designDocToPassBack, HttpStatus.SC_INTERNAL_SERVER_ERROR)); CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); + // When... CouchdbException thrown = catchThrowableOfType( - () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()), + () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl(), mockTimeService), CouchdbException.class ); @@ -492,10 +512,12 @@ public void testCheckCouchdbDatabaseIsValidWithUpdateDesignDocResponseThrowsErro interactions.add(new GetTokensDatabaseDesignInteraction(tokensDesignDocUrl, designDocToPassBack, HttpStatus.SC_OK)); interactions.add(new UpdateTokensDatabaseDesignInteraction(tokensDesignDocUrl, "", HttpStatus.SC_INTERNAL_SERVER_ERROR)); CloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + + MockTimeService mockTimeService = new MockTimeService(Instant.now()); // When... CouchdbException thrown = catchThrowableOfType( - () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl()), + () -> validator.checkCouchdbDatabaseIsValid(couchdbUri, mockHttpClient, new HttpRequestFactoryImpl(), mockTimeService), CouchdbException.class ); diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbBaseValidator.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbBaseValidator.java index 146d5ce6..be61815b 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbBaseValidator.java +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbBaseValidator.java @@ -27,6 +27,7 @@ import dev.galasa.extensions.common.couchdb.pojos.Welcome; import dev.galasa.extensions.common.api.HttpRequestFactory; import dev.galasa.framework.spi.utils.GalasaGson; +import dev.galasa.framework.spi.utils.ITimeService; public abstract class CouchdbBaseValidator implements CouchdbValidator { @@ -37,7 +38,7 @@ public abstract class CouchdbBaseValidator implements CouchdbValidator { private CloseableHttpClient httpClient; @Override - public void checkCouchdbDatabaseIsValid(URI couchdbUri, CloseableHttpClient httpClient, HttpRequestFactory httpRequestFactory) throws CouchdbException { + public void checkCouchdbDatabaseIsValid(URI couchdbUri, CloseableHttpClient httpClient, HttpRequestFactory httpRequestFactory, ITimeService timeService) throws CouchdbException { this.requestFactory = httpRequestFactory; this.httpClient = httpClient; diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbValidator.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbValidator.java index db93223b..c42b707e 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbValidator.java +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbValidator.java @@ -8,9 +8,10 @@ import org.apache.http.impl.client.CloseableHttpClient; import dev.galasa.extensions.common.api.HttpRequestFactory; +import dev.galasa.framework.spi.utils.ITimeService; import java.net.URI; public interface CouchdbValidator { - public void checkCouchdbDatabaseIsValid(URI couchdbUri, CloseableHttpClient httpClient, HttpRequestFactory requestFactory) throws CouchdbException; + public void checkCouchdbDatabaseIsValid(URI couchdbUri, CloseableHttpClient httpClient, HttpRequestFactory requestFactory, ITimeService timeService ) throws CouchdbException; } diff --git a/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/couchdb/MockCouchdbValidator.java b/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/couchdb/MockCouchdbValidator.java index d853c128..097446e1 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/couchdb/MockCouchdbValidator.java +++ b/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/couchdb/MockCouchdbValidator.java @@ -11,6 +11,7 @@ import dev.galasa.extensions.common.couchdb.CouchdbException; import dev.galasa.extensions.common.couchdb.CouchdbValidator; +import dev.galasa.framework.spi.utils.ITimeService; import dev.galasa.extensions.common.api.HttpRequestFactory; public class MockCouchdbValidator implements CouchdbValidator { @@ -22,7 +23,7 @@ public void setThrowException(boolean throwException) { } @Override - public void checkCouchdbDatabaseIsValid(URI couchdbUri, CloseableHttpClient httpClient, HttpRequestFactory requestFactory) throws CouchdbException { + public void checkCouchdbDatabaseIsValid(URI couchdbUri, CloseableHttpClient httpClient, HttpRequestFactory requestFactory, ITimeService timeService) throws CouchdbException { if (throwException) { throw new CouchdbException("simulating a validation failure!"); } diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbRasStore.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbRasStore.java index dde0c98f..d1745c1a 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbRasStore.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbRasStore.java @@ -32,6 +32,8 @@ import dev.galasa.framework.spi.ras.ResultArchiveStoreFileStore; import dev.galasa.framework.spi.teststructure.TestStructure; import dev.galasa.framework.spi.utils.GalasaGson; +import dev.galasa.framework.spi.utils.ITimeService; +import dev.galasa.framework.spi.utils.SystemTimeService; import dev.galasa.extensions.common.api.HttpClientFactory; import dev.galasa.extensions.common.api.LogFactory; import dev.galasa.extensions.common.couchdb.CouchdbException; @@ -77,6 +79,7 @@ public class CouchdbRasStore extends CouchdbStore implements IResultArchiveStore private String artifactDocumentRev; private TestStructure lastTestStructure; + private ITimeService timeService ; private LogFactory logFactory; @@ -99,9 +102,10 @@ public CouchdbRasStore(IFramework framework, URI rasUri, HttpClientFactory httpF this.logFactory = logFactory; this.logger = logFactory.getLog(getClass()); this.framework = framework; + this.timeService = new SystemTimeService(); // *** Validate the connection to the server and it's version - validator.checkCouchdbDatabaseIsValid(this.storeUri,this.httpClient, this.httpRequestFactory); + validator.checkCouchdbDatabaseIsValid(this.storeUri,this.httpClient, this.httpRequestFactory, timeService); this.run = this.framework.getTestRun(); diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java index 697c2655..ee9afd8c 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java @@ -35,6 +35,7 @@ import dev.galasa.extensions.common.couchdb.CouchdbException; import dev.galasa.extensions.common.couchdb.CouchdbValidator; import dev.galasa.framework.spi.utils.GalasaGson; +import dev.galasa.framework.spi.utils.ITimeService; public class CouchdbValidatorImpl implements CouchdbValidator { @@ -44,7 +45,7 @@ public class CouchdbValidatorImpl implements CouchdbValidator { private static final CouchDbVersion minCouchDbVersion = new CouchDbVersion(3,3,3); - public void checkCouchdbDatabaseIsValid( URI rasUri, CloseableHttpClient httpClient , HttpRequestFactory httpRequestFactory) throws CouchdbException { + public void checkCouchdbDatabaseIsValid( URI rasUri, CloseableHttpClient httpClient , HttpRequestFactory httpRequestFactory, ITimeService timeService) throws CouchdbException { this.requestFactory = httpRequestFactory; HttpGet httpGet = requestFactory.getHttpGetRequest(rasUri.toString()); diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImplTest.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImplTest.java index 6b09835b..f964adcc 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImplTest.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/test/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImplTest.java @@ -13,7 +13,7 @@ import org.junit.*; import org.junit.rules.TestName; - +import java.time.Instant; import dev.galasa.framework.spi.utils.GalasaGson; import dev.galasa.extensions.common.couchdb.pojos.Welcome; import dev.galasa.extensions.common.impl.HttpRequestFactoryImpl; @@ -273,9 +273,10 @@ public void TestRasStoreCreateBlowsUpIfCouchDBDoesntReturnWelcomeString() throws CouchdbValidator validatorUnderTest = new CouchdbValidatorImpl(); HttpRequestFactory requestFactory = new HttpRequestFactoryImpl("Basic", "checkisvalid"); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); // When.. - Throwable thrown = catchThrowable(()-> validatorUnderTest.checkCouchdbDatabaseIsValid( CouchdbTestFixtures.rasUri , mockHttpClient, requestFactory)); + Throwable thrown = catchThrowable(()-> validatorUnderTest.checkCouchdbDatabaseIsValid( CouchdbTestFixtures.rasUri , mockHttpClient, requestFactory, mockTimeService)); // Then.. assertThat(thrown).isNotNull(); @@ -322,9 +323,10 @@ public void TestRasStoreCreatesDBIfCouchDBReturnsWelcomeString() throws Exceptio CouchdbValidator validatorUnderTest = new CouchdbValidatorImpl(); HttpRequestFactory requestFactory = new HttpRequestFactoryImpl("Basic", "checkisvalid"); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); // When.. - Throwable thrown = catchThrowable(()->validatorUnderTest.checkCouchdbDatabaseIsValid( CouchdbTestFixtures.rasUri , mockHttpClient, requestFactory)); + Throwable thrown = catchThrowable(()->validatorUnderTest.checkCouchdbDatabaseIsValid( CouchdbTestFixtures.rasUri , mockHttpClient, requestFactory, mockTimeService)); assertThat(thrown).isNull(); } @@ -381,9 +383,10 @@ public MockCloseableHttpResponse getResponse() { CouchdbValidator validatorUnderTest = new CouchdbValidatorImpl(); HttpRequestFactory requestFactory = new HttpRequestFactoryImpl("Basic", "checkisvalid"); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); // When.. - Throwable thrown = catchThrowable(()->validatorUnderTest.checkCouchdbDatabaseIsValid( CouchdbTestFixtures.rasUri , mockHttpClient, requestFactory)); + Throwable thrown = catchThrowable(()->validatorUnderTest.checkCouchdbDatabaseIsValid( CouchdbTestFixtures.rasUri , mockHttpClient, requestFactory, mockTimeService)); assertThat(thrown).isNotNull(); assertThat(thrown.getMessage()).contains("Validation failed of database galasa_run"); @@ -449,9 +452,10 @@ public MockCloseableHttpResponse getResponse() { CouchdbValidator validatorUnderTest = new CouchdbValidatorImpl(); HttpRequestFactory requestFactory = new HttpRequestFactoryImpl("Basic", "checkisvalid"); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); // When.. - Throwable thrown = catchThrowable(()->validatorUnderTest.checkCouchdbDatabaseIsValid( CouchdbTestFixtures.rasUri , mockHttpClient, requestFactory)); + Throwable thrown = catchThrowable(()->validatorUnderTest.checkCouchdbDatabaseIsValid( CouchdbTestFixtures.rasUri , mockHttpClient, requestFactory,mockTimeService)); assertThat(thrown).isNotNull(); assertThat(thrown.getMessage()).contains("Create Database galasa_run failed on CouchDB server due to conflicts, attempted 10 times"); From d2de322b3304ef9be019f45195cf6ce52b12be7c Mon Sep 17 00:00:00 2001 From: Aashir Siddiqui Date: Thu, 5 Sep 2024 15:11:04 +0100 Subject: [PATCH 11/13] Swapped thread.sleep with timeService.sleepMillis Signed-off-by: Aashir Siddiqui --- ...ryableCouchdbUpdateOperationProcessor.java | 6 +-- .../extensions/mocks/MockTimeService.java | 6 +++ .../internal/CouchdbValidatorImpl.java | 50 +++++++++++-------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java index 98d12277..b1589a20 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java @@ -44,7 +44,7 @@ public void retryCouchDbUpdateOperation(RetryableCouchdbUpdateOperation retryabl logger.info("Clashing update detected. Backing off for a short time to avoid another clash immediately. "); - waitForBackoffDelay(); + waitForBackoffDelay(timeService); attemptsToGoBeforeGiveUp -= 1; if (attemptsToGoBeforeGiveUp == 0) { @@ -56,12 +56,12 @@ public void retryCouchDbUpdateOperation(RetryableCouchdbUpdateOperation retryabl } } - private void waitForBackoffDelay() { + private void waitForBackoffDelay(ITimeService timeService) { Long delayMilliSecs = 1000L + new Random().nextInt(3000); try { logger.info("Waiting "+delayMilliSecs+" during a back-off delay. starting now."); - timeService.wait(delayMilliSecs); + timeService.sleepMillis(delayMilliSecs); } catch(InterruptedException ex ) { logger.info("Interrupted from waiting during a back-off delay. Ignoring this, but cutting our wait short."); } diff --git a/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/MockTimeService.java b/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/MockTimeService.java index eadad615..ed388c1f 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/MockTimeService.java +++ b/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/MockTimeService.java @@ -22,6 +22,12 @@ public Instant now() { return currentTime; } + @Override + public void sleepMillis(long millisToSleep) throws InterruptedException { + // Pretend we are sleeping, so the current time advances. + this.currentTime = this.currentTime.plusMillis(millisToSleep); + } + public void setCurrentTime(Instant currentTime) { this.currentTime = currentTime; } diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java index ee9afd8c..4d18d26b 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java @@ -45,7 +45,13 @@ public class CouchdbValidatorImpl implements CouchdbValidator { private static final CouchDbVersion minCouchDbVersion = new CouchDbVersion(3,3,3); - public void checkCouchdbDatabaseIsValid( URI rasUri, CloseableHttpClient httpClient , HttpRequestFactory httpRequestFactory, ITimeService timeService) throws CouchdbException { + public void checkCouchdbDatabaseIsValid( + URI rasUri, + CloseableHttpClient httpClient , + HttpRequestFactory httpRequestFactory, + ITimeService timeService + ) throws CouchdbException { + this.requestFactory = httpRequestFactory; HttpGet httpGet = requestFactory.getHttpGetRequest(rasUri.toString()); @@ -64,20 +70,20 @@ public void checkCouchdbDatabaseIsValid( URI rasUri, CloseableHttpClient httpCli } checkVersion(welcome.version, minCouchDbVersion); - checkDatabasePresent(httpClient, rasUri, 1, "galasa_run"); - checkDatabasePresent(httpClient, rasUri, 1, "galasa_log"); - checkDatabasePresent(httpClient, rasUri, 1, "galasa_artifacts"); + checkDatabasePresent(httpClient, rasUri, 1, "galasa_run", timeService); + checkDatabasePresent(httpClient, rasUri, 1, "galasa_log", timeService); + checkDatabasePresent(httpClient, rasUri, 1, "galasa_artifacts", timeService); - checkRunDesignDocument(httpClient, rasUri,1); + checkRunDesignDocument(httpClient, rasUri,1, timeService); - checkIndex(httpClient, rasUri, 1, "galasa_run", "runName"); - checkIndex(httpClient, rasUri, 1, "galasa_run", "requestor"); - checkIndex(httpClient, rasUri, 1, "galasa_run", "queued"); - checkIndex(httpClient, rasUri, 1, "galasa_run", "startTime"); - checkIndex(httpClient, rasUri, 1, "galasa_run", "endTime"); - checkIndex(httpClient, rasUri, 1, "galasa_run", "testName"); - checkIndex(httpClient, rasUri, 1, "galasa_run", "bundle"); - checkIndex(httpClient, rasUri, 1, "galasa_run", "result"); + checkIndex(httpClient, rasUri, 1, "galasa_run", "runName",timeService); + checkIndex(httpClient, rasUri, 1, "galasa_run", "requestor",timeService); + checkIndex(httpClient, rasUri, 1, "galasa_run", "queued",timeService); + checkIndex(httpClient, rasUri, 1, "galasa_run", "startTime", timeService); + checkIndex(httpClient, rasUri, 1, "galasa_run", "endTime", timeService); + checkIndex(httpClient, rasUri, 1, "galasa_run", "testName", timeService); + checkIndex(httpClient, rasUri, 1, "galasa_run", "bundle", timeService); + checkIndex(httpClient, rasUri, 1, "galasa_run", "result", timeService); logger.debug("RAS CouchDB at " + rasUri.toString() + " validated"); } catch (CouchdbException e) { @@ -89,7 +95,7 @@ public void checkCouchdbDatabaseIsValid( URI rasUri, CloseableHttpClient httpCli - private void checkDatabasePresent( CloseableHttpClient httpClient, URI rasUri, int attempts, String dbName) throws CouchdbException { + private void checkDatabasePresent( CloseableHttpClient httpClient, URI rasUri, int attempts, String dbName, ITimeService timeService) throws CouchdbException { HttpHead httpHead = requestFactory.getHttpHeadRequest(rasUri + "/" + dbName); try (CloseableHttpResponse response = httpClient.execute(httpHead)) { @@ -121,8 +127,8 @@ private void checkDatabasePresent( CloseableHttpClient httpClient, URI rasUri, i throw new CouchdbException( "Create Database " + dbName + " failed on CouchDB server due to conflicts, attempted 10 times"); } - Thread.sleep(1000 + new Random().nextInt(3000)); - checkDatabasePresent(httpClient, rasUri, attempts, dbName); + timeService.sleepMillis(1000 + new Random().nextInt(3000)); + checkDatabasePresent(httpClient, rasUri, attempts, dbName, timeService); return; } @@ -140,7 +146,7 @@ private void checkDatabasePresent( CloseableHttpClient httpClient, URI rasUri, i } } - private void checkRunDesignDocument( CloseableHttpClient httpClient , URI rasUri , int attempts) throws CouchdbException { + private void checkRunDesignDocument( CloseableHttpClient httpClient , URI rasUri , int attempts, ITimeService timeService) throws CouchdbException { HttpGet httpGet = requestFactory.getHttpGetRequest(rasUri + "/galasa_run/_design/docs"); String docJson = null; @@ -243,8 +249,8 @@ private void checkRunDesignDocument( CloseableHttpClient httpClient , URI rasUri throw new CouchdbException( "Update of galasa_run design document failed on CouchDB server due to conflicts, attempted 10 times"); } - Thread.sleep(1000 + new Random().nextInt(3000)); - checkRunDesignDocument(httpClient, rasUri, attempts); + timeService.sleepMillis(1000 + new Random().nextInt(3000)); + checkRunDesignDocument(httpClient, rasUri, attempts, timeService); return; } @@ -315,7 +321,7 @@ private void checkVersion(String version, CouchDbVersion minVersion) - private void checkIndex(CloseableHttpClient httpClient, URI rasUri , int attempts, String dbName, String field) throws CouchdbException { + private void checkIndex(CloseableHttpClient httpClient, URI rasUri , int attempts, String dbName, String field, ITimeService timeService) throws CouchdbException { HttpGet httpGet = requestFactory.getHttpGetRequest(rasUri + "/galasa_run/_index"); String idxJson = null; @@ -392,8 +398,8 @@ private void checkIndex(CloseableHttpClient httpClient, URI rasUri , int attempt throw new CouchdbException( "Update of galasa_run index failed on CouchDB server due to conflicts, attempted 10 times"); } - Thread.sleep(1000 + new Random().nextInt(3000)); - checkIndex(httpClient, rasUri, attempts, dbName, field); + timeService.sleepMillis(1000 + new Random().nextInt(3000)); + checkIndex(httpClient, rasUri, attempts, dbName, field, timeService); return; } From 86a1603607398d720c0bb0213087292d4208606f Mon Sep 17 00:00:00 2001 From: Aashir Siddiqui Date: Thu, 5 Sep 2024 15:37:32 +0100 Subject: [PATCH 12/13] Implemented the changes requested Signed-off-by: Aashir Siddiqui --- .../internal/CouchdbAuthStoreValidator.java | 10 ++++---- .../beans/TokensDBNameViewDesign.java | 10 ++++++++ .../auth/couchdb/TestCouchdbAuthStore.java | 24 +++++++++++++++++++ .../TestCouchdbAuthStoreValidator.java | 13 ---------- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java index 746fab82..7f5bb33a 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java @@ -37,7 +37,7 @@ public class CouchdbAuthStoreValidator extends CouchdbBaseValidator { private final Log logger = LogFactory.getLog(getClass()); private final GalasaGson gson = new GalasaGson(); - + // A couchDB view, it gets all the access tokens of a the user based on the loginId provided. public static final String DB_TABLE_TOKENS_DESIGN = "function (doc) { if (doc.owner && doc.owner.loginId) {emit(doc.owner.loginId, doc); } }"; @Override @@ -75,7 +75,7 @@ public void checkTokensDesignDocument(CloseableHttpClient httpClient, URI couchd TokensDBNameViewDesign tableDesign = parseTokenDesignFromJson(docJson); - boolean isDesignUpdated = transformDesignToDesired(tableDesign); + boolean isDesignUpdated = updateDesignDocToDesiredDesignDoc(tableDesign); if (isDesignUpdated) { updateTokenDesignDocument(httpClient, couchdbUri, attempts, tableDesign); @@ -96,7 +96,7 @@ private TokensDBNameViewDesign parseTokenDesignFromJson(String docJson) throws C return tableDesign; } - private boolean transformDesignToDesired(TokensDBNameViewDesign tableDesign) { + private boolean updateDesignDocToDesiredDesignDoc(TokensDBNameViewDesign tableDesign) { boolean isUpdated = false; if (tableDesign.views == null) { @@ -126,7 +126,7 @@ private boolean transformDesignToDesired(TokensDBNameViewDesign tableDesign) { private String getTokenDesignDocument(CloseableHttpClient httpClient, URI couchdbUri, int attempts) throws CouchdbException { HttpRequestFactory requestFactory = super.getRequestFactory(); - HttpGet httpGet = requestFactory.getHttpGetRequest(couchdbUri + "/galasa_tokens/_design/docs"); + HttpGet httpGet = requestFactory.getHttpGetRequest(couchdbUri + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME +"/_design/docs"); String docJson = null; try (CloseableHttpResponse response = httpClient.execute(httpGet)) { @@ -160,7 +160,7 @@ private void updateTokenDesignDocument(CloseableHttpClient httpClient, URI couch HttpEntity entity = new StringEntity(gson.toJson(tokenViewDesign), ContentType.APPLICATION_JSON); - HttpPut httpPut = requestFactory.getHttpPutRequest(couchdbUri + "/galasa_tokens/_design/docs"); + HttpPut httpPut = requestFactory.getHttpPutRequest(couchdbUri + "/" + CouchdbAuthStore.TOKENS_DATABASE_NAME +"/_design/docs"); httpPut.setEntity(entity); if (tokenViewDesign._rev != null) { diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokensDBNameViewDesign.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokensDBNameViewDesign.java index 0308a7e4..8c7add64 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokensDBNameViewDesign.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/beans/TokensDBNameViewDesign.java @@ -5,6 +5,16 @@ */ package dev.galasa.auth.couchdb.internal.beans; +//{ +// "_id": "_design/docs", +// "_rev": "3-xxxxxxxxxxx9c9072dyy", +// "views": { +// "loginId-view": { +// "map": "function (doc) {\n if (doc.owner && doc.owner.loginId) {\n emit(doc.owner.loginId, doc);\n }\n}" +// } +// }, +// "language": "javascript" +// } public class TokensDBNameViewDesign { public String _rev; public String _id; diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStore.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStore.java index 9da9e6c1..a3e6289b 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStore.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStore.java @@ -194,6 +194,30 @@ public void testGetTokensReturnsTokensByLoginIdFromCouchdbOK() throws Exception assertThat(actualToken).usingRecursiveComparison().isEqualTo(mockToken); } + @Test + public void testGetTokensReturnsTokensByLoginIdWithFailingRequestReturnsError() throws Exception { + // Given... + URI authStoreUri = URI.create("couchdb:https://my-auth-store"); + MockLogFactory logFactory = new MockLogFactory(); + + List interactions = new ArrayList(); + interactions.add(new GetAllTokenDocumentsInteraction("https://my-auth-store/galasa_tokens/_design/docs/_view/loginId-view?key=johndoe", HttpStatus.SC_INTERNAL_SERVER_ERROR, null)); + + MockCloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); + + MockHttpClientFactory httpClientFactory = new MockHttpClientFactory(mockHttpClient); + MockTimeService mockTimeService = new MockTimeService(Instant.now()); + + CouchdbAuthStore authStore = new CouchdbAuthStore(authStoreUri, httpClientFactory, new HttpRequestFactoryImpl(), logFactory, new MockCouchdbValidator(), mockTimeService); + + // When... + AuthStoreException thrown = catchThrowableOfType(() -> authStore.getTokensByLoginId("johndoe"), AuthStoreException.class); + + // Then... + assertThat(thrown).isNotNull(); + assertThat(thrown.getMessage()).contains("GAL6101E", "Failed to get auth tokens from the CouchDB auth store"); + } + @Test public void testStoreTokenSendsRequestToCreateTokenDocumentOK() throws Exception { // Given... diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java index 33b1967e..f183a819 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStoreValidator.java @@ -108,19 +108,6 @@ public void validateRequest(HttpHost host, HttpRequest request) throws RuntimeEx } } - - -// "_id": "_design/docs", -// "_rev": "3-9e69612124f138c029ab40c9c9072deb", -// "views": { -// "loginId-view": { -// "map": "function (doc) {\n if (doc.owner && doc.owner.loginId) {\n emit(doc.owner.loginId, doc);\n }\n}" -// } -// }, -// "language": "javascript" -// } - - @Test public void testCheckCouchdbDatabaseIsValidWithValidDatabaseIsOK() throws Exception { // Given... From 151bf7da57bd189fdaaea3c773f91484ed53a8e0 Mon Sep 17 00:00:00 2001 From: Aashir Siddiqui Date: Thu, 12 Sep 2024 16:33:20 +0100 Subject: [PATCH 13/13] Wrote unit tests for RetryableProcessor Signed-off-by: Aashir Siddiqui --- .../couchdb/internal/CouchdbAuthStore.java | 2 +- .../internal/CouchdbAuthStoreValidator.java | 28 +++- ...ryableCouchdbUpdateOperationProcessor.java | 69 --------- .../auth/couchdb/TestCouchdbAuthStore.java | 11 +- .../dev.galasa.extensions.common/build.gradle | 2 + .../dev/galasa/extensions/common/Errors.java | 1 + .../common/couchdb/CouchdbStore.java | 116 +++++++++----- ...ryableCouchdbUpdateOperationProcessor.java | 111 ++++++++++++++ .../common/couchdb/pojos/ViewRow.java | 1 + .../common/couchdb/CouchDbVersionTest.java | 2 - ...leCouchdbUpdateOperationProcessorTest.java | 143 ++++++++++++++++++ .../extensions/mocks/MockTimeService.java | 5 - .../internal/CouchdbValidatorImpl.java | 1 + 13 files changed, 366 insertions(+), 126 deletions(-) delete mode 100644 galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java create mode 100644 galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/RetryableCouchdbUpdateOperationProcessor.java create mode 100644 galasa-extensions-parent/dev.galasa.extensions.common/src/test/java/dev/galasa/extensions/common/couchdb/RetryableCouchdbUpdateOperationProcessorTest.java diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java index 95093c62..a88b3810 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStore.java @@ -94,7 +94,7 @@ public List getTokensByLoginId(String loginId) throws AuthSt // Build up a list of all the tokens using the document IDs for (ViewRow row : tokenDocuments) { - tokens.add(getAuthTokenFromDocument(row.key)); + tokens.add(getAuthTokenFromDocument(row.id)); } logger.info("Tokens retrieved from CouchDB OK"); diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java index 7f5bb33a..3c3e8d41 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/CouchdbAuthStoreValidator.java @@ -8,7 +8,6 @@ import java.net.URI; import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; import org.apache.http.StatusLine; @@ -22,9 +21,13 @@ import com.google.gson.JsonSyntaxException; + +import dev.galasa.extensions.common.api.LogFactory; + import dev.galasa.extensions.common.couchdb.CouchdbBaseValidator; import dev.galasa.extensions.common.couchdb.CouchdbClashingUpdateException; import dev.galasa.extensions.common.couchdb.CouchdbException; +import dev.galasa.extensions.common.couchdb.RetryableCouchdbUpdateOperationProcessor; import dev.galasa.auth.couchdb.internal.beans.*; import dev.galasa.extensions.common.api.HttpRequestFactory; import dev.galasa.framework.spi.utils.GalasaGson; @@ -34,12 +37,27 @@ public class CouchdbAuthStoreValidator extends CouchdbBaseValidator { - private final Log logger = LogFactory.getLog(getClass()); + private final Log logger ; private final GalasaGson gson = new GalasaGson(); + private final LogFactory logFactory; // A couchDB view, it gets all the access tokens of a the user based on the loginId provided. public static final String DB_TABLE_TOKENS_DESIGN = "function (doc) { if (doc.owner && doc.owner.loginId) {emit(doc.owner.loginId, doc); } }"; + public CouchdbAuthStoreValidator() { + this(new LogFactory(){ + @Override + public Log getLog(Class clazz) { + return org.apache.commons.logging.LogFactory.getLog(clazz); + } + }); + } + + public CouchdbAuthStoreValidator(LogFactory logFactory) { + this.logFactory = logFactory; + this.logger = logFactory.getLog(getClass()); + } + @Override public void checkCouchdbDatabaseIsValid( URI couchdbUri, @@ -51,7 +69,7 @@ public void checkCouchdbDatabaseIsValid( // Perform the base CouchDB checks super.checkCouchdbDatabaseIsValid(couchdbUri, httpClient, httpRequestFactory, timeService); - RetryableCouchdbUpdateOperationProcessor retryProcessor = new RetryableCouchdbUpdateOperationProcessor(timeService); + RetryableCouchdbUpdateOperationProcessor retryProcessor = new RetryableCouchdbUpdateOperationProcessor(timeService, this.logFactory); retryProcessor.retryCouchDbUpdateOperation( ()->{ tryToCheckAndUpdateCouchDBTokenView(couchdbUri, httpClient, httpRequestFactory); @@ -170,13 +188,15 @@ private void updateTokenDesignDocument(CloseableHttpClient httpClient, URI couch try (CloseableHttpResponse response = httpClient.execute(httpPut)) { StatusLine statusLine = response.getStatusLine(); int statusCode = statusLine.getStatusCode(); + + EntityUtils.consumeQuietly(response.getEntity()); + if (statusCode == HttpStatus.SC_CONFLICT) { // Someone possibly updated the document while we were thinking about it. // It was probably another instance of this exact code. throw new CouchdbClashingUpdateException(ERROR_FAILED_TO_UPDATE_COUCHDB_DESING_DOC_CONFLICT.toString()); } - EntityUtils.consumeQuietly(response.getEntity()); if (statusCode != HttpStatus.SC_CREATED) { throw new CouchdbException( diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java deleted file mode 100644 index b1589a20..00000000 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/main/java/dev/galasa/auth/couchdb/internal/RetryableCouchdbUpdateOperationProcessor.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright contributors to the Galasa project - * - * SPDX-License-Identifier: EPL-2.0 - */ -package dev.galasa.auth.couchdb.internal; - -import java.util.Random; - -import dev.galasa.extensions.common.couchdb.CouchdbClashingUpdateException; -import dev.galasa.extensions.common.couchdb.CouchdbException; -import dev.galasa.framework.spi.utils.ITimeService; - -import org.apache.commons.logging.Log; - -/** - * Allows a lambda function to be used, and that function will be retried a number of times before giving up. - */ -public class RetryableCouchdbUpdateOperationProcessor { - - public int MAX_ATTEMPTS_TO_GO_BEFORE_GIVE_UP = 10; - private ITimeService timeService ; - private Log logger; - public interface RetryableCouchdbUpdateOperation { - public void tryToUpdateCouchDb() throws CouchdbException; - } - - - public RetryableCouchdbUpdateOperationProcessor(ITimeService timeService) { - this.timeService = timeService; - } - - public void retryCouchDbUpdateOperation(RetryableCouchdbUpdateOperation retryableOperation) throws CouchdbException{ - int attemptsToGoBeforeGiveUp = MAX_ATTEMPTS_TO_GO_BEFORE_GIVE_UP; - boolean isDone = false; - - while (!isDone) { - - try { - retryableOperation.tryToUpdateCouchDb(); - - isDone = true; - } catch (CouchdbClashingUpdateException updateClashedEx) { - - logger.info("Clashing update detected. Backing off for a short time to avoid another clash immediately. "); - - waitForBackoffDelay(timeService); - - attemptsToGoBeforeGiveUp -= 1; - if (attemptsToGoBeforeGiveUp == 0) { - throw new CouchdbException("Failed after " + Integer.toString(MAX_ATTEMPTS_TO_GO_BEFORE_GIVE_UP) + " attempts to update the design document in CouchDB due to conflicts.", updateClashedEx); - } else { - logger.info("Failed to update CouchDB design document, retrying..."); - } - } - } - } - - private void waitForBackoffDelay(ITimeService timeService) { - Long delayMilliSecs = 1000L + new Random().nextInt(3000); - - try { - logger.info("Waiting "+delayMilliSecs+" during a back-off delay. starting now."); - timeService.sleepMillis(delayMilliSecs); - } catch(InterruptedException ex ) { - logger.info("Interrupted from waiting during a back-off delay. Ignoring this, but cutting our wait short."); - } - } -} \ No newline at end of file diff --git a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStore.java b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStore.java index a3e6289b..0ec9b21d 100644 --- a/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStore.java +++ b/galasa-extensions-parent/dev.galasa.auth.couchdb/src/test/java/dev/galasa/auth/couchdb/TestCouchdbAuthStore.java @@ -105,7 +105,7 @@ public void testGetTokensReturnsTokensWithFailingRequestReturnsError() throws Ex MockLogFactory logFactory = new MockLogFactory(); List interactions = new ArrayList(); - interactions.add(new GetAllTokenDocumentsInteraction("https://my-auth-store/galasa_tokens/_all_docs", HttpStatus.SC_INTERNAL_SERVER_ERROR, null)); + interactions.add(new GetAllTokenDocumentsInteraction("https://my-auth-store/galasa_tokens/_all_docs?include_docs=true&endkey=%22_%22", HttpStatus.SC_INTERNAL_SERVER_ERROR, null)); MockCloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); @@ -137,7 +137,7 @@ public void testGetTokensReturnsTokensFromCouchdbOK() throws Exception { CouchdbAuthToken mockToken = new CouchdbAuthToken("token1", "dex-client", "my test token", Instant.now(), new CouchdbUser("johndoe", "dex-user-id")); List interactions = new ArrayList(); - interactions.add(new GetAllTokenDocumentsInteraction("https://my-auth-store/galasa_tokens/_all_docs", HttpStatus.SC_OK, mockAllDocsResponse)); + interactions.add(new GetAllTokenDocumentsInteraction("https://my-auth-store/galasa_tokens/_all_docs?include_docs=true&endkey=%22_%22", HttpStatus.SC_OK, mockAllDocsResponse)); interactions.add(new GetTokenDocumentInteraction("https://my-auth-store/galasa_tokens/token1", HttpStatus.SC_OK, mockToken)); MockCloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); @@ -164,7 +164,7 @@ public void testGetTokensReturnsTokensByLoginIdFromCouchdbOK() throws Exception MockLogFactory logFactory = new MockLogFactory(); ViewRow tokenDoc = new ViewRow(); - tokenDoc.key = "token1"; + tokenDoc.id = "token1"; List mockDocs = List.of(tokenDoc); ViewResponse mockAllDocsResponse = new ViewResponse(); @@ -173,7 +173,7 @@ public void testGetTokensReturnsTokensByLoginIdFromCouchdbOK() throws Exception CouchdbAuthToken mockToken = new CouchdbAuthToken("token1", "dex-client", "my test token", Instant.now(), new CouchdbUser("johndoe", "dex-user-id")); CouchdbAuthToken mockToken2 = new CouchdbAuthToken("token2", "dex-client", "my test token", Instant.now(), new CouchdbUser("notJohnDoe", "dex-user-id")); List interactions = new ArrayList(); - interactions.add(new GetAllTokenDocumentsInteraction("https://my-auth-store/galasa_tokens/_design/docs/_view/loginId-view?key=johndoe", HttpStatus.SC_OK, mockAllDocsResponse)); + interactions.add(new GetAllTokenDocumentsInteraction("https://my-auth-store/galasa_tokens/_design/docs/_view/loginId-view?key=%22johndoe%22", HttpStatus.SC_OK, mockAllDocsResponse)); interactions.add(new GetTokenDocumentInteraction("https://my-auth-store/galasa_tokens/token1", HttpStatus.SC_OK, mockToken)); interactions.add(new GetTokenDocumentInteraction("https://my-auth-store/galasa_tokens/token1", HttpStatus.SC_OK, mockToken2)); @@ -183,7 +183,6 @@ public void testGetTokensReturnsTokensByLoginIdFromCouchdbOK() throws Exception MockTimeService mockTimeService = new MockTimeService(Instant.now()); CouchdbAuthStore authStore = new CouchdbAuthStore(authStoreUri, httpClientFactory, new HttpRequestFactoryImpl(), logFactory, new MockCouchdbValidator(), mockTimeService); - // When... List tokens = authStore.getTokensByLoginId("johndoe"); @@ -201,7 +200,7 @@ public void testGetTokensReturnsTokensByLoginIdWithFailingRequestReturnsError() MockLogFactory logFactory = new MockLogFactory(); List interactions = new ArrayList(); - interactions.add(new GetAllTokenDocumentsInteraction("https://my-auth-store/galasa_tokens/_design/docs/_view/loginId-view?key=johndoe", HttpStatus.SC_INTERNAL_SERVER_ERROR, null)); + interactions.add(new GetAllTokenDocumentsInteraction("https://my-auth-store/galasa_tokens/_design/docs/_view/loginId-view?key=%22johndoe%22", HttpStatus.SC_INTERNAL_SERVER_ERROR, null)); MockCloseableHttpClient mockHttpClient = new MockCloseableHttpClient(interactions); diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/build.gradle b/galasa-extensions-parent/dev.galasa.extensions.common/build.gradle index 64a33227..7c43f48d 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.common/build.gradle +++ b/galasa-extensions-parent/dev.galasa.extensions.common/build.gradle @@ -11,6 +11,8 @@ dependencies { implementation ('org.apache.httpcomponents:httpclient-osgi:4.5.13') implementation ('org.apache.httpcomponents:httpcore-osgi:4.4.14') implementation ('com.google.code.gson:gson:2.10.1') + + testImplementation(project(':dev.galasa.extensions.mocks')) } // Note: These values are consumed by the parent build process diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/Errors.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/Errors.java index 6470c571..7b0df5f4 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/Errors.java +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/Errors.java @@ -58,6 +58,7 @@ public enum Errors { ERROR_GALASA_REST_CALL_TO_GET_CPS_NAMESPACES_FAILED (7020,"GAL7020E: Could not get the CPS namespaces information from URL ''{0}''. Cause: {1}"), ERROR_GALASA_REST_CALL_TO_GET_CPS_NAMESPACES_BAD_JSON_RETURNED (7021,"GAL7021E: Could not get the CPS namespaces value from URL ''{0}''. Cause: Bad json returned from the server. {1}"), + ERROR_GALASA_COUCHDB_UPDATED_FAILED_AFTER_RETRIES (7022,"GAL7022E: Couchdb operation failed after {0} attempts, due to conflicts."), ; private String template; diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbStore.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbStore.java index 8bd5878d..a7d86b99 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbStore.java +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/CouchdbStore.java @@ -8,8 +8,10 @@ import static dev.galasa.extensions.common.Errors.*; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.CopyOption; import java.nio.file.Files; @@ -18,6 +20,8 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; import org.apache.http.ParseException; @@ -40,8 +44,10 @@ import dev.galasa.framework.spi.utils.GalasaGson; /** - * This is a base class for CouchDB implementations of Galasa stores that defines functions for common interactions - * with CouchDB, including creating documents in a database and getting all documents that are stored in a database. + * This is a base class for CouchDB implementations of Galasa stores that + * defines functions for common interactions + * with CouchDB, including creating documents in a database and getting all + * documents that are stored in a database. */ public abstract class CouchdbStore { @@ -49,11 +55,14 @@ public abstract class CouchdbStore { protected final URI storeUri; + private Log logger = LogFactory.getLog(this.getClass()); + protected HttpRequestFactory httpRequestFactory; protected CloseableHttpClient httpClient; protected GalasaGson gson = new GalasaGson(); - public CouchdbStore(URI storeUri, HttpRequestFactory httpRequestFactory, HttpClientFactory httpClientFactory) throws CouchdbException { + public CouchdbStore(URI storeUri, HttpRequestFactory httpRequestFactory, HttpClientFactory httpClientFactory) + throws CouchdbException { // Strip off the 'couchdb:' prefix from the auth store URI // e.g. couchdb:https://myhost:5984 becomes https://myhost:5984 String storeUriStr = storeUri.toString(); @@ -71,10 +80,12 @@ public CouchdbStore(URI storeUri, HttpRequestFactory httpRequestFactory, HttpCli /** * Creates a new document in the given database with the given JSON content. * - * @param dbName the database to create the new document within - * @param jsonContent the JSON content to send to CouchDB in order to populate the new document + * @param dbName the database to create the new document within + * @param jsonContent the JSON content to send to CouchDB in order to populate + * the new document * @return PutPostResponse the response from the CouchDB service - * @throws CouchdbException if there is a problem accessing the CouchDB server or creating the document + * @throws CouchdbException if there is a problem accessing the CouchDB server + * or creating the document */ protected PutPostResponse createDocument(String dbName, String jsonContent) throws CouchdbException { // Create a new document in the tokens database with the new token to store @@ -95,15 +106,20 @@ protected PutPostResponse createDocument(String dbName, String jsonContent) thro } /** - * Sends a GET request to CouchDB's /{db}/_all_docs endpoint and returns the "rows" list in the response, + * Sends a GET request to CouchDB's /{db}/_all_docs endpoint and returns the + * "rows" list in the response, * which corresponds to the list of documents within the given database. * * @param dbName the name of the database to retrieve the documents of * @return a list of rows corresponding to documents within the database - * @throws CouchdbException if there was a problem accessing the CouchDB store or its response + * @throws CouchdbException if there was a problem accessing the CouchDB store + * or its response */ protected List getAllDocsFromDatabase(String dbName) throws CouchdbException { - HttpGet getTokensDocs = httpRequestFactory.getHttpGetRequest(storeUri + "/" + dbName + "/_all_docs"); + + //The end key is "_" because, design docs start with "_design", + // this will exclude any design documents from being fetched from couchdb. + HttpGet getTokensDocs = httpRequestFactory.getHttpGetRequest(storeUri + "/" + dbName + "/_all_docs?include_docs=true&endkey=%22_%22"); String responseEntity = sendHttpRequest(getTokensDocs, HttpStatus.SC_OK); ViewResponse allDocs = gson.fromJson(responseEntity, ViewResponse.class); @@ -113,21 +129,29 @@ protected List getAllDocsFromDatabase(String dbName) throws CouchdbExce String errorMessage = ERROR_FAILED_TO_GET_DOCUMENTS_FROM_DATABASE.getMessage(dbName); throw new CouchdbException(errorMessage); } - + return viewRows; } /** - * Sends a GET request to CouchDB's /{db}/_design/docs/_view/loginId-view?key={loginId} endpoint and returns the "rows" list in the response, + * Sends a GET request to CouchDB's + * /{db}/_design/docs/_view/loginId-view?key={loginId} endpoint and returns the + * "rows" list in the response, * which corresponds to the list of documents within the given database. * - * @param dbName the name of the database to retrieve the documents of + * @param dbName the name of the database to retrieve the documents of * @param loginId the loginId of the user to retrieve the doucemnts of * @return a list of rows corresponding to documents within the database - * @throws CouchdbException if there was a problem accessing the CouchDB store or its response + * @throws CouchdbException if there was a problem accessing the + * CouchDB store or its response + * @throws UnsupportedEncodingException A failure occurred. */ protected List getAllDocsByLoginId(String dbName, String loginId) throws CouchdbException { - HttpGet getTokensDocs = httpRequestFactory.getHttpGetRequest(storeUri + "/" + dbName + "/_design/docs/_view/loginId-view?key=" + loginId); + + String encodedLoginId = URLEncoder.encode("\"" + loginId + "\"", StandardCharsets.UTF_8); + String url = storeUri + "/" + dbName + "/_design/docs/_view/loginId-view?key=" + encodedLoginId; + + HttpGet getTokensDocs = httpRequestFactory.getHttpGetRequest(url); getTokensDocs.addHeader("Content-Type", "application/json"); String responseEntity = sendHttpRequest(getTokensDocs, HttpStatus.SC_OK); @@ -144,28 +168,32 @@ protected List getAllDocsByLoginId(String dbName, String loginId) throw } /** - * Gets an object from a given database's document using its document ID by sending a + * Gets an object from a given database's document using its document ID by + * sending a * GET /{db}/{docid} request to the CouchDB server. * - * @param The object type to be returned - * @param dbName the name of the database to retrieve the document from - * @param documentId the CouchDB ID for the document to retrieve - * @param classOfObject the class of the JSON object to retrieve from the CouchDB Document + * @param The object type to be returned + * @param dbName the name of the database to retrieve the document from + * @param documentId the CouchDB ID for the document to retrieve + * @param classOfObject the class of the JSON object to retrieve from the + * CouchDB Document * @return an object of the class provided in classOfObject - * @throws CouchdbException if there was a problem accessing the CouchDB store or its response + * @throws CouchdbException if there was a problem accessing the CouchDB store + * or its response */ - protected T getDocumentFromDatabase(String dbName, String documentId, Class classOfObject) throws CouchdbException { + protected T getDocumentFromDatabase(String dbName, String documentId, Class classOfObject) + throws CouchdbException { HttpGet getDocumentRequest = httpRequestFactory.getHttpGetRequest(storeUri + "/" + dbName + "/" + documentId); return gson.fromJson(sendHttpRequest(getDocumentRequest, HttpStatus.SC_OK), classOfObject); } - - protected void retrieveArtifactFromDatabase(String URI, Path cachePath, CopyOption copyOption) throws CouchdbException{ + protected void retrieveArtifactFromDatabase(String URI, Path cachePath, CopyOption copyOption) + throws CouchdbException { HttpGet httpGet = httpRequestFactory.getHttpGetRequest(URI); try (CloseableHttpResponse response = httpClient.execute(httpGet)) { StatusLine statusLine = response.getStatusLine(); if (statusLine.getStatusCode() != HttpStatus.SC_OK) { - String errorMessage = ERROR_URI_IS_INVALID .getMessage(URI); + String errorMessage = ERROR_URI_IS_INVALID.getMessage(URI); throw new CouchdbException(errorMessage); } HttpEntity entity = response.getEntity(); @@ -181,9 +209,10 @@ protected void retrieveArtifactFromDatabase(String URI, Path cachePath, CopyOpti * Deletes a document from a given database using its document ID by sending a * DELETE /{db}/{docid} request to the CouchDB server. * - * @param dbName the name of the database to delete the document from + * @param dbName the name of the database to delete the document from * @param documentId the CouchDB ID for the document to delete - * @throws CouchdbException if there was a problem accessing the CouchDB store or its response + * @throws CouchdbException if there was a problem accessing the CouchDB store + * or its response */ protected void deleteDocumentFromDatabase(String dbName, String documentId) throws CouchdbException { IdRev documentIdRev = getDocumentFromDatabase(dbName, documentId, IdRev.class); @@ -199,14 +228,18 @@ protected void deleteDocumentFromDatabase(String dbName, String documentId) thro } /** - * Sends a given HTTP request to the CouchDB server and returns the response body as a string. + * Sends a given HTTP request to the CouchDB server and returns the response + * body as a string. * - * @param httpRequest the HTTP request to send to the CouchDB server - * @param expectedHttpStatusCodes the expected Status code to get from the CouchDb server upon the request being actioned + * @param httpRequest the HTTP request to send to the CouchDB server + * @param expectedHttpStatusCodes the expected Status code to get from the + * CouchDb server upon the request being actioned * @return a string representation of the response. - * @throws CouchdbException if there was a problem accessing the CouchDB store or its response + * @throws CouchdbException if there was a problem accessing the CouchDB store + * or its response */ - protected String sendHttpRequest(HttpUriRequest httpRequest, int... expectedHttpStatusCodes) throws CouchdbException { + protected String sendHttpRequest(HttpUriRequest httpRequest, int... expectedHttpStatusCodes) + throws CouchdbException { String responseEntity = ""; try (CloseableHttpResponse response = httpClient.execute(httpRequest)) { StatusLine statusLine = response.getStatusLine(); @@ -214,10 +247,11 @@ protected String sendHttpRequest(HttpUriRequest httpRequest, int... expectedHttp if (!isStatusCodeExpected(actualStatusCode, expectedHttpStatusCodes)) { String expectedStatusCodesStr = IntStream.of(expectedHttpStatusCodes) - .mapToObj(Integer::toString) - .collect(Collectors.joining(", ")); + .mapToObj(Integer::toString) + .collect(Collectors.joining(", ")); - String errorMessage = ERROR_UNEXPECTED_COUCHDB_HTTP_RESPONSE.getMessage(httpRequest.getURI().toString(), expectedStatusCodesStr, actualStatusCode); + String errorMessage = ERROR_UNEXPECTED_COUCHDB_HTTP_RESPONSE.getMessage(httpRequest.getURI().toString(), + expectedStatusCodesStr, actualStatusCode); throw new CouchdbException(errorMessage); } @@ -225,18 +259,22 @@ protected String sendHttpRequest(HttpUriRequest httpRequest, int... expectedHttp responseEntity = EntityUtils.toString(entity); } catch (ParseException | IOException e) { - String errorMessage = ERROR_FAILURE_OCCURRED_WHEN_CONTACTING_COUCHDB.getMessage(httpRequest.getURI().toString(), e.getMessage()); + String errorMessage = ERROR_FAILURE_OCCURRED_WHEN_CONTACTING_COUCHDB + .getMessage(httpRequest.getURI().toString(), e.getMessage()); throw new CouchdbException(errorMessage, e); } return responseEntity; } /** - * Checks if a given status code is an expected status code using a given array of expected status codes. + * Checks if a given status code is an expected status code using a given array + * of expected status codes. * - * @param actualStatusCode the status code to check - * @param expectedStatusCodes an array of expected status codes returned from CouchDB - * @return true if the actual status code is an expected status code, false otherwise + * @param actualStatusCode the status code to check + * @param expectedStatusCodes an array of expected status codes returned from + * CouchDB + * @return true if the actual status code is an expected status code, false + * otherwise */ private boolean isStatusCodeExpected(int actualStatusCode, int... expectedStatusCodes) { boolean isExpectedStatusCode = false; diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/RetryableCouchdbUpdateOperationProcessor.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/RetryableCouchdbUpdateOperationProcessor.java new file mode 100644 index 00000000..2d4b77e4 --- /dev/null +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/RetryableCouchdbUpdateOperationProcessor.java @@ -0,0 +1,111 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.extensions.common.couchdb; + +import java.util.Random; + +import dev.galasa.extensions.common.Errors; +import dev.galasa.extensions.common.api.LogFactory; +import dev.galasa.framework.spi.utils.ITimeService; + +import org.apache.commons.logging.Log; + +/** + * Allows a lambda function to be used, and that function will be retried a number of times before giving up. + */ +public class RetryableCouchdbUpdateOperationProcessor { + + public static final int DEFAULT_MAX_ATTEMPTS_TO_GO_BEFORE_GIVE_UP = 10; + + private ITimeService timeService ; + private Log logger; + + /** + * Lambda supplying the code which should be repeated during successive attempts + */ + public interface RetryableCouchdbUpdateOperation { + /** + * @throws CouchdbException Something went wrong and no retries should be attempted, failing by passing this error upwards to the caller. + * @throws CouchdbClashingUpdateException Couchdb can't do the update right now, the routine will be re-tried. + */ + public void tryToUpdateCouchDb() throws CouchdbException, CouchdbClashingUpdateException; + } + + /** + * Calculates how much time delay we need to leave between attempts to update. + */ + public interface BackoffTimeCalculator { + /** + * @return The number of milliseconds to wait between successive re-tries of the couchdb update operation. + */ + public default long getBackoffDelayMillis() { + return 1000L + new Random().nextInt(3000); + } + } + + public RetryableCouchdbUpdateOperationProcessor(ITimeService timeService, LogFactory logFactory ) { + this.timeService = timeService; + this.logger = logFactory.getLog(this.getClass()); + } + + /** + * Pass an operation you want retried if it fails with a CouchdbClashingUpdateException, using defaults. + * + * It retries for a default number of times before giving up, with a default random backoff time between retry attempts. + * + * @param retryableOperation The operation we want to retry. + * @throws CouchdbException A failure occurred. + */ + public void retryCouchDbUpdateOperation(RetryableCouchdbUpdateOperation retryableOperation) throws CouchdbException { + retryCouchDbUpdateOperation(retryableOperation, DEFAULT_MAX_ATTEMPTS_TO_GO_BEFORE_GIVE_UP, new BackoffTimeCalculator() {} ); + } + + /** + * Pass an operation you want retried if it fails with a CouchdbClashingUpdateException. + * + * @param retryableOperation The operation we want to retry. + * @param attemptsToGoBeforeGiveUp The number of times the retryable operation is attempted before eventually giving up with a failure. + * @param backofftimeCalculator The lambda operation we consult to find out backoff times between retry attempts + * @throws CouchdbException A failure occurred. + */ + public void retryCouchDbUpdateOperation(RetryableCouchdbUpdateOperation retryableOperation, int attemptsToGoBeforeGiveUp, BackoffTimeCalculator backofftimeCalculator) throws CouchdbException{ + boolean isDone = false; + int retriesRemaining = attemptsToGoBeforeGiveUp; + + while (!isDone) { + + try { + retryableOperation.tryToUpdateCouchDb(); + + isDone = true; + } catch (CouchdbClashingUpdateException updateClashedEx) { + + logger.info("Clashing update detected. Backing off for a short time to avoid another clash immediately. "); + + waitForBackoffDelay(timeService, backofftimeCalculator); + + retriesRemaining -= 1; + if (retriesRemaining == 0) { + String msg = Errors.ERROR_GALASA_COUCHDB_UPDATED_FAILED_AFTER_RETRIES.getMessage(Integer.toString(attemptsToGoBeforeGiveUp)); + logger.info(msg); + throw new CouchdbException(msg, updateClashedEx); + } else { + logger.info("Failed to perform the couchdb operation, retrying..."); + } + } + } + } + + void waitForBackoffDelay(ITimeService timeService2, BackoffTimeCalculator backofftimeCalculator) { + long delayMilliSecs = backofftimeCalculator.getBackoffDelayMillis(); + try { + logger.info("Waiting "+delayMilliSecs+" during a back-off delay. starting now."); + timeService2.sleepMillis(delayMilliSecs); + } catch(InterruptedException ex ) { + logger.info("Interrupted from waiting during a back-off delay. Ignoring this, but cutting our wait short."); + } + } +} \ No newline at end of file diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/pojos/ViewRow.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/pojos/ViewRow.java index 61b39dbd..6decec20 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/pojos/ViewRow.java +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/couchdb/pojos/ViewRow.java @@ -7,6 +7,7 @@ public class ViewRow { + public String id; public String key; public Object value; diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/src/test/java/dev/galasa/extensions/common/couchdb/CouchDbVersionTest.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/test/java/dev/galasa/extensions/common/couchdb/CouchDbVersionTest.java index 287e9384..c43099ea 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.common/src/test/java/dev/galasa/extensions/common/couchdb/CouchDbVersionTest.java +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/test/java/dev/galasa/extensions/common/couchdb/CouchDbVersionTest.java @@ -36,7 +36,6 @@ public void testInvalidVersionStringThrowsParsingError() throws Exception { CouchdbException ex = catchThrowableOfType( ()->{ new CouchDbVersion(invalidVersion); }, CouchdbException.class ); assertThat(ex).hasMessageContaining("GAL6010E: Invalid CouchDB server version format detected. The CouchDB version '" + invalidVersion + "'"); - // TODO: Assert that more of the message is in here. } @Test @@ -47,7 +46,6 @@ public void testInvalidLeadingDotVersionStringThrowsParsingError() throws Except CouchdbException ex = catchThrowableOfType( ()->{ new CouchDbVersion(invalidVersion); }, CouchdbException.class ); assertThat(ex).hasMessageContaining("GAL6010E: Invalid CouchDB server version format detected. The CouchDB version '" + invalidVersion + "'"); - // TODO: Assert that more of the message is in here. } diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/src/test/java/dev/galasa/extensions/common/couchdb/RetryableCouchdbUpdateOperationProcessorTest.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/test/java/dev/galasa/extensions/common/couchdb/RetryableCouchdbUpdateOperationProcessorTest.java new file mode 100644 index 00000000..44f2a7c6 --- /dev/null +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/test/java/dev/galasa/extensions/common/couchdb/RetryableCouchdbUpdateOperationProcessorTest.java @@ -0,0 +1,143 @@ +package dev.galasa.extensions.common.couchdb; + +import java.time.Instant; + +import org.junit.Test; + +import dev.galasa.extensions.common.couchdb.RetryableCouchdbUpdateOperationProcessor.BackoffTimeCalculator; +import dev.galasa.extensions.common.couchdb.RetryableCouchdbUpdateOperationProcessor.RetryableCouchdbUpdateOperation; +import dev.galasa.extensions.mocks.MockLogFactory; +import dev.galasa.extensions.mocks.MockTimeService; + +import static org.assertj.core.api.Assertions.*; + +public class RetryableCouchdbUpdateOperationProcessorTest { + + + @Test + public void testSuccessfulUpdateDoesNotThrowException() throws Exception { + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); + MockLogFactory mockLogFactory = new MockLogFactory(); + + int attemptsBeforeGivingUp = 10; + BackoffTimeCalculator backoffTimeCalculator = new BackoffTimeCalculator() {}; + + RetryableCouchdbUpdateOperationProcessor processor = new RetryableCouchdbUpdateOperationProcessor(mockTimeService, mockLogFactory); + + RetryableCouchdbUpdateOperation operationThatPasses = new RetryableCouchdbUpdateOperation() { + @Override + public void tryToUpdateCouchDb() throws CouchdbException, CouchdbClashingUpdateException { + // Simulate successful update + } + }; + + // When... + processor.retryCouchDbUpdateOperation(operationThatPasses, attemptsBeforeGivingUp, backoffTimeCalculator); + + // Then... + // No errors should have been thrown + assertThat(mockTimeService.now()).as("time passed, procesor waited when it should not have done so.").isEqualTo(Instant.EPOCH); + assertThat(mockLogFactory.toString()).as("retry processor logged something, when nothing was expected if the retry operation passes first time.").isBlank(); + } + + @Test + public void testRetriesUntilItGivesUp() throws Exception { + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); + MockLogFactory mockLogFactory = new MockLogFactory(); + int attemptsBeforeGivingUp = 10; + + // A backoff of 1ms each time, so there is no random element in a unit test, and we can compare the time delayed later. + BackoffTimeCalculator backoffTimeCalculator = new BackoffTimeCalculator() { + public long getBackoffDelayMillis() { + return 1; + } + }; + + RetryableCouchdbUpdateOperationProcessor processor = new RetryableCouchdbUpdateOperationProcessor(mockTimeService, mockLogFactory); + + RetryableCouchdbUpdateOperation operationThatFails = new RetryableCouchdbUpdateOperation() { + @Override + public void tryToUpdateCouchDb() throws CouchdbException, CouchdbClashingUpdateException { + throw new CouchdbClashingUpdateException("simulating constant failures"); + } + }; + + // When... + CouchdbException thrown = catchThrowableOfType(() -> { + processor.retryCouchDbUpdateOperation(operationThatFails, attemptsBeforeGivingUp, backoffTimeCalculator); + }, CouchdbException.class); + + // Then + assertThat(thrown).isNotNull(); + assertThat(thrown.getMessage()).contains("Couchdb operation failed after 10 attempts"); + + // We expect 10 backoff attempts, so the time would have advanced by 10 times the backoff time, which is a constant in this test of 1ms + assertThat(mockTimeService.now()).as("time passed, procesor waited when it should not have done so.").isEqualTo(Instant.EPOCH.plusMillis(attemptsBeforeGivingUp)); + + assertThat(mockLogFactory.toString()).as("retry processor didn't log what we expected. ").contains("Couchdb operation failed after 10 attempts","due to conflicts."); + } + + @Test + public void testOperationThatPassesAfterFailureDoesNotThrowError() throws Exception { + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH); + MockLogFactory mockLogFactory = new MockLogFactory(); + RetryableCouchdbUpdateOperationProcessor processor = new RetryableCouchdbUpdateOperationProcessor(mockTimeService, mockLogFactory); + int attemptsBeforeGivingUp = 10; + BackoffTimeCalculator backoffTimeCalculator = new BackoffTimeCalculator() { + public long getBackoffDelayMillis() { + return 1; + } + }; + + RetryableCouchdbUpdateOperation operationThatPassesAfterFailure = new RetryableCouchdbUpdateOperation() { + private boolean hasTriedUpdating = false; + + @Override + public void tryToUpdateCouchDb() throws CouchdbException, CouchdbClashingUpdateException { + if (!hasTriedUpdating) { + hasTriedUpdating = true; + throw new CouchdbClashingUpdateException("simulating constant failures"); + } + } + }; + + // When... + processor.retryCouchDbUpdateOperation(operationThatPassesAfterFailure, attemptsBeforeGivingUp, backoffTimeCalculator); + + // Then + // No errors should have been thrown + } + + @Test + public void testDefaultBackoffTimeCalculatorGivesNumbersWithinExpectedRange() throws Exception { + BackoffTimeCalculator backoffTimeCalculator = new BackoffTimeCalculator(){}; + for(int i=0; i<100; i++) { + long millis = backoffTimeCalculator.getBackoffDelayMillis(); + assertThat(millis).isGreaterThan(1000L); + assertThat(millis).isLessThanOrEqualTo(4000L); + } + } + + @Test + public void testWaitForBackOffDelayLogsInterruptedException() throws Exception { + MockTimeService mockTimeService = new MockTimeService(Instant.EPOCH){ + @Override + public void sleepMillis(long millis) throws InterruptedException { + // Simulate InterruptedException + throw new InterruptedException(); + } + }; + MockLogFactory mockLogFactory = new MockLogFactory(); + + // A backoff of 1ms each time, so there is no random element in a unit test, and we can compare the time delayed later. + BackoffTimeCalculator backoffTimeCalculator = new BackoffTimeCalculator() {}; + + RetryableCouchdbUpdateOperationProcessor processor = new RetryableCouchdbUpdateOperationProcessor(mockTimeService, mockLogFactory); + + // When... + processor.waitForBackoffDelay(mockTimeService, backoffTimeCalculator); + + // Then + assertThat(mockLogFactory.toString()).as("retry processor didn't log what we expected. ").contains("Interrupted from waiting during a back-off delay. Ignoring this, but cutting our wait short."); + } +} diff --git a/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/MockTimeService.java b/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/MockTimeService.java index 68eb175e..877cf151 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/MockTimeService.java +++ b/galasa-extensions-parent/dev.galasa.extensions.mocks/src/main/java/dev/galasa/extensions/mocks/MockTimeService.java @@ -32,9 +32,4 @@ public void setCurrentTime(Instant currentTime) { this.currentTime = currentTime; } - @Override - public void sleepMillis(long millisToSleep) throws InterruptedException { - // Pretend we are sleeping, so the current time advances. - setCurrentTime(currentTime.plusMillis(millisToSleep)); - } } diff --git a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java index 4d18d26b..7084dabc 100644 --- a/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java +++ b/galasa-extensions-parent/dev.galasa.ras.couchdb/src/main/java/dev/galasa/ras/couchdb/internal/CouchdbValidatorImpl.java @@ -45,6 +45,7 @@ public class CouchdbValidatorImpl implements CouchdbValidator { private static final CouchDbVersion minCouchDbVersion = new CouchDbVersion(3,3,3); + @Override public void checkCouchdbDatabaseIsValid( URI rasUri, CloseableHttpClient httpClient ,