From 2228cfc6a1302ddefd46bc76909f6e0d8b71dbdc Mon Sep 17 00:00:00 2001 From: Graham Packer Date: Fri, 3 Jan 2025 18:12:22 +0000 Subject: [PATCH 01/12] Add ADR counter, get ADRs and create ADR endpoints (#716) --- calm-hub/mongo/init-mongo.js | 11 ++ calm-hub/pom.xml | 26 +++ .../java/integration/MongoAdrIntegration.java | 81 ++++++++ .../MongoArchitectureIntegration.java | 1 - .../java/integration/MongoSetup.java | 2 + .../main/java/org/finos/calm/domain/Adr.java | 9 + .../org/finos/calm/resources/AdrResource.java | 97 ++++++++++ .../java/org/finos/calm/store/AdrStore.java | 13 ++ .../finos/calm/store/mongo/MongoAdrStore.java | 77 ++++++++ .../calm/store/mongo/MongoCounterStore.java | 4 + .../calm/resources/TestAdrResourceShould.java | 141 ++++++++++++++ .../store/mongo/TestMongoAdrStoreShould.java | 175 ++++++++++++++++++ 12 files changed, 636 insertions(+), 1 deletion(-) create mode 100644 calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/Adr.java create mode 100644 calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java create mode 100644 calm-hub/src/main/java/org/finos/calm/store/AdrStore.java create mode 100644 calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java create mode 100644 calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java create mode 100644 calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java diff --git a/calm-hub/mongo/init-mongo.js b/calm-hub/mongo/init-mongo.js index de4f17517..ad3611072 100644 --- a/calm-hub/mongo/init-mongo.js +++ b/calm-hub/mongo/init-mongo.js @@ -21,6 +21,17 @@ if (db.counters.countDocuments({ _id: "architectureStoreCounter" }) === 1) { print("architectureStoreCounter already exists, no initialization needed"); } +if (db.counters.countDocuments({ _id: "adrStoreCounter" }) === 1) { + db.counters.insertOne({ + _id: "adrStoreCounter", + sequence_value: 1 + }); + print("Initialized adrStoreCounter with sequence_value 1"); +} else { + print("adrStoreCounter already exists, no initialization needed"); +} + + db.schemas.insertMany([ // Insert initial documents into the schemas collection { version: "2024-10", diff --git a/calm-hub/pom.xml b/calm-hub/pom.xml index a95f1db59..fe50309ba 100644 --- a/calm-hub/pom.xml +++ b/calm-hub/pom.xml @@ -44,6 +44,12 @@ io.quarkus quarkus-mongodb-client + + io.soabase.record-builder + record-builder-core + 44 + provided + @@ -165,6 +171,11 @@ org.jacoco jacoco-maven-plugin 0.8.12 + + + **/*Builder.* + + default-prepare-agent @@ -200,6 +211,21 @@ + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.soabase.record-builder + record-builder-processor + 44 + + + + diff --git a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java new file mode 100644 index 000000000..88b58e82e --- /dev/null +++ b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java @@ -0,0 +1,81 @@ +package integration; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import org.bson.Document; +import org.eclipse.microprofile.config.ConfigProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; + +import static integration.MongoSetup.counterSetup; +import static integration.MongoSetup.namespaceSetup; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +@QuarkusTest +@TestProfile(IntegrationTestProfile.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MongoAdrIntegration { + + private static final Logger logger = LoggerFactory.getLogger(MongoAdrIntegration.class); + public static final String ADR = "{\"name\": \"test-adr\"}"; + + + @BeforeEach + public void setupAdrs() { + String mongoUri = ConfigProvider.getConfig().getValue("quarkus.mongodb.connection-string", String.class); + + // Safeguard: Fail fast if URI is not set + if(mongoUri == null || mongoUri.isBlank()) { + logger.error("MongoDB URI is not set. Check the EndToEndResource configuration."); + throw new IllegalStateException("MongoDB URI is not set. Check the EndToEndResource configuration."); + } + + try(MongoClient mongoClient = MongoClients.create(mongoUri)) { + MongoDatabase database = mongoClient.getDatabase("calmSchemas"); + + if(!database.listCollectionNames().into(new ArrayList<>()).contains("adrs")) { + database.createCollection("adrs"); + database.getCollection("adrs").insertOne( + new Document("namespace", "finos").append("adrs", new ArrayList<>()) + ); + } + + counterSetup(database); + namespaceSetup(database); + } + } + + @Test + @Order(1) + void end_to_end_get_with_no_architecture() { + given() + .when().get("/calm/namespaces/finos/adrs") + .then() + .statusCode(200) + .body("values", empty()); + } + + @Test + @Order(2) + void end_to_end_create_an_adr() { + given() + .body(ADR) + .header("Content-Type", "application/json") + .when().post("/calm/namespaces/finos/adrs") + .then() + .statusCode(201) + .header("Location", containsString("calm/namespaces/finos/adrs/1")); + } + +} \ No newline at end of file diff --git a/calm-hub/src/integration-test/java/integration/MongoArchitectureIntegration.java b/calm-hub/src/integration-test/java/integration/MongoArchitectureIntegration.java index c425c6d12..5420d0f6e 100644 --- a/calm-hub/src/integration-test/java/integration/MongoArchitectureIntegration.java +++ b/calm-hub/src/integration-test/java/integration/MongoArchitectureIntegration.java @@ -3,7 +3,6 @@ import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.MongoDatabase; -import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; import org.bson.Document; diff --git a/calm-hub/src/integration-test/java/integration/MongoSetup.java b/calm-hub/src/integration-test/java/integration/MongoSetup.java index b4b2ee0e8..3a5402ee0 100644 --- a/calm-hub/src/integration-test/java/integration/MongoSetup.java +++ b/calm-hub/src/integration-test/java/integration/MongoSetup.java @@ -25,8 +25,10 @@ public static void counterSetup(MongoDatabase database) { database.createCollection("counters"); Document patternStoreCounter = new Document("_id", "patternStoreCounter").append("sequence_value", 0); Document architectureStoreCounter = new Document("_id", "architectureStoreCounter").append("sequence_value", 0); + Document adrStoreCounter = new Document("_id", "adrStoreCounter").append("sequence_value", 0); database.getCollection("counters").insertOne(patternStoreCounter); database.getCollection("counters").insertOne(architectureStoreCounter); + database.getCollection("counters").insertOne(adrStoreCounter); } } } diff --git a/calm-hub/src/main/java/org/finos/calm/domain/Adr.java b/calm-hub/src/main/java/org/finos/calm/domain/Adr.java new file mode 100644 index 000000000..1faa3dd5b --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/domain/Adr.java @@ -0,0 +1,9 @@ +package org.finos.calm.domain; + +import io.soabase.recordbuilder.core.RecordBuilder; + +@RecordBuilder.Options(enableWither = false) +@RecordBuilder +public record Adr(String namespace, int id, int revision, String adr) { + +} diff --git a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java new file mode 100644 index 000000000..d5b46561b --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java @@ -0,0 +1,97 @@ +package org.finos.calm.resources; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.bson.json.JsonParseException; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.finos.calm.domain.Adr; +import org.finos.calm.domain.AdrBuilder; +import org.finos.calm.domain.Architecture; +import org.finos.calm.domain.ValueWrapper; +import org.finos.calm.domain.exception.NamespaceNotFoundException; +import org.finos.calm.store.AdrStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.net.URISyntaxException; + +/** + * Resource for managing ADRs in a given namespace + */ +@Path("/calm/namespaces") +public class AdrResource { + + private final AdrStore store; + private final Logger logger = LoggerFactory.getLogger(AdrResource.class); + + public AdrResource(AdrStore store) { + this.store = store; + } + + /** + * Retrieve a list of ADRs in a given namespace + * @param namespace the namespace to retrieve ADRs for + * @return a list of ADRs in the given namespace + */ + @GET + @Path("{namespace}/adrs") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Retrieve ADRs in a given namespace", + description = "ADRs stored in a given namespace" + ) + public Response getAdrsForNamespace(@PathParam("namespace") String namespace) { + try { + return Response.ok(new ValueWrapper<>(store.getAdrsForNamespace(namespace))).build(); + } catch (NamespaceNotFoundException e) { + logger.error("Invalid namespace [{}] when retrieving ADRs", namespace, e); + return invalidNamespaceResponse(namespace); + } + } + + + @POST + @Path("{namespace}/adrs") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Create ADR for namespace", + description = "Creates an ADR for a given namespace with an allocated ID and revision 1" + ) + public Response createAdrForNamespace(@PathParam("namespace") String namespace, String adrJson) throws URISyntaxException { + Adr adr = AdrBuilder.builder() + .namespace(namespace) + .revision(1) + .adr(adrJson) + .build(); + + try { + return adrWithLocationResponse(store.createAdrForNamespace(adr)); + } catch (NamespaceNotFoundException e) { + logger.error("Invalid namespace [{}] when creating ADR", namespace, e); + return invalidNamespaceResponse(namespace); + } catch (JsonParseException e) { + logger.error("Cannot parse Architecture JSON for namespace [{}]. Architecture JSON : [{}]", namespace, adrJson, e); + return invalidAdrJsonResponse(namespace); + } + } + + private Response adrWithLocationResponse(Adr adr) throws URISyntaxException { + return Response.created(new URI("/calm/namespaces/" + adr.namespace() + "/adrs/" + adr.id() + "/revisions/" + adr.revision())).build(); + } + + private Response invalidNamespaceResponse(String namespace) { + return Response.status(Response.Status.NOT_FOUND).entity("Invalid namespace provided: " + namespace).build(); + } + + private Response invalidAdrJsonResponse(String adrJson) { + return Response.status(Response.Status.BAD_REQUEST).entity("The ADR JSON could not be parsed: " + adrJson).build(); + } +} diff --git a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java new file mode 100644 index 000000000..54090f39b --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java @@ -0,0 +1,13 @@ +package org.finos.calm.store; + +import org.finos.calm.domain.Adr; +import org.finos.calm.domain.Architecture; +import org.finos.calm.domain.exception.NamespaceNotFoundException; + +import java.util.List; + +public interface AdrStore { + + List getAdrsForNamespace(String namespace) throws NamespaceNotFoundException; + Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException; +} diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java new file mode 100644 index 000000000..3618379fa --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java @@ -0,0 +1,77 @@ +package org.finos.calm.store.mongo; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.Updates; +import jakarta.enterprise.context.ApplicationScoped; +import org.bson.Document; +import org.finos.calm.domain.Adr; +import org.finos.calm.domain.AdrBuilder; +import org.finos.calm.domain.Architecture; +import org.finos.calm.domain.exception.NamespaceNotFoundException; +import org.finos.calm.store.AdrStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +@ApplicationScoped +public class MongoAdrStore implements AdrStore { + + private final MongoCounterStore counterStore; + private final MongoNamespaceStore namespaceStore; + private final MongoCollection adrCollection; + private final Logger log = LoggerFactory.getLogger(getClass()); + + public MongoAdrStore(MongoClient mongoClient, MongoCounterStore counterStore, MongoNamespaceStore namespaceStore) { + this.counterStore = counterStore; + this.namespaceStore = namespaceStore; + MongoDatabase database = mongoClient.getDatabase("calmSchemas"); + this.adrCollection = database.getCollection("adrs"); + } + + @Override + public List getAdrsForNamespace(String namespace) throws NamespaceNotFoundException { + if(!namespaceStore.namespaceExists(namespace)) { + throw new NamespaceNotFoundException(); + } + + Document namespaceDocument = adrCollection.find(Filters.eq("namespace", namespace)).first(); + + //protects from an unpopulated mongo collection + if(namespaceDocument == null || namespaceDocument.isEmpty()) { + return List.of(); + } + + List patterns = namespaceDocument.getList("adrs", Document.class); + List adrIds = new ArrayList<>(); + + for (Document pattern : patterns) { + adrIds.add(pattern.getInteger("adrId")); + } + + return adrIds; + } + + @Override + public Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException { + if(!namespaceStore.namespaceExists(adr.namespace())) { + throw new NamespaceNotFoundException(); + } + + int id = counterStore.getNextAdrSequenceValue(); + Document adrDocument = new Document("adrId", id).append("revisions", + new Document(String.valueOf(adr.revision()), Document.parse(adr.adr()))); + + adrCollection.updateOne( + Filters.eq("namespace", adr.namespace()), + Updates.push("adrs", adrDocument), + new UpdateOptions().upsert(true)); + + return AdrBuilder.builder(adr).id(id).build(); + } +} diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoCounterStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoCounterStore.java index 4014c43e1..f7f7e758e 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoCounterStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoCounterStore.java @@ -26,6 +26,10 @@ public int getNextArchitectureSequenceValue() { return nextValueForCounter("architectureStoreCounter"); } + public int getNextAdrSequenceValue() { + return nextValueForCounter("adrStoreCounter"); + } + private int nextValueForCounter(String counterId) { Document filter = new Document("_id", counterId); Document update = new Document("$inc", new Document("sequence_value", 1)); diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java new file mode 100644 index 000000000..0ea86ae06 --- /dev/null +++ b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java @@ -0,0 +1,141 @@ +package org.finos.calm.resources; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import org.bson.json.JsonParseException; +import org.finos.calm.domain.Adr; +import org.finos.calm.domain.AdrBuilder; +import org.finos.calm.domain.Architecture; +import org.finos.calm.domain.exception.NamespaceNotFoundException; +import org.finos.calm.store.AdrStore; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@QuarkusTest +@ExtendWith(MockitoExtension.class) +public class TestAdrResourceShould { + + @InjectMock + AdrStore mockAdrStore; + + @Test + void return_a_404_when_an_invalid_namespace_is_provided_on_get_adrs() throws NamespaceNotFoundException { + when(mockAdrStore.getAdrsForNamespace(anyString())).thenThrow(new NamespaceNotFoundException()); + + given() + .when() + .get("/calm/namespaces/invalid/adrs") + .then() + .statusCode(404); + + verify(mockAdrStore, times(1)).getAdrsForNamespace("invalid"); + } + + @Test + void return_list_of_adr_ids_when_valid_namespace_provided_on_get_adrs() throws NamespaceNotFoundException { + when(mockAdrStore.getAdrsForNamespace(anyString())).thenReturn(Arrays.asList(12345,54321)); + + given() + .when() + .get("/calm/namespaces/finos/adrs") + .then() + .statusCode(200) + .body(equalTo("{\"values\":[12345,54321]}")); + + verify(mockAdrStore, times(1)).getAdrsForNamespace("finos"); + } + + @Test + void return_a_404_when_invalid_namespace_is_provided_on_create_adr() throws NamespaceNotFoundException { + when(mockAdrStore.createAdrForNamespace(any(Adr.class))) + .thenThrow(new NamespaceNotFoundException()); + + String adr = "{ \"test\": \"json\" }"; + + given() + .header("Content-Type", "application/json") + .body(adr) + .when() + .post("/calm/namespaces/invalid/adrs") + .then() + .statusCode(404); + + Adr expectedAdr = AdrBuilder.builder() + .adr(adr) + .revision(1) + .namespace("invalid") + .build(); + + verify(mockAdrStore, times(1)).createAdrForNamespace(expectedAdr); + } + + @Test + void return_a_400_when_invalid_adr_json_is_provided_on_create_adr() throws NamespaceNotFoundException { + when(mockAdrStore.createAdrForNamespace(any(Adr.class))) + .thenThrow(new JsonParseException()); + + String adr = "{ \"test\": im invalid json"; + + given() + .header("Content-Type", "application/json") + .body(adr) + .when() + .post("/calm/namespaces/invalid/adrs") + .then() + .statusCode(400); + + Adr expectedAdr = AdrBuilder.builder() + .adr(adr) + .namespace("invalid") + .revision(1) + .build(); + + verify(mockAdrStore, times(1)).createAdrForNamespace(expectedAdr); + } + + @Test + void return_a_created_with_location_of_adr_when_creating_adr() throws NamespaceNotFoundException { + String adrJson = "{ \"test\": \"json\" }"; + String namespace = "finos"; + + Adr stubbedReturnAdr = AdrBuilder.builder() + .adr(adrJson) + .revision(1) + .id(12) + .namespace(namespace) + .build(); + + when(mockAdrStore.createAdrForNamespace(any(Adr.class))).thenReturn(stubbedReturnAdr); + + given() + .header("Content-Type", "application/json") + .body(adrJson) + .when() + .post("/calm/namespaces/finos/adrs") + .then() + .statusCode(201) + //Derived from stubbed architecture in resource + .header("Location", containsString("/calm/namespaces/finos/adrs/12/revisions/1")); + + Adr expectedAdrToCreate = AdrBuilder.builder() + .adr(adrJson) + .namespace(namespace) + .revision(1) + .build(); + + verify(mockAdrStore, times(1)).createAdrForNamespace(expectedAdrToCreate); + } + +} diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java new file mode 100644 index 000000000..c8d22e842 --- /dev/null +++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java @@ -0,0 +1,175 @@ +package org.finos.calm.store.mongo; + +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.Updates; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import org.bson.Document; +import org.bson.json.JsonParseException; +import org.finos.calm.domain.Adr; +import org.finos.calm.domain.AdrBuilder; +import org.finos.calm.domain.Architecture; +import org.finos.calm.domain.exception.NamespaceNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@QuarkusTest +public class TestMongoAdrStoreShould { + + @InjectMock + MongoClient mongoClient; + + @InjectMock + MongoCounterStore counterStore; + + @InjectMock + MongoNamespaceStore namespaceStore; + + private MongoDatabase mongoDatabase; + private MongoCollection adrCollection; + private MongoAdrStore mongoAdrStore; + private final String NAMESPACE = "finos"; + + private final String validJson = "{\"test\": \"test\"}"; + + @BeforeEach + void setup() { + mongoDatabase = Mockito.mock(MongoDatabase.class); + adrCollection = Mockito.mock(MongoCollection.class); + + when(mongoClient.getDatabase("calmSchemas")).thenReturn(mongoDatabase); + when(mongoDatabase.getCollection("adrs")).thenReturn(adrCollection); + mongoAdrStore = new MongoAdrStore(mongoClient, counterStore, namespaceStore); + } + + @Test + void get_adrs_for_namespace_that_doesnt_exist_throws_exception() { + when(namespaceStore.namespaceExists(anyString())).thenReturn(false); + String namespace = "does-not-exist"; + + assertThrows(NamespaceNotFoundException.class, + () -> mongoAdrStore.getAdrsForNamespace(namespace)); + + verify(namespaceStore).namespaceExists(namespace); + } + + @Test + void get_adrs_for_namespace_returns_empty_list_when_none_exist() throws NamespaceNotFoundException { + FindIterable findIterable = Mockito.mock(FindIterable.class); + when(namespaceStore.namespaceExists(anyString())).thenReturn(true); + when(adrCollection.find(eq(Filters.eq("namespace", NAMESPACE)))) + .thenReturn(findIterable); + Document documentMock = Mockito.mock(Document.class); + when(findIterable.first()).thenReturn(documentMock); + when(documentMock.getList("adrs", Document.class)) + .thenReturn(new ArrayList<>()); + + assertThat(mongoAdrStore.getAdrsForNamespace(NAMESPACE), is(empty())); + verify(namespaceStore).namespaceExists(NAMESPACE); + } + + @Test + void get_adrs_for_namespace_returns_empty_list_when_mongo_collection_not_created() throws NamespaceNotFoundException { + FindIterable findIterable = Mockito.mock(FindIterable.class); + when(namespaceStore.namespaceExists(anyString())).thenReturn(true); + when(adrCollection.find(eq(Filters.eq("namespace", NAMESPACE)))) + .thenReturn(findIterable); + when(findIterable.first()).thenReturn(null); + + assertThat(mongoAdrStore.getAdrsForNamespace(NAMESPACE), is(empty())); + verify(namespaceStore).namespaceExists(NAMESPACE); + } + + @Test + void get_adrs_for_namespace_returns_values() throws NamespaceNotFoundException { + FindIterable findIterable = Mockito.mock(FindIterable.class); + when(namespaceStore.namespaceExists(anyString())).thenReturn(true); + when(adrCollection.find(eq(Filters.eq("namespace", NAMESPACE)))) + .thenReturn(findIterable); + Document documentMock = Mockito.mock(Document.class); + when(findIterable.first()).thenReturn(documentMock); + + Document doc1 = new Document("adrId", 1001); + Document doc2 = new Document("adrId", 1002); + + when(documentMock.getList("adrs", Document.class)) + .thenReturn(Arrays.asList(doc1, doc2)); + + List architectureIds = mongoAdrStore.getAdrsForNamespace(NAMESPACE); + + assertThat(architectureIds, is(Arrays.asList(1001, 1002))); + verify(namespaceStore).namespaceExists(NAMESPACE); + } + + @Test + void return_a_namespace_exception_when_namespace_does_not_exist_when_creating_an_adr() { + when(namespaceStore.namespaceExists(anyString())).thenReturn(false); + String namespace = "does-not-exist"; + Adr adr = AdrBuilder.builder().namespace(namespace).build(); + + assertThrows(NamespaceNotFoundException.class, + () -> mongoAdrStore.createAdrForNamespace(adr)); + verify(namespaceStore).namespaceExists(namespace); + } + + @Test + void return_a_json_parse_exception_when_an_invalid_json_object_is_presented_when_creating_an_adr() { + when(namespaceStore.namespaceExists(anyString())).thenReturn(true); + when(counterStore.getNextArchitectureSequenceValue()).thenReturn(42); + Adr adr = AdrBuilder.builder().namespace(NAMESPACE) + .adr("Invalid JSON") + .build(); + + assertThrows(JsonParseException.class, + () -> mongoAdrStore.createAdrForNamespace(adr)); + } + + @Test + void return_created_adr_when_parameters_are_valid() throws NamespaceNotFoundException { + String validNamespace = NAMESPACE; + int sequenceNumber = 42; + when(namespaceStore.namespaceExists(anyString())).thenReturn(true); + when(counterStore.getNextAdrSequenceValue()).thenReturn(sequenceNumber); + Adr adrToCreate = AdrBuilder.builder().adr(validJson) + .namespace(validNamespace) + .revision(1) + .build(); + + Adr adr = mongoAdrStore.createAdrForNamespace(adrToCreate); + + Adr expectedAdr = AdrBuilder.builder().adr(validJson) + .namespace(validNamespace) + .revision(1) + .id(sequenceNumber) + .build(); + + assertThat(adr, is(expectedAdr)); + Document expectedDoc = new Document("adrId", adr.id()).append("revisions", + new Document("1", Document.parse(adr.adr()))); + + verify(adrCollection).updateOne( + eq(Filters.eq("namespace", validNamespace)), + eq(Updates.push("adrs", expectedDoc)), + any(UpdateOptions.class)); + } +} From 5e5502cd9a138b6e7a8d911c01fa4351b5e70614 Mon Sep 17 00:00:00 2001 From: Graham Packer Date: Fri, 3 Jan 2025 20:23:48 +0000 Subject: [PATCH 02/12] Add get ADR revisions endpoint (#716) --- .../java/integration/MongoAdrIntegration.java | 11 +++ .../exception/AdrNotFoundException.java | 7 ++ .../org/finos/calm/resources/AdrResource.java | 34 ++++++++- .../java/org/finos/calm/store/AdrStore.java | 4 + .../finos/calm/store/mongo/MongoAdrStore.java | 40 ++++++++++ .../calm/resources/TestAdrResourceShould.java | 53 +++++++++++++ .../store/mongo/TestMongoAdrStoreShould.java | 74 +++++++++++++++++++ 7 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/exception/AdrNotFoundException.java diff --git a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java index 88b58e82e..36cd71019 100644 --- a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java +++ b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java @@ -78,4 +78,15 @@ void end_to_end_create_an_adr() { .header("Location", containsString("calm/namespaces/finos/adrs/1")); } + @Test + @Order(3) + void end_to_end_verify_revisions() { + given() + .when().get("/calm/namespaces/finos/adrs/1/revisions") + .then() + .statusCode(200) + .body("values", hasSize(1)) + .body("values[0]", equalTo(1)); + } + } \ No newline at end of file diff --git a/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrNotFoundException.java b/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrNotFoundException.java new file mode 100644 index 000000000..a157439a8 --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrNotFoundException.java @@ -0,0 +1,7 @@ +package org.finos.calm.domain.exception; + +/** + * Exception thrown when the specified ADR is not found. + */ +public class AdrNotFoundException extends Exception { +} diff --git a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java index d5b46561b..e99c6e429 100644 --- a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java +++ b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java @@ -12,8 +12,8 @@ import org.eclipse.microprofile.openapi.annotations.Operation; import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; -import org.finos.calm.domain.Architecture; import org.finos.calm.domain.ValueWrapper; +import org.finos.calm.domain.exception.AdrNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; import org.slf4j.Logger; @@ -78,11 +78,37 @@ public Response createAdrForNamespace(@PathParam("namespace") String namespace, logger.error("Invalid namespace [{}] when creating ADR", namespace, e); return invalidNamespaceResponse(namespace); } catch (JsonParseException e) { - logger.error("Cannot parse Architecture JSON for namespace [{}]. Architecture JSON : [{}]", namespace, adrJson, e); + logger.error("Cannot parse ADR JSON for namespace [{}]. ADR JSON : [{}]", namespace, adrJson, e); return invalidAdrJsonResponse(namespace); } } + + @GET + @Path("{namespace}/adrs/{adrId}/revisions") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Retrieve a list of revisions for a given ADR", + description = "The most recent revision is the canonical ADR, with others available for audit or exploring changes." + ) + public Response getAdrRevisions(@PathParam("namespace") String namespace, @PathParam("adrId") int adrId) { + Adr adr = AdrBuilder.builder() + .namespace(namespace) + .id(adrId) + .build(); + + try { + return Response.ok(new ValueWrapper<>(store.getAdrRevisions(adr))).build(); + } catch (NamespaceNotFoundException e) { + logger.error("Invalid namespace [{}] when getting revisions of ADR", adr, e); + return invalidNamespaceResponse(namespace); + } catch (AdrNotFoundException e) { + logger.error("Invalid ADR [{}] when getting versions of ADR", adr, e); + return invalidAdrResponse(adrId); + } + } + + private Response adrWithLocationResponse(Adr adr) throws URISyntaxException { return Response.created(new URI("/calm/namespaces/" + adr.namespace() + "/adrs/" + adr.id() + "/revisions/" + adr.revision())).build(); } @@ -94,4 +120,8 @@ private Response invalidNamespaceResponse(String namespace) { private Response invalidAdrJsonResponse(String adrJson) { return Response.status(Response.Status.BAD_REQUEST).entity("The ADR JSON could not be parsed: " + adrJson).build(); } + + private Response invalidAdrResponse(int adrId) { + return Response.status(Response.Status.NOT_FOUND).entity("Invalid adrId provided: " + adrId).build(); + } } diff --git a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java index 54090f39b..7c0c518a2 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java @@ -2,6 +2,8 @@ import org.finos.calm.domain.Adr; import org.finos.calm.domain.Architecture; +import org.finos.calm.domain.exception.AdrNotFoundException; +import org.finos.calm.domain.exception.ArchitectureNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import java.util.List; @@ -10,4 +12,6 @@ public interface AdrStore { List getAdrsForNamespace(String namespace) throws NamespaceNotFoundException; Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException; + List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException; + } diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java index 3618379fa..a150c0d52 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java @@ -4,13 +4,17 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; import com.mongodb.client.model.Filters; +import com.mongodb.client.model.Projections; import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.model.Updates; import jakarta.enterprise.context.ApplicationScoped; import org.bson.Document; +import org.bson.conversions.Bson; import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; import org.finos.calm.domain.Architecture; +import org.finos.calm.domain.exception.AdrNotFoundException; +import org.finos.calm.domain.exception.ArchitectureNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; import org.slf4j.Logger; @@ -18,6 +22,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; @ApplicationScoped public class MongoAdrStore implements AdrStore { @@ -74,4 +79,39 @@ public Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException { return AdrBuilder.builder(adr).id(id).build(); } + + @Override + public List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException { + Document result = retrieveAdrRevisions(adr); + + List adrs = (List) result.get("adrs"); + for (Document adrDoc : adrs) { + if (adr.id() == adrDoc.getInteger("adrId")) { + // Extract the revisions map from the matching pattern + Document revisions = (Document) adrDoc.get("revisions"); + Set revisionKeys = revisions.keySet(); + + return revisionKeys.stream().map(Integer::parseInt).toList(); + } + } + + throw new AdrNotFoundException(); + } + + private Document retrieveAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException { + if(!namespaceStore.namespaceExists(adr.namespace())) { + throw new NamespaceNotFoundException(); + } + + Bson filter = new Document("namespace", adr.namespace()); + Bson projection = Projections.fields(Projections.include("adrs")); + + Document result = adrCollection.find(filter).projection(projection).first(); + + if (result == null) { + throw new AdrNotFoundException(); + } + + return result; + } } diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java index 0ea86ae06..33d54dcf2 100644 --- a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java +++ b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java @@ -6,13 +6,20 @@ import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; import org.finos.calm.domain.Architecture; +import org.finos.calm.domain.exception.AdrNotFoundException; +import org.finos.calm.domain.exception.ArchitectureNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.containsString; @@ -138,4 +145,50 @@ void return_a_created_with_location_of_adr_when_creating_adr() throws NamespaceN verify(mockAdrStore, times(1)).createAdrForNamespace(expectedAdrToCreate); } + static Stream provideParametersForAdrVersionTests() { + return Stream.of( + Arguments.of("invalid", new NamespaceNotFoundException(), 404), + Arguments.of("valid", new AdrNotFoundException(), 404), + Arguments.of("valid", null, 200) + ); + } + + @ParameterizedTest + @MethodSource("provideParametersForAdrVersionTests") + void respond_correctly_to_get_adr_versions_query(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException { + var revisions = List.of(1, 2); + if (exceptionToThrow != null) { + when(mockAdrStore.getAdrRevisions(any(Adr.class))).thenThrow(exceptionToThrow); + } else { + when(mockAdrStore.getAdrRevisions(any(Adr.class))).thenReturn(revisions); + } + + if (expectedStatusCode == 200 ) { + String expectedBody = "{\"values\":[1,2]}"; + given() + .when() + .get("/calm/namespaces/" + namespace + "/adrs/12/revisions") + .then() + .statusCode(expectedStatusCode) + .body(equalTo(expectedBody)); + } else { + given() + .when() + .get("/calm/namespaces/" + namespace + "/adrs/12/revisions") + .then() + .statusCode(expectedStatusCode); + } + + verifyExpectedAdrForRevisions(namespace); + } + + private void verifyExpectedAdrForRevisions(String namespace) throws NamespaceNotFoundException, AdrNotFoundException { + Adr expectedAdrToRetrieve = AdrBuilder.builder() + .namespace(namespace) + .id(12) + .build(); + + verify(mockAdrStore, times(1)).getAdrRevisions(expectedAdrToRetrieve); + } + } diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java index c8d22e842..05e465525 100644 --- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java +++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java @@ -5,15 +5,19 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; import com.mongodb.client.model.Filters; +import com.mongodb.client.model.Projections; import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.model.Updates; import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; import org.bson.Document; +import org.bson.conversions.Bson; import org.bson.json.JsonParseException; import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; import org.finos.calm.domain.Architecture; +import org.finos.calm.domain.exception.AdrNotFoundException; +import org.finos.calm.domain.exception.ArchitectureNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -21,7 +25,9 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; @@ -172,4 +178,72 @@ void return_created_adr_when_parameters_are_valid() throws NamespaceNotFoundExce eq(Updates.push("adrs", expectedDoc)), any(UpdateOptions.class)); } + + @Test + void get_adr_revisions_for_invalid_namespace_throws_exception() { + when(namespaceStore.namespaceExists(anyString())).thenReturn(false); + Adr adr = AdrBuilder.builder().namespace("does-not-exist").build(); + + assertThrows(NamespaceNotFoundException.class, + () -> mongoAdrStore.getAdrRevisions(adr)); + + verify(namespaceStore).namespaceExists(adr.namespace()); + } + + private FindIterable setupInvalidAdr() { + FindIterable findIterable = Mockito.mock(FindIterable.class); + when(namespaceStore.namespaceExists(anyString())).thenReturn(true); + //Return the same find iterable as the projection unboxes, then return null + when(adrCollection.find(any(Bson.class))) + .thenReturn(findIterable); + when(findIterable.projection(any(Bson.class))).thenReturn(findIterable); + when(findIterable.first()).thenReturn(null); + + return findIterable; + } + + @Test + void get_adr_revisions_for_invalid_adr_throws_exception() { + FindIterable findIterable = setupInvalidAdr(); + Adr adr = AdrBuilder.builder().namespace(NAMESPACE).build(); + + assertThrows(AdrNotFoundException.class, + () -> mongoAdrStore.getAdrRevisions(adr)); + + verify(adrCollection).find(new Document("namespace", adr.namespace())); + verify(findIterable).projection(Projections.fields(Projections.include("adrs"))); + } + + @Test + void get_adr_revisions_for_valid_adr_returns_list_of_revisions() throws NamespaceNotFoundException, AdrNotFoundException { + mockSetupAdrDocumentWithRevisions(); + + Adr adr = AdrBuilder.builder().namespace(NAMESPACE).id(42).build(); + List adrRevisions = mongoAdrStore.getAdrRevisions(adr); + + assertThat(adrRevisions, is(List.of(1))); + } + + private void mockSetupAdrDocumentWithRevisions() { + Document mainDocument = setupAdrRevisionDocument(); + FindIterable findIterable = Mockito.mock(FindIterable.class); + when(namespaceStore.namespaceExists(anyString())).thenReturn(true); + when(adrCollection.find(any(Bson.class))) + .thenReturn(findIterable); + when(findIterable.projection(any(Bson.class))).thenReturn(findIterable); + when(findIterable.first()).thenReturn(mainDocument); + } + + private Document setupAdrRevisionDocument() { + //Set up an ADR document with 2 ADRs in (one with a valid revision) + Map revisionMap = new HashMap<>(); + revisionMap.put("1", Document.parse(validJson)); + Document targetStoredAdr = new Document("adrId", 42) + .append("revisions", new Document(revisionMap)); + + Document paddingAdr = new Document("adrId", 0); + + return new Document("namespace", NAMESPACE) + .append("adrs", Arrays.asList(paddingAdr, targetStoredAdr)); + } } From d3df270c9359e64e3efa7146a13234fb18814ae6 Mon Sep 17 00:00:00 2001 From: Graham Packer Date: Sat, 4 Jan 2025 14:48:14 +0000 Subject: [PATCH 03/12] Add get ADR Revision endpoint (#716) --- .../java/integration/MongoAdrIntegration.java | 10 ++++ .../AdrRevisionNotFoundException.java | 7 +++ .../org/finos/calm/resources/AdrResource.java | 39 +++++++++++- .../java/org/finos/calm/store/AdrStore.java | 3 + .../finos/calm/store/mongo/MongoAdrStore.java | 25 ++++++++ .../calm/resources/TestAdrResourceShould.java | 60 +++++++++++++++++-- .../store/mongo/TestMongoAdrStoreShould.java | 37 +++++++++++- 7 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/exception/AdrRevisionNotFoundException.java diff --git a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java index 36cd71019..dc1e21c0f 100644 --- a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java +++ b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java @@ -89,4 +89,14 @@ void end_to_end_verify_revisions() { .body("values[0]", equalTo(1)); } + @Test + @Order(4) + void end_to_end_verify_architecture() { + given() + .when().get("/calm/namespaces/finos/adrs/1/revisions/1") + .then() + .statusCode(200) + .body(equalTo(ADR)); + } + } \ No newline at end of file diff --git a/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrRevisionNotFoundException.java b/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrRevisionNotFoundException.java new file mode 100644 index 000000000..4f10f75c0 --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrRevisionNotFoundException.java @@ -0,0 +1,7 @@ +package org.finos.calm.domain.exception; + +/** + * Exception thrown when the specified ADR revision is not found. + */ +public class AdrRevisionNotFoundException extends Exception { +} diff --git a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java index e99c6e429..37b1b414a 100644 --- a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java +++ b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java @@ -12,8 +12,12 @@ import org.eclipse.microprofile.openapi.annotations.Operation; import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; +import org.finos.calm.domain.Architecture; import org.finos.calm.domain.ValueWrapper; import org.finos.calm.domain.exception.AdrNotFoundException; +import org.finos.calm.domain.exception.AdrRevisionNotFoundException; +import org.finos.calm.domain.exception.ArchitectureNotFoundException; +import org.finos.calm.domain.exception.ArchitectureVersionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; import org.slf4j.Logger; @@ -100,14 +104,41 @@ public Response getAdrRevisions(@PathParam("namespace") String namespace, @PathP try { return Response.ok(new ValueWrapper<>(store.getAdrRevisions(adr))).build(); } catch (NamespaceNotFoundException e) { - logger.error("Invalid namespace [{}] when getting revisions of ADR", adr, e); + logger.error("Invalid namespace [{}] when getting revisions of ADR", namespace, e); return invalidNamespaceResponse(namespace); } catch (AdrNotFoundException e) { - logger.error("Invalid ADR [{}] when getting versions of ADR", adr, e); + logger.error("Invalid ADR [{}] when getting versions of ADR", adrId, e); return invalidAdrResponse(adrId); } } + @GET + @Path("{namespace}/adrs/{adrId}/revisions/{revision}") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Retrieve a specific revision of an ADR", + description = "Retrieve a specific revision of an ADR" + ) + public Response getAdrRevision(@PathParam("namespace") String namespace, @PathParam("adrId") int adrId, @PathParam("revision") int revision) { + Adr adr = AdrBuilder.builder() + .namespace(namespace) + .id(adrId) + .revision(revision) + .build(); + + try { + return Response.ok(store.getAdrRevision(adr)).build(); + } catch (NamespaceNotFoundException e) { + logger.error("Invalid namespace [{}] when getting an ADR", namespace, e); + return invalidNamespaceResponse(namespace); + } catch (AdrNotFoundException e) { + logger.error("Invalid ADR [{}] when getting an ADR revision", adrId, e); + return invalidAdrResponse(adrId); + } catch (AdrRevisionNotFoundException e) { + logger.error("Invalid revision [{}] when getting an ADR", revision, e); + return invalidRevisionResponse(revision); + } + } private Response adrWithLocationResponse(Adr adr) throws URISyntaxException { return Response.created(new URI("/calm/namespaces/" + adr.namespace() + "/adrs/" + adr.id() + "/revisions/" + adr.revision())).build(); @@ -124,4 +155,8 @@ private Response invalidAdrJsonResponse(String adrJson) { private Response invalidAdrResponse(int adrId) { return Response.status(Response.Status.NOT_FOUND).entity("Invalid adrId provided: " + adrId).build(); } + + private Response invalidRevisionResponse(int revision) { + return Response.status(Response.Status.NOT_FOUND).entity("Invalid revision provided: " + revision).build(); + } } diff --git a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java index 7c0c518a2..e8ca4e94e 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java @@ -3,7 +3,9 @@ import org.finos.calm.domain.Adr; import org.finos.calm.domain.Architecture; import org.finos.calm.domain.exception.AdrNotFoundException; +import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.ArchitectureNotFoundException; +import org.finos.calm.domain.exception.ArchitectureVersionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import java.util.List; @@ -13,5 +15,6 @@ public interface AdrStore { List getAdrsForNamespace(String namespace) throws NamespaceNotFoundException; Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException; List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException; + String getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; } diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java index a150c0d52..682c3cbbd 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java @@ -14,7 +14,9 @@ import org.finos.calm.domain.AdrBuilder; import org.finos.calm.domain.Architecture; import org.finos.calm.domain.exception.AdrNotFoundException; +import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.ArchitectureNotFoundException; +import org.finos.calm.domain.exception.ArchitectureVersionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; import org.slf4j.Logger; @@ -98,6 +100,29 @@ public List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, throw new AdrNotFoundException(); } + @Override + public String getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + Document result = retrieveAdrRevisions(adr); + + List adrs = (List) result.get("adrs"); + for (Document adrDoc : adrs) { + if (adr.id() == adrDoc.getInteger("adrId")) { + // Retrieve the versions map from the matching pattern + Document revisions = (Document) adrDoc.get("revisions"); + + // Return the ADR JSON blob for the specified version + Document revisionDoc = (Document) revisions.get(String.valueOf(adr.revision())); + log.info("RevisionDoc: [{}], Revision: [{}]", adrDoc.get("revisions"), adr.revision()); + if(revisionDoc == null) { + throw new AdrRevisionNotFoundException(); + } + return revisionDoc.toJson(); + } + } + //ADR Revisions is empty, no version to find + throw new AdrRevisionNotFoundException(); + } + private Document retrieveAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException { if(!namespaceStore.namespaceExists(adr.namespace())) { throw new NamespaceNotFoundException(); diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java index 33d54dcf2..89901918f 100644 --- a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java +++ b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java @@ -7,7 +7,9 @@ import org.finos.calm.domain.AdrBuilder; import org.finos.calm.domain.Architecture; import org.finos.calm.domain.exception.AdrNotFoundException; +import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.ArchitectureNotFoundException; +import org.finos.calm.domain.exception.ArchitectureVersionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; import org.junit.jupiter.api.Test; @@ -145,7 +147,7 @@ void return_a_created_with_location_of_adr_when_creating_adr() throws NamespaceN verify(mockAdrStore, times(1)).createAdrForNamespace(expectedAdrToCreate); } - static Stream provideParametersForAdrVersionTests() { + static Stream provideParametersForAdrRevisionTests() { return Stream.of( Arguments.of("invalid", new NamespaceNotFoundException(), 404), Arguments.of("valid", new AdrNotFoundException(), 404), @@ -154,8 +156,8 @@ static Stream provideParametersForAdrVersionTests() { } @ParameterizedTest - @MethodSource("provideParametersForAdrVersionTests") - void respond_correctly_to_get_adr_versions_query(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException { + @MethodSource("provideParametersForAdrRevisionTests") + void respond_correctly_to_get_adr_revisions_query(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException { var revisions = List.of(1, 2); if (exceptionToThrow != null) { when(mockAdrStore.getAdrRevisions(any(Adr.class))).thenThrow(exceptionToThrow); @@ -179,10 +181,10 @@ void respond_correctly_to_get_adr_versions_query(String namespace, Throwable exc .statusCode(expectedStatusCode); } - verifyExpectedAdrForRevisions(namespace); + verifyExpectedAdrRevisions(namespace); } - private void verifyExpectedAdrForRevisions(String namespace) throws NamespaceNotFoundException, AdrNotFoundException { + private void verifyExpectedAdrRevisions(String namespace) throws NamespaceNotFoundException, AdrNotFoundException { Adr expectedAdrToRetrieve = AdrBuilder.builder() .namespace(namespace) .id(12) @@ -191,4 +193,52 @@ private void verifyExpectedAdrForRevisions(String namespace) throws NamespaceNot verify(mockAdrStore, times(1)).getAdrRevisions(expectedAdrToRetrieve); } + + static Stream provideParametersForGetAdrRevisionTests() { + return Stream.of( + Arguments.of("invalid", new NamespaceNotFoundException(), 404), + Arguments.of("valid", new AdrNotFoundException(), 404), + Arguments.of("valid", new AdrRevisionNotFoundException(), 404), + Arguments.of("valid", null, 200) + ); + } + + @ParameterizedTest + @MethodSource("provideParametersForGetAdrRevisionTests") + void respond_correctly_to_get_adr_revision(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + if (exceptionToThrow != null) { + when(mockAdrStore.getAdrRevision(any(Adr.class))).thenThrow(exceptionToThrow); + } else { + String adr = "{ \"test\": \"json\" }"; + when(mockAdrStore.getAdrRevision(any(Adr.class))).thenReturn(adr); + } + + if (expectedStatusCode == 200) { + given() + .when() + .get("/calm/namespaces/" + namespace + "/adrs/12/revisions/1") + .then() + .statusCode(expectedStatusCode) + .body(equalTo("{ \"test\": \"json\" }")); + } else { + given() + .when() + .get("/calm/namespaces/" + namespace + "/adrs/12/revisions/1") + .then() + .statusCode(expectedStatusCode); + } + + verifyExpectedGetAdrRevision(namespace); + } + + private void verifyExpectedGetAdrRevision(String namespace) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + Adr expectedAdrToRetrieve = AdrBuilder.builder() + .namespace(namespace) + .id(12) + .revision(1) + .build(); + + verify(mockAdrStore, times(1)).getAdrRevision(expectedAdrToRetrieve); + } + } diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java index 05e465525..8498eb3b5 100644 --- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java +++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java @@ -17,7 +17,8 @@ import org.finos.calm.domain.AdrBuilder; import org.finos.calm.domain.Architecture; import org.finos.calm.domain.exception.AdrNotFoundException; -import org.finos.calm.domain.exception.ArchitectureNotFoundException; +import org.finos.calm.domain.exception.AdrRevisionNotFoundException; +import org.finos.calm.domain.exception.ArchitectureVersionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -224,6 +225,29 @@ void get_adr_revisions_for_valid_adr_returns_list_of_revisions() throws Namespac assertThat(adrRevisions, is(List.of(1))); } + @Test + void throw_an_exception_for_an_invalid_adre_when_retrieving_adr_revision() { + FindIterable findIterable = setupInvalidAdr(); + Adr adr = AdrBuilder.builder().namespace(NAMESPACE).build(); + + assertThrows(AdrNotFoundException.class, + () -> mongoAdrStore.getAdrRevision(adr)); + + verify(adrCollection).find(new Document("namespace", adr.namespace())); + verify(findIterable).projection(Projections.fields(Projections.include("adrs"))); + } + + @Test + void return_an_adr_revision() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + mockSetupAdrDocumentWithRevisions(); + + Adr adr = AdrBuilder.builder().namespace(NAMESPACE) + .id(42).revision(1).build(); + + String adrRevision = mongoAdrStore.getAdrRevision(adr); + assertThat(adrRevision, is(validJson)); + } + private void mockSetupAdrDocumentWithRevisions() { Document mainDocument = setupAdrRevisionDocument(); FindIterable findIterable = Mockito.mock(FindIterable.class); @@ -246,4 +270,15 @@ private Document setupAdrRevisionDocument() { return new Document("namespace", NAMESPACE) .append("adrs", Arrays.asList(paddingAdr, targetStoredAdr)); } + + @Test + void throw_an_exception_when_revision_of_adr_does_not_exist() { + mockSetupAdrDocumentWithRevisions(); + + Adr adr = AdrBuilder.builder().namespace(NAMESPACE) + .id(42).revision(9).build(); + + assertThrows(AdrRevisionNotFoundException.class, + () -> mongoAdrStore.getAdrRevision(adr)); + } } From 601fd69986747fdaedef533ac515f47828b4dd2c Mon Sep 17 00:00:00 2001 From: Graham Packer Date: Sat, 4 Jan 2025 21:30:21 +0000 Subject: [PATCH 04/12] Add Get ADR endpoint (#716) --- .../java/integration/MongoAdrIntegration.java | 18 ++++- .../org/finos/calm/resources/AdrResource.java | 30 ++++++++ .../java/org/finos/calm/store/AdrStore.java | 1 + .../finos/calm/store/mongo/MongoAdrStore.java | 56 +++++++++++--- .../calm/resources/TestAdrResourceShould.java | 37 +++++++++ .../store/mongo/TestMongoAdrStoreShould.java | 77 ++++++++++++++++++- 6 files changed, 204 insertions(+), 15 deletions(-) diff --git a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java index dc1e21c0f..134015e78 100644 --- a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java +++ b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java @@ -58,7 +58,7 @@ public void setupAdrs() { @Test @Order(1) - void end_to_end_get_with_no_architecture() { + void end_to_end_verify_get_with_no_architecture() { given() .when().get("/calm/namespaces/finos/adrs") .then() @@ -68,7 +68,7 @@ void end_to_end_get_with_no_architecture() { @Test @Order(2) - void end_to_end_create_an_adr() { + void end_to_end_verify_create_an_adr() { given() .body(ADR) .header("Content-Type", "application/json") @@ -80,7 +80,7 @@ void end_to_end_create_an_adr() { @Test @Order(3) - void end_to_end_verify_revisions() { + void end_to_end_verify_get_revisions() { given() .when().get("/calm/namespaces/finos/adrs/1/revisions") .then() @@ -91,7 +91,7 @@ void end_to_end_verify_revisions() { @Test @Order(4) - void end_to_end_verify_architecture() { + void end_to_end_verify_get_adr_revision() { given() .when().get("/calm/namespaces/finos/adrs/1/revisions/1") .then() @@ -99,4 +99,14 @@ void end_to_end_verify_architecture() { .body(equalTo(ADR)); } + @Test + @Order(5) + void end_to_end_verify_get_adr() { + given() + .when().get("/calm/namespaces/finos/adrs/1") + .then() + .statusCode(200) + .body(equalTo(ADR)); + } + } \ No newline at end of file diff --git a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java index 37b1b414a..2ff30d2a0 100644 --- a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java +++ b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java @@ -87,6 +87,32 @@ public Response createAdrForNamespace(@PathParam("namespace") String namespace, } } + @GET + @Path("{namespace}/adrs/{adrId}") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Retrieve the latest revision of an ADR", + description = "Retrieve the latest revision of an ADR" + ) + public Response getAdr(@PathParam("namespace") String namespace, @PathParam("adrId") int adrId) { + Adr adr = AdrBuilder.builder() + .namespace(namespace) + .id(adrId) + .build(); + + try { + return Response.ok(store.getAdr(adr)).build(); + } catch (NamespaceNotFoundException e) { + logger.error("Invalid namespace [{}] when getting an ADR", namespace, e); + return invalidNamespaceResponse(namespace); + } catch (AdrNotFoundException e) { + logger.error("Invalid ADR [{}] when getting an ADR", adrId, e); + return invalidAdrResponse(adrId); + } catch (AdrRevisionNotFoundException e) { + logger.error("Could not get latest revision of ADR [{}]", adrId, e); + return invalidLatestRevisionResponse(adrId); + } + } @GET @Path("{namespace}/adrs/{adrId}/revisions") @@ -159,4 +185,8 @@ private Response invalidAdrResponse(int adrId) { private Response invalidRevisionResponse(int revision) { return Response.status(Response.Status.NOT_FOUND).entity("Invalid revision provided: " + revision).build(); } + + private Response invalidLatestRevisionResponse(int adrId) { + return Response.status(Response.Status.NOT_FOUND).entity("Latest revision not found for ADR: [{}]: " + adrId).build(); + } } diff --git a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java index e8ca4e94e..ef688758e 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java @@ -14,6 +14,7 @@ public interface AdrStore { List getAdrsForNamespace(String namespace) throws NamespaceNotFoundException; Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException; + String getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException; String getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java index 682c3cbbd..c47f8f490 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java @@ -12,11 +12,8 @@ import org.bson.conversions.Bson; import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; -import org.finos.calm.domain.Architecture; import org.finos.calm.domain.exception.AdrNotFoundException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; -import org.finos.calm.domain.exception.ArchitectureNotFoundException; -import org.finos.calm.domain.exception.ArchitectureVersionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; import org.slf4j.Logger; @@ -24,6 +21,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.OptionalInt; import java.util.Set; @ApplicationScoped @@ -54,11 +52,11 @@ public List getAdrsForNamespace(String namespace) throws NamespaceNotFo return List.of(); } - List patterns = namespaceDocument.getList("adrs", Document.class); + List adrs = namespaceDocument.getList("adrs", Document.class); List adrIds = new ArrayList<>(); - for (Document pattern : patterns) { - adrIds.add(pattern.getInteger("adrId")); + for (Document adr : adrs) { + adrIds.add(adr.getInteger("adrId")); } return adrIds; @@ -82,6 +80,39 @@ public Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException { return AdrBuilder.builder(adr).id(id).build(); } + @Override + public String getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + Document result = retrieveAdrRevisions(adr); + List adrs = (List) result.get("adrs"); + for (Document adrDoc : adrs) { + if (adr.id() == adrDoc.getInteger("adrId")) { + // Extract the revisions map from the matching adr + Document revisions = (Document) adrDoc.get("revisions"); + int latestRevision = getLatestRevision(adr, revisions); + + // Return the ADR JSON blob for the specified revision + Document revisionDoc = (Document) revisions.get(String.valueOf(latestRevision)); + log.info("RevisionDoc: [{}], Revision: [{}]", adrDoc.get("revisions"), latestRevision); + return revisionDoc.toJson(); + } + } + throw new AdrNotFoundException(); + } + + private int getLatestRevision(Adr adr, Document revisionsDoc) throws AdrRevisionNotFoundException { + if(revisionsDoc == null || revisionsDoc.isEmpty()) { + log.error("Could not find the latest revision of ADR [{}]", adr.id()); + throw new AdrRevisionNotFoundException(); + } + + Set revisionKeys = revisionsDoc.keySet(); + return revisionKeys.stream() + .map(Integer::parseInt) + .mapToInt(i -> i) + .max() + .getAsInt(); + } + @Override public List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException { Document result = retrieveAdrRevisions(adr); @@ -89,7 +120,7 @@ public List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, List adrs = (List) result.get("adrs"); for (Document adrDoc : adrs) { if (adr.id() == adrDoc.getInteger("adrId")) { - // Extract the revisions map from the matching pattern + // Extract the revisions map from the matching adr Document revisions = (Document) adrDoc.get("revisions"); Set revisionKeys = revisions.keySet(); @@ -107,10 +138,15 @@ public String getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotF List adrs = (List) result.get("adrs"); for (Document adrDoc : adrs) { if (adr.id() == adrDoc.getInteger("adrId")) { - // Retrieve the versions map from the matching pattern + // Retrieve the revisions map from the matching adr Document revisions = (Document) adrDoc.get("revisions"); - // Return the ADR JSON blob for the specified version + if(revisions == null || revisions.isEmpty()) { + //ADR Revisions collection not initialized + throw new AdrRevisionNotFoundException(); + } + + // Return the ADR JSON blob for the specified revision Document revisionDoc = (Document) revisions.get(String.valueOf(adr.revision())); log.info("RevisionDoc: [{}], Revision: [{}]", adrDoc.get("revisions"), adr.revision()); if(revisionDoc == null) { @@ -119,7 +155,7 @@ public String getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotF return revisionDoc.toJson(); } } - //ADR Revisions is empty, no version to find + //ADR Revisions is empty, no revisions to find throw new AdrRevisionNotFoundException(); } diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java index 89901918f..a3b5f060a 100644 --- a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java +++ b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java @@ -231,6 +231,34 @@ void respond_correctly_to_get_adr_revision(String namespace, Throwable exception verifyExpectedGetAdrRevision(namespace); } + @ParameterizedTest + @MethodSource("provideParametersForGetAdrRevisionTests") + void respond_correctly_to_get_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + if (exceptionToThrow != null) { + when(mockAdrStore.getAdr(any(Adr.class))).thenThrow(exceptionToThrow); + } else { + String adr = "{ \"test\": \"json\" }"; + when(mockAdrStore.getAdr(any(Adr.class))).thenReturn(adr); + } + + if (expectedStatusCode == 200) { + given() + .when() + .get("/calm/namespaces/" + namespace + "/adrs/12") + .then() + .statusCode(expectedStatusCode) + .body(equalTo("{ \"test\": \"json\" }")); + } else { + given() + .when() + .get("/calm/namespaces/" + namespace + "/adrs/12") + .then() + .statusCode(expectedStatusCode); + } + + verifyExpectedGetAdr(namespace); + } + private void verifyExpectedGetAdrRevision(String namespace) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { Adr expectedAdrToRetrieve = AdrBuilder.builder() .namespace(namespace) @@ -241,4 +269,13 @@ private void verifyExpectedGetAdrRevision(String namespace) throws NamespaceNotF verify(mockAdrStore, times(1)).getAdrRevision(expectedAdrToRetrieve); } + private void verifyExpectedGetAdr(String namespace) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + Adr expectedAdrToRetrieve = AdrBuilder.builder() + .namespace(namespace) + .id(12) + .build(); + + verify(mockAdrStore, times(1)).getAdr(expectedAdrToRetrieve); + } + } diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java index 8498eb3b5..76fd5c436 100644 --- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java +++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java @@ -226,7 +226,18 @@ void get_adr_revisions_for_valid_adr_returns_list_of_revisions() throws Namespac } @Test - void throw_an_exception_for_an_invalid_adre_when_retrieving_adr_revision() { + void get_adr_revision_for_invalid_namespace_throws_exception() { + when(namespaceStore.namespaceExists(anyString())).thenReturn(false); + Adr adr = AdrBuilder.builder().namespace("does-not-exist").build(); + + assertThrows(NamespaceNotFoundException.class, + () -> mongoAdrStore.getAdrRevision(adr)); + + verify(namespaceStore).namespaceExists(adr.namespace()); + } + + @Test + void throw_an_exception_for_an_invalid_adr_when_retrieving_adr_revision() { FindIterable findIterable = setupInvalidAdr(); Adr adr = AdrBuilder.builder().namespace(NAMESPACE).build(); @@ -281,4 +292,68 @@ void throw_an_exception_when_revision_of_adr_does_not_exist() { assertThrows(AdrRevisionNotFoundException.class, () -> mongoAdrStore.getAdrRevision(adr)); } + + @Test + void throw_an_exception_when_no_revision_exists_when_getting_adr() { + mockSetupAdrDocumentWithNoRevisions(); + + Adr adr = AdrBuilder.builder().namespace(NAMESPACE) + .id(42).revision(1).build(); + + assertThrows(AdrRevisionNotFoundException.class, + () -> mongoAdrStore.getAdr(adr)); + } + + private Document setupAdrWithNoRevisions() { + //Set up an ADR document with 1 ADR with No Revisions + + Document targetStoredAdr = new Document("adrId", 42); + + return new Document("namespace", NAMESPACE) + .append("adrs", List.of(targetStoredAdr)); + } + + private void mockSetupAdrDocumentWithNoRevisions() { + Document mainDocument = setupAdrWithNoRevisions(); + FindIterable findIterable = Mockito.mock(FindIterable.class); + when(namespaceStore.namespaceExists(anyString())).thenReturn(true); + when(adrCollection.find(any(Bson.class))) + .thenReturn(findIterable); + when(findIterable.projection(any(Bson.class))).thenReturn(findIterable); + when(findIterable.first()).thenReturn(mainDocument); + } + + @Test + void throw_an_exception_for_an_invalid_adr_when_retrieving_adr() { + FindIterable findIterable = setupInvalidAdr(); + Adr adr = AdrBuilder.builder().namespace(NAMESPACE).id(7).build(); + + assertThrows(AdrNotFoundException.class, + () -> mongoAdrStore.getAdr(adr)); + + verify(adrCollection).find(new Document("namespace", adr.namespace())); + verify(findIterable).projection(Projections.fields(Projections.include("adrs"))); + } + + @Test + void get_adr_for_invalid_namespace_throws_exception() { + when(namespaceStore.namespaceExists(anyString())).thenReturn(false); + Adr adr = AdrBuilder.builder().namespace("does-not-exist").build(); + + assertThrows(NamespaceNotFoundException.class, + () -> mongoAdrStore.getAdr(adr)); + + verify(namespaceStore).namespaceExists(adr.namespace()); + } + + @Test + void return_the_latest_adr_revision() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + mockSetupAdrDocumentWithRevisions(); + + Adr adr = AdrBuilder.builder().namespace(NAMESPACE) + .id(42).revision(1).build(); + + String adrRevision = mongoAdrStore.getAdr(adr); + assertThat(adrRevision, is(validJson)); + } } From 9957c2168483d21fa7bf37967170f83c8a884ccc Mon Sep 17 00:00:00 2001 From: Graham Packer Date: Tue, 7 Jan 2025 14:29:34 +0000 Subject: [PATCH 05/12] Add update ADR endpoint --- .../java/integration/MongoAdrIntegration.java | 32 ++++++--- .../main/java/org/finos/calm/domain/Adr.java | 4 ++ .../org/finos/calm/resources/AdrResource.java | 35 +++++++++- .../java/org/finos/calm/store/AdrStore.java | 5 +- .../finos/calm/store/mongo/MongoAdrStore.java | 38 ++++++++++- .../calm/resources/TestAdrResourceShould.java | 48 ++++++++++++-- .../store/mongo/TestMongoAdrStoreShould.java | 66 +++++++++++++++++-- 7 files changed, 202 insertions(+), 26 deletions(-) diff --git a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java index 134015e78..fb4e40cfb 100644 --- a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java +++ b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java @@ -80,20 +80,19 @@ void end_to_end_verify_create_an_adr() { @Test @Order(3) - void end_to_end_verify_get_revisions() { + void end_to_end_verify_get_adr_revision() { given() - .when().get("/calm/namespaces/finos/adrs/1/revisions") + .when().get("/calm/namespaces/finos/adrs/1/revisions/1") .then() .statusCode(200) - .body("values", hasSize(1)) - .body("values[0]", equalTo(1)); + .body(equalTo(ADR)); } @Test @Order(4) - void end_to_end_verify_get_adr_revision() { + void end_to_end_verify_get_adr() { given() - .when().get("/calm/namespaces/finos/adrs/1/revisions/1") + .when().get("/calm/namespaces/finos/adrs/1") .then() .statusCode(200) .body(equalTo(ADR)); @@ -101,12 +100,27 @@ void end_to_end_verify_get_adr_revision() { @Test @Order(5) - void end_to_end_verify_get_adr() { + void end_to_end_verify_update_an_adr() { given() - .when().get("/calm/namespaces/finos/adrs/1") + .body(ADR) + .header("Content-Type", "application/json") + .when().post("/calm/namespaces/finos/adrs/1") + .then() + .statusCode(201) + .header("Location", containsString("calm/namespaces/finos/adrs/1")); + } + + @Test + @Order(6) + void end_to_end_verify_get_revisions() { + given() + .when().get("/calm/namespaces/finos/adrs/1/revisions") .then() .statusCode(200) - .body(equalTo(ADR)); + .body("values", hasSize(2)) + .body("values[0]", equalTo(1)) + .body("values[1]", equalTo(2)); + } } \ No newline at end of file diff --git a/calm-hub/src/main/java/org/finos/calm/domain/Adr.java b/calm-hub/src/main/java/org/finos/calm/domain/Adr.java index 1faa3dd5b..3a6b821c0 100644 --- a/calm-hub/src/main/java/org/finos/calm/domain/Adr.java +++ b/calm-hub/src/main/java/org/finos/calm/domain/Adr.java @@ -2,6 +2,10 @@ import io.soabase.recordbuilder.core.RecordBuilder; +/** + * Represents an ADR and the associated namespace, id, and revision. + * The ADR is represented as a String in JSON format. + */ @RecordBuilder.Options(enableWither = false) @RecordBuilder public record Adr(String namespace, int id, int revision, String adr) { diff --git a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java index 2ff30d2a0..3fdeaa569 100644 --- a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java +++ b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java @@ -12,12 +12,9 @@ import org.eclipse.microprofile.openapi.annotations.Operation; import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; -import org.finos.calm.domain.Architecture; import org.finos.calm.domain.ValueWrapper; import org.finos.calm.domain.exception.AdrNotFoundException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; -import org.finos.calm.domain.exception.ArchitectureNotFoundException; -import org.finos.calm.domain.exception.ArchitectureVersionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; import org.slf4j.Logger; @@ -87,6 +84,38 @@ public Response createAdrForNamespace(@PathParam("namespace") String namespace, } } + @POST + @Path("{namespace}/adrs/{adrId}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Update ADR for namespace", + description = "Updates an ADR for a given namespace. Creates a new revision." + ) + public Response updateAdrForNamespace(@PathParam("namespace") String namespace, @PathParam("adrId") int adrId, String adrJson) throws URISyntaxException { + Adr adr = AdrBuilder.builder() + .namespace(namespace) + .id(adrId) + .adr(adrJson) + .build(); + + try { + return adrWithLocationResponse(store.updateAdrForNamespace(adr)); + } catch (NamespaceNotFoundException e) { + logger.error("Invalid namespace [{}] when creating ADR", namespace, e); + return invalidNamespaceResponse(namespace); + } catch (JsonParseException e) { + logger.error("Cannot parse ADR JSON for namespace [{}]. ADR JSON : [{}]", namespace, adrJson, e); + return invalidAdrJsonResponse(namespace); + } catch(AdrNotFoundException e) { + logger.error("Invalid ADR [{}] when creating new revision of ADR", adrId, e); + return invalidAdrResponse(adrId); + } catch(AdrRevisionNotFoundException e) { + logger.error("No existing revision of ADR [{}] found", adrId, e); + return invalidLatestRevisionResponse(adrId); + } + } + @GET @Path("{namespace}/adrs/{adrId}") @Produces(MediaType.APPLICATION_JSON) diff --git a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java index ef688758e..97a6ed627 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java @@ -1,11 +1,8 @@ package org.finos.calm.store; import org.finos.calm.domain.Adr; -import org.finos.calm.domain.Architecture; import org.finos.calm.domain.exception.AdrNotFoundException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; -import org.finos.calm.domain.exception.ArchitectureNotFoundException; -import org.finos.calm.domain.exception.ArchitectureVersionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import java.util.List; @@ -17,5 +14,5 @@ public interface AdrStore { String getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException; String getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; - + Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; } diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java index c47f8f490..175c276c0 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java @@ -1,5 +1,6 @@ package org.finos.calm.store.mongo; +import com.mongodb.MongoWriteException; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; @@ -21,7 +22,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.OptionalInt; import java.util.Set; @ApplicationScoped @@ -159,6 +159,26 @@ public String getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotF throw new AdrRevisionNotFoundException(); } + @Override + public Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + Document result = retrieveAdrRevisions(adr); + List adrs = (List) result.get("adrs"); + for (Document adrDoc : adrs) { + if (adr.id() == adrDoc.getInteger("adrId")) { + // Extract the revisions map from the matching adr + Document revisions = (Document) adrDoc.get("revisions"); + int newRevision = getLatestRevision(adr, revisions) + 1; + + Adr newAdr = AdrBuilder.builder(adr).revision(newRevision).build(); + + writeAdrToMongo(newAdr); + return newAdr; + + } + } + throw new AdrNotFoundException(); + } + private Document retrieveAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException { if(!namespaceStore.namespaceExists(adr.namespace())) { throw new NamespaceNotFoundException(); @@ -175,4 +195,20 @@ private Document retrieveAdrRevisions(Adr adr) throws NamespaceNotFoundException return result; } + + private void writeAdrToMongo(Adr adr) throws AdrNotFoundException { + + Document adrDocument = Document.parse(adr.adr()); + Document filter = new Document("namespace", adr.namespace()) + .append("adrs.adrId", adr.id()); + Document update = new Document("$set", + new Document("adrs.$.revisions." + adr.revision(), adrDocument)); + + try { + adrCollection.updateOne(filter, update, new UpdateOptions().upsert(true)); + } catch (MongoWriteException ex) { + log.error("Failed to write ADR to mongo [{}]", adr, ex); + throw new AdrNotFoundException(); + } + } } diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java index a3b5f060a..9c5906c25 100644 --- a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java +++ b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java @@ -5,11 +5,8 @@ import org.bson.json.JsonParseException; import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; -import org.finos.calm.domain.Architecture; import org.finos.calm.domain.exception.AdrNotFoundException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; -import org.finos.calm.domain.exception.ArchitectureNotFoundException; -import org.finos.calm.domain.exception.ArchitectureVersionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; import org.junit.jupiter.api.Test; @@ -135,7 +132,7 @@ void return_a_created_with_location_of_adr_when_creating_adr() throws NamespaceN .post("/calm/namespaces/finos/adrs") .then() .statusCode(201) - //Derived from stubbed architecture in resource + //Derived from stubbed adr in resource .header("Location", containsString("/calm/namespaces/finos/adrs/12/revisions/1")); Adr expectedAdrToCreate = AdrBuilder.builder() @@ -147,6 +144,49 @@ void return_a_created_with_location_of_adr_when_creating_adr() throws NamespaceN verify(mockAdrStore, times(1)).createAdrForNamespace(expectedAdrToCreate); } + //Update ADR Tests + + static Stream provideParametersForUpdateAdrTests() { + return Stream.of( + Arguments.of("invalid", new NamespaceNotFoundException(), 404), + Arguments.of("valid", new AdrNotFoundException(), 404), + Arguments.of("valid", new AdrRevisionNotFoundException(), 404), + Arguments.of("valid", null, 201) + ); + } + + @ParameterizedTest + @MethodSource("provideParametersForUpdateAdrTests") + void return_a_404_when_invalid_namespace_is_provided_on_update_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + String adrJson = "{ \"test\": \"json\" }"; + if (exceptionToThrow != null) { + when(mockAdrStore.updateAdrForNamespace(any(Adr.class))).thenThrow(exceptionToThrow); + } else { + Adr adr = AdrBuilder.builder() + .adr(adrJson) + .id(2) + .namespace(namespace) + .build(); + when(mockAdrStore.updateAdrForNamespace(any(Adr.class))).thenReturn(adr); + } + + given() + .header("Content-Type", "application/json") + .body(adrJson) + .when() + .post("/calm/namespaces/invalid/adrs/1") + .then() + .statusCode(expectedStatusCode); + + Adr expectedAdr = AdrBuilder.builder() + .adr(adrJson) + .id(1) + .namespace("invalid") + .build(); + + verify(mockAdrStore, times(1)).updateAdrForNamespace(expectedAdr); + } + static Stream provideParametersForAdrRevisionTests() { return Stream.of( Arguments.of("invalid", new NamespaceNotFoundException(), 404), diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java index 76fd5c436..e852eb581 100644 --- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java +++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java @@ -15,10 +15,8 @@ import org.bson.json.JsonParseException; import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; -import org.finos.calm.domain.Architecture; import org.finos.calm.domain.exception.AdrNotFoundException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; -import org.finos.calm.domain.exception.ArchitectureVersionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,6 +35,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -122,9 +121,9 @@ void get_adrs_for_namespace_returns_values() throws NamespaceNotFoundException { when(documentMock.getList("adrs", Document.class)) .thenReturn(Arrays.asList(doc1, doc2)); - List architectureIds = mongoAdrStore.getAdrsForNamespace(NAMESPACE); + List adrIds = mongoAdrStore.getAdrsForNamespace(NAMESPACE); - assertThat(architectureIds, is(Arrays.asList(1001, 1002))); + assertThat(adrIds, is(Arrays.asList(1001, 1002))); verify(namespaceStore).namespaceExists(NAMESPACE); } @@ -142,7 +141,7 @@ void return_a_namespace_exception_when_namespace_does_not_exist_when_creating_an @Test void return_a_json_parse_exception_when_an_invalid_json_object_is_presented_when_creating_an_adr() { when(namespaceStore.namespaceExists(anyString())).thenReturn(true); - when(counterStore.getNextArchitectureSequenceValue()).thenReturn(42); + when(counterStore.getNextAdrSequenceValue()).thenReturn(42); Adr adr = AdrBuilder.builder().namespace(NAMESPACE) .adr("Invalid JSON") .build(); @@ -356,4 +355,61 @@ void return_the_latest_adr_revision() throws NamespaceNotFoundException, AdrNotF String adrRevision = mongoAdrStore.getAdr(adr); assertThat(adrRevision, is(validJson)); } + + @Test + void throw_an_exception_when_update_adr_with_a_namespace_that_doesnt_exists() { + when(namespaceStore.namespaceExists(anyString())).thenReturn(false); + + Adr adr = AdrBuilder.builder() + .namespace(NAMESPACE) + .id(42) + .build(); + + assertThrows(NamespaceNotFoundException.class, + () -> mongoAdrStore.updateAdrForNamespace(adr)); + + verify(namespaceStore, times(1)).namespaceExists(adr.namespace()); + } + + @Test + void throw_an_exception_when_updating_an_adr_that_doesnt_exist() { + mockSetupAdrDocumentWithRevisions(); + + Adr adr = AdrBuilder.builder() + .namespace(NAMESPACE) + .id(22) + .build(); + + assertThrows(AdrNotFoundException.class, + () -> mongoAdrStore.updateAdrForNamespace(adr)); + } + + @Test + void throw_an_exception_when_updating_an_adr_but_no_revisions_exist() { + mockSetupAdrDocumentWithNoRevisions(); + + Adr adr = AdrBuilder.builder() + .namespace(NAMESPACE) + .id(42) + .adr(validJson) + .build(); + + assertThrows(AdrRevisionNotFoundException.class, + () -> mongoAdrStore.updateAdrForNamespace(adr)); + } + + @Test + void return_successfully_when_correctly_updating_an_adr() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + mockSetupAdrDocumentWithRevisions(); + Adr adr = AdrBuilder.builder() + .namespace(NAMESPACE) + .id(42) + .revision(2) + .adr(validJson) + .build(); + + mongoAdrStore.updateAdrForNamespace(adr); + + verify(adrCollection, times(1)).updateOne(any(Bson.class), any(Bson.class), any(UpdateOptions.class)); + } } From ddb57af657015d0d51f9e533aed4d20273645143 Mon Sep 17 00:00:00 2001 From: Graham Packer Date: Mon, 13 Jan 2025 13:40:29 +0000 Subject: [PATCH 06/12] Add ADR model (#716) --- calm-hub/.gitignore | 4 +- calm-hub/pom.xml | 4 + .../java/integration/MongoAdrIntegration.java | 75 ++++++++-- .../main/java/org/finos/calm/domain/Adr.java | 2 +- .../org/finos/calm/domain/AdrContent.java | 57 ++++++++ .../org/finos/calm/domain/AdrDecision.java | 8 + .../java/org/finos/calm/domain/AdrLink.java | 8 + .../java/org/finos/calm/domain/AdrOption.java | 10 ++ .../java/org/finos/calm/domain/AdrStatus.java | 10 ++ .../java/org/finos/calm/domain/NewAdr.java | 18 +++ .../org/finos/calm/resources/AdrResource.java | 31 +++- .../java/org/finos/calm/store/AdrStore.java | 5 +- .../finos/calm/store/mongo/MongoAdrStore.java | 36 +++-- .../calm/resources/TestAdrResourceShould.java | 138 +++++++++--------- .../store/mongo/TestMongoAdrStoreShould.java | 73 +++++---- 15 files changed, 339 insertions(+), 140 deletions(-) create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/AdrContent.java create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/AdrDecision.java create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/AdrLink.java create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/AdrOption.java create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/AdrStatus.java create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/NewAdr.java diff --git a/calm-hub/.gitignore b/calm-hub/.gitignore index 04115d600..5a78487a9 100644 --- a/calm-hub/.gitignore +++ b/calm-hub/.gitignore @@ -42,7 +42,7 @@ # .idea/modules.xml # .idea/*.iml # .idea/modules -# *.iml +*.iml # *.ipr # CMake @@ -313,4 +313,4 @@ thumb sketch -# End of https://www.toptal.com/developers/gitignore/api/intellij,react,node,java,maven \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/intellij,react,node,java,maven diff --git a/calm-hub/pom.xml b/calm-hub/pom.xml index fe50309ba..9fb82398c 100644 --- a/calm-hub/pom.xml +++ b/calm-hub/pom.xml @@ -50,6 +50,10 @@ 44 provided + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + diff --git a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java index fb4e40cfb..560b9ed61 100644 --- a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java +++ b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java @@ -1,5 +1,7 @@ package integration; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.MongoDatabase; @@ -7,6 +9,17 @@ import io.quarkus.test.junit.TestProfile; import org.bson.Document; import org.eclipse.microprofile.config.ConfigProvider; +import org.finos.calm.domain.Adr; +import org.finos.calm.domain.AdrContent; +import org.finos.calm.domain.AdrContentBuilder; +import org.finos.calm.domain.AdrDecision; +import org.finos.calm.domain.AdrDecisionBuilder; +import org.finos.calm.domain.AdrLinkBuilder; +import org.finos.calm.domain.AdrOption; +import org.finos.calm.domain.AdrOptionBuilder; +import org.finos.calm.domain.AdrStatus; +import org.finos.calm.domain.NewAdr; +import org.finos.calm.domain.NewAdrBuilder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; @@ -16,20 +29,50 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.List; +import com.fasterxml.jackson.databind.ObjectMapper; import static integration.MongoSetup.counterSetup; import static integration.MongoSetup.namespaceSetup; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertEquals; @QuarkusTest @TestProfile(IntegrationTestProfile.class) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class MongoAdrIntegration { - private static final Logger logger = LoggerFactory.getLogger(MongoAdrIntegration.class); - public static final String ADR = "{\"name\": \"test-adr\"}"; + ObjectMapper objectMapper; + private static final Logger logger = LoggerFactory.getLogger(MongoAdrIntegration.class); + public static final String ADR = "{\"name\": \"test-adrContent\"}"; + + private final String TITLE = "My ADR"; + private final String DRAFT = "DRAFT"; + private final String PROBLEM_STATEMENT = "My problem is..."; + private final List DECISION_DRIVERS = List.of("a", "b", "c"); + private final AdrOption OPTION_A = AdrOptionBuilder.builder().name("Option 1").description("optionDescription") + .positiveConsequences(List.of("a")).negativeConsequences(List.of("b")).build(); + private final AdrOption OPTION_B = AdrOptionBuilder.builder().name("Option 2").description("optionDescription") + .positiveConsequences(List.of("c")).negativeConsequences(List.of("d")).build(); + private final List CONSIDERED_OPTIONS = List.of(OPTION_A, OPTION_B); + private final String RATIONALE = "This is the best option"; + private final AdrDecision DECISION_OUTCOME = AdrDecisionBuilder.builder() + .rationale(RATIONALE) + .chosenOption(OPTION_A) + .build(); + + private final NewAdr newAdr = NewAdrBuilder.builder() + .title(TITLE) + .contextAndProblemStatement(PROBLEM_STATEMENT) + .decisionDrivers(DECISION_DRIVERS) + .consideredOptions(CONSIDERED_OPTIONS) + .decisionOutcome(DECISION_OUTCOME) + .links(List.of(AdrLinkBuilder.builder().rel("abc").href("http://abc.com").build())) + .build(); + + private final AdrContent adrContent = AdrContent.builderFromNewAdr(newAdr).status(AdrStatus.DRAFT).build(); @BeforeEach public void setupAdrs() { @@ -54,6 +97,8 @@ public void setupAdrs() { counterSetup(database); namespaceSetup(database); } + this.objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); } @Test @@ -68,9 +113,9 @@ void end_to_end_verify_get_with_no_architecture() { @Test @Order(2) - void end_to_end_verify_create_an_adr() { + void end_to_end_verify_create_an_adr() throws JsonProcessingException { given() - .body(ADR) + .body(objectMapper.writeValueAsString(newAdr)) .header("Content-Type", "application/json") .when().post("/calm/namespaces/finos/adrs") .then() @@ -80,29 +125,37 @@ void end_to_end_verify_create_an_adr() { @Test @Order(3) - void end_to_end_verify_get_adr_revision() { - given() + void end_to_end_verify_get_adr_revision() throws JsonProcessingException { + AdrContent result = given() .when().get("/calm/namespaces/finos/adrs/1/revisions/1") .then() .statusCode(200) - .body(equalTo(ADR)); + .extract() + .body() + .as(AdrContent.class); + + assertEquals(adrContent, result); } @Test @Order(4) void end_to_end_verify_get_adr() { - given() + AdrContent result = given() .when().get("/calm/namespaces/finos/adrs/1") .then() .statusCode(200) - .body(equalTo(ADR)); + .extract() + .body() + .as(AdrContent.class); + + assertEquals(adrContent, result); } @Test @Order(5) - void end_to_end_verify_update_an_adr() { + void end_to_end_verify_update_an_adr() throws JsonProcessingException { given() - .body(ADR) + .body(objectMapper.writeValueAsString(newAdr)) .header("Content-Type", "application/json") .when().post("/calm/namespaces/finos/adrs/1") .then() diff --git a/calm-hub/src/main/java/org/finos/calm/domain/Adr.java b/calm-hub/src/main/java/org/finos/calm/domain/Adr.java index 3a6b821c0..b9624fa3e 100644 --- a/calm-hub/src/main/java/org/finos/calm/domain/Adr.java +++ b/calm-hub/src/main/java/org/finos/calm/domain/Adr.java @@ -8,6 +8,6 @@ */ @RecordBuilder.Options(enableWither = false) @RecordBuilder -public record Adr(String namespace, int id, int revision, String adr) { +public record Adr(String namespace, int id, int revision, AdrContent adrContent) { } diff --git a/calm-hub/src/main/java/org/finos/calm/domain/AdrContent.java b/calm-hub/src/main/java/org/finos/calm/domain/AdrContent.java new file mode 100644 index 000000000..f8c231b09 --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/domain/AdrContent.java @@ -0,0 +1,57 @@ +package org.finos.calm.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import io.soabase.recordbuilder.core.RecordBuilder; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +@RecordBuilder.Options(enableWither = false) +@RecordBuilder +public record AdrContent( + String title, + AdrStatus status, + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss:SSS") + LocalDateTime creationDateTime, + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss:SSS") + LocalDateTime updateDateTime, + String contextAndProblemStatement, + List decisionDrivers, + List consideredOptions, + AdrDecision decisionOutcome, + List links + +) { + + public static AdrContentBuilder builderFromNewAdr(NewAdr newAdr) { + return AdrContentBuilder.builder() + .title(newAdr.title()) + .contextAndProblemStatement(newAdr.contextAndProblemStatement()) + .decisionDrivers(newAdr.decisionDrivers()) + .consideredOptions(newAdr.consideredOptions()) + .decisionOutcome(newAdr.decisionOutcome()) + .links(newAdr.links()); + } + + @Override + public boolean equals(Object o) { + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + AdrContent that = (AdrContent) o; + return Objects.equals(title, that.title) && status == that.status && Objects.equals(contextAndProblemStatement, that.contextAndProblemStatement) && Objects.equals(decisionDrivers, that.decisionDrivers) && Objects.equals(consideredOptions, that.consideredOptions) && Objects.equals(decisionOutcome, that.decisionOutcome) && Objects.equals(links, that.links); + } + + @Override + public int hashCode() { + return Objects.hash(title, status, contextAndProblemStatement, decisionDrivers, consideredOptions, decisionOutcome, links); + } +} diff --git a/calm-hub/src/main/java/org/finos/calm/domain/AdrDecision.java b/calm-hub/src/main/java/org/finos/calm/domain/AdrDecision.java new file mode 100644 index 000000000..693edf9eb --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/domain/AdrDecision.java @@ -0,0 +1,8 @@ +package org.finos.calm.domain; + +import io.soabase.recordbuilder.core.RecordBuilder; + +@RecordBuilder.Options(enableWither = false) +@RecordBuilder +public record AdrDecision(AdrOption chosenOption, String rationale) { +} diff --git a/calm-hub/src/main/java/org/finos/calm/domain/AdrLink.java b/calm-hub/src/main/java/org/finos/calm/domain/AdrLink.java new file mode 100644 index 000000000..99c5c8088 --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/domain/AdrLink.java @@ -0,0 +1,8 @@ +package org.finos.calm.domain; + +import io.soabase.recordbuilder.core.RecordBuilder; + +@RecordBuilder.Options(enableWither = false) +@RecordBuilder +public record AdrLink(String rel, String href) { +} diff --git a/calm-hub/src/main/java/org/finos/calm/domain/AdrOption.java b/calm-hub/src/main/java/org/finos/calm/domain/AdrOption.java new file mode 100644 index 000000000..c307f6ebc --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/domain/AdrOption.java @@ -0,0 +1,10 @@ +package org.finos.calm.domain; + +import io.soabase.recordbuilder.core.RecordBuilder; + +import java.util.List; + +@RecordBuilder.Options(enableWither = false) +@RecordBuilder +public record AdrOption(String name, String description, List positiveConsequences, List negativeConsequences) { +} diff --git a/calm-hub/src/main/java/org/finos/calm/domain/AdrStatus.java b/calm-hub/src/main/java/org/finos/calm/domain/AdrStatus.java new file mode 100644 index 000000000..322cb116b --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/domain/AdrStatus.java @@ -0,0 +1,10 @@ +package org.finos.calm.domain; + +public enum AdrStatus { + DRAFT, + PROPOSED, + ACCEPTED, + SUPERSEDED, + REJECTED, + DEPRECATED; +} diff --git a/calm-hub/src/main/java/org/finos/calm/domain/NewAdr.java b/calm-hub/src/main/java/org/finos/calm/domain/NewAdr.java new file mode 100644 index 000000000..5ade2afbb --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/domain/NewAdr.java @@ -0,0 +1,18 @@ +package org.finos.calm.domain; + +import io.soabase.recordbuilder.core.RecordBuilder; + +import java.time.LocalDateTime; +import java.util.List; + +@RecordBuilder.Options(enableWither = false) +@RecordBuilder +public record NewAdr( + String title, + String contextAndProblemStatement, + List decisionDrivers, + List consideredOptions, + AdrDecision decisionOutcome, + List links +) { +} diff --git a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java index 3fdeaa569..b47ea1e1a 100644 --- a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java +++ b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java @@ -1,5 +1,6 @@ package org.finos.calm.resources; +import com.fasterxml.jackson.core.JsonProcessingException; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -12,6 +13,9 @@ import org.eclipse.microprofile.openapi.annotations.Operation; import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; +import org.finos.calm.domain.AdrContent; +import org.finos.calm.domain.AdrStatus; +import org.finos.calm.domain.NewAdr; import org.finos.calm.domain.ValueWrapper; import org.finos.calm.domain.exception.AdrNotFoundException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; @@ -22,6 +26,7 @@ import java.net.URI; import java.net.URISyntaxException; +import java.time.LocalDateTime; /** * Resource for managing ADRs in a given namespace @@ -66,11 +71,17 @@ public Response getAdrsForNamespace(@PathParam("namespace") String namespace) { summary = "Create ADR for namespace", description = "Creates an ADR for a given namespace with an allocated ID and revision 1" ) - public Response createAdrForNamespace(@PathParam("namespace") String namespace, String adrJson) throws URISyntaxException { + public Response createAdrForNamespace(@PathParam("namespace") String namespace, NewAdr newAdr) throws URISyntaxException { + AdrContent adrContent = AdrContent.builderFromNewAdr(newAdr) + .status(AdrStatus.DRAFT) + .creationDateTime(LocalDateTime.now()) + .updateDateTime(LocalDateTime.now()) + .build(); + Adr adr = AdrBuilder.builder() .namespace(namespace) .revision(1) - .adr(adrJson) + .adrContent(adrContent) .build(); try { @@ -78,8 +89,8 @@ public Response createAdrForNamespace(@PathParam("namespace") String namespace, } catch (NamespaceNotFoundException e) { logger.error("Invalid namespace [{}] when creating ADR", namespace, e); return invalidNamespaceResponse(namespace); - } catch (JsonParseException e) { - logger.error("Cannot parse ADR JSON for namespace [{}]. ADR JSON : [{}]", namespace, adrJson, e); + } catch (JsonParseException | JsonProcessingException e) { + logger.error("Cannot parse ADR for namespace [{}]. ADR: [{}]", namespace, newAdr, e); return invalidAdrJsonResponse(namespace); } } @@ -92,11 +103,15 @@ public Response createAdrForNamespace(@PathParam("namespace") String namespace, summary = "Update ADR for namespace", description = "Updates an ADR for a given namespace. Creates a new revision." ) - public Response updateAdrForNamespace(@PathParam("namespace") String namespace, @PathParam("adrId") int adrId, String adrJson) throws URISyntaxException { + public Response updateAdrForNamespace(@PathParam("namespace") String namespace, @PathParam("adrId") int adrId, NewAdr newAdrRevision) throws URISyntaxException { + AdrContent adrContent = AdrContent.builderFromNewAdr(newAdrRevision) + .updateDateTime(LocalDateTime.now()) + .build(); + Adr adr = AdrBuilder.builder() .namespace(namespace) .id(adrId) - .adr(adrJson) + .adrContent(adrContent) .build(); try { @@ -104,8 +119,8 @@ public Response updateAdrForNamespace(@PathParam("namespace") String namespace, } catch (NamespaceNotFoundException e) { logger.error("Invalid namespace [{}] when creating ADR", namespace, e); return invalidNamespaceResponse(namespace); - } catch (JsonParseException e) { - logger.error("Cannot parse ADR JSON for namespace [{}]. ADR JSON : [{}]", namespace, adrJson, e); + } catch (JsonParseException | JsonProcessingException e) { + logger.error("Cannot parse new ADR Revision for namespace [{}]. New ADR Revision: [{}]", namespace, newAdrRevision, e); return invalidAdrJsonResponse(namespace); } catch(AdrNotFoundException e) { logger.error("Invalid ADR [{}] when creating new revision of ADR", adrId, e); diff --git a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java index 97a6ed627..516382bbb 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java @@ -1,5 +1,6 @@ package org.finos.calm.store; +import com.fasterxml.jackson.core.JsonProcessingException; import org.finos.calm.domain.Adr; import org.finos.calm.domain.exception.AdrNotFoundException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; @@ -10,9 +11,9 @@ public interface AdrStore { List getAdrsForNamespace(String namespace) throws NamespaceNotFoundException; - Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException; + Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException, JsonProcessingException; String getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException; String getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; - Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; + Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException; } diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java index 175c276c0..5ac3dacf1 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java @@ -1,5 +1,8 @@ package org.finos.calm.store.mongo; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.mongodb.MongoWriteException; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; @@ -13,6 +16,8 @@ import org.bson.conversions.Bson; import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; +import org.finos.calm.domain.AdrContent; +import org.finos.calm.domain.AdrContentBuilder; import org.finos.calm.domain.exception.AdrNotFoundException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; @@ -30,6 +35,7 @@ public class MongoAdrStore implements AdrStore { private final MongoCounterStore counterStore; private final MongoNamespaceStore namespaceStore; private final MongoCollection adrCollection; + private final ObjectMapper objectMapper; private final Logger log = LoggerFactory.getLogger(getClass()); public MongoAdrStore(MongoClient mongoClient, MongoCounterStore counterStore, MongoNamespaceStore namespaceStore) { @@ -37,6 +43,8 @@ public MongoAdrStore(MongoClient mongoClient, MongoCounterStore counterStore, Mo this.namespaceStore = namespaceStore; MongoDatabase database = mongoClient.getDatabase("calmSchemas"); this.adrCollection = database.getCollection("adrs"); + this.objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); } @Override @@ -63,14 +71,14 @@ public List getAdrsForNamespace(String namespace) throws NamespaceNotFo } @Override - public Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException { + public Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException, JsonProcessingException { if(!namespaceStore.namespaceExists(adr.namespace())) { throw new NamespaceNotFoundException(); } int id = counterStore.getNextAdrSequenceValue(); Document adrDocument = new Document("adrId", id).append("revisions", - new Document(String.valueOf(adr.revision()), Document.parse(adr.adr()))); + new Document(String.valueOf(adr.revision()), Document.parse(objectMapper.writeValueAsString(adr.adrContent())))); adrCollection.updateOne( Filters.eq("namespace", adr.namespace()), @@ -86,7 +94,7 @@ public String getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundExce List adrs = (List) result.get("adrs"); for (Document adrDoc : adrs) { if (adr.id() == adrDoc.getInteger("adrId")) { - // Extract the revisions map from the matching adr + // Extract the revisions map from the matching adrContent Document revisions = (Document) adrDoc.get("revisions"); int latestRevision = getLatestRevision(adr, revisions); @@ -120,7 +128,7 @@ public List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, List adrs = (List) result.get("adrs"); for (Document adrDoc : adrs) { if (adr.id() == adrDoc.getInteger("adrId")) { - // Extract the revisions map from the matching adr + // Extract the revisions map from the matching adrContent Document revisions = (Document) adrDoc.get("revisions"); Set revisionKeys = revisions.keySet(); @@ -138,7 +146,7 @@ public String getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotF List adrs = (List) result.get("adrs"); for (Document adrDoc : adrs) { if (adr.id() == adrDoc.getInteger("adrId")) { - // Retrieve the revisions map from the matching adr + // Retrieve the revisions map from the matching adrContent Document revisions = (Document) adrDoc.get("revisions"); if(revisions == null || revisions.isEmpty()) { @@ -160,16 +168,22 @@ public String getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotF } @Override - public Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + public Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { Document result = retrieveAdrRevisions(adr); List adrs = (List) result.get("adrs"); for (Document adrDoc : adrs) { if (adr.id() == adrDoc.getInteger("adrId")) { - // Extract the revisions map from the matching adr + // Extract the revisions map from the matching adrContent Document revisions = (Document) adrDoc.get("revisions"); - int newRevision = getLatestRevision(adr, revisions) + 1; + int latestRevision = getLatestRevision(adr, revisions); + Document revisionDoc = (Document) revisions.get(String.valueOf(latestRevision)); + AdrContent latestAdrContent = objectMapper.readValue(revisionDoc.toJson(), AdrContent.class); + AdrContent newAdrContent = AdrContentBuilder.builder(adr.adrContent()) + .status(latestAdrContent.status()) + .creationDateTime(latestAdrContent.creationDateTime()) + .build(); - Adr newAdr = AdrBuilder.builder(adr).revision(newRevision).build(); + Adr newAdr = AdrBuilder.builder(adr).adrContent(newAdrContent).revision(latestRevision + 1).build(); writeAdrToMongo(newAdr); return newAdr; @@ -196,9 +210,9 @@ private Document retrieveAdrRevisions(Adr adr) throws NamespaceNotFoundException return result; } - private void writeAdrToMongo(Adr adr) throws AdrNotFoundException { + private void writeAdrToMongo(Adr adr) throws AdrNotFoundException, JsonProcessingException { - Document adrDocument = Document.parse(adr.adr()); + Document adrDocument = Document.parse(objectMapper.writeValueAsString(adr.adrContent())); Document filter = new Document("namespace", adr.namespace()) .append("adrs.adrId", adr.id()); Document update = new Document("$set", diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java index 9c5906c25..c4e9dc3d5 100644 --- a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java +++ b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java @@ -1,14 +1,18 @@ package org.finos.calm.resources; +import com.fasterxml.jackson.core.JsonProcessingException; import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; import org.bson.json.JsonParseException; import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; +import org.finos.calm.domain.AdrContentBuilder; +import org.finos.calm.domain.AdrStatus; import org.finos.calm.domain.exception.AdrNotFoundException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; +import org.hamcrest.Matcher; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -16,6 +20,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.junit.jupiter.MockitoExtension; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; @@ -23,6 +28,8 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.times; @@ -51,7 +58,7 @@ void return_a_404_when_an_invalid_namespace_is_provided_on_get_adrs() throws Nam @Test void return_list_of_adr_ids_when_valid_namespace_provided_on_get_adrs() throws NamespaceNotFoundException { - when(mockAdrStore.getAdrsForNamespace(anyString())).thenReturn(Arrays.asList(12345,54321)); + when(mockAdrStore.getAdrsForNamespace(anyString())).thenReturn(Arrays.asList(12345, 54321)); given() .when() @@ -63,88 +70,62 @@ void return_list_of_adr_ids_when_valid_namespace_provided_on_get_adrs() throws N verify(mockAdrStore, times(1)).getAdrsForNamespace("finos"); } - @Test - void return_a_404_when_invalid_namespace_is_provided_on_create_adr() throws NamespaceNotFoundException { - when(mockAdrStore.createAdrForNamespace(any(Adr.class))) - .thenThrow(new NamespaceNotFoundException()); - - String adr = "{ \"test\": \"json\" }"; - - given() - .header("Content-Type", "application/json") - .body(adr) - .when() - .post("/calm/namespaces/invalid/adrs") - .then() - .statusCode(404); - - Adr expectedAdr = AdrBuilder.builder() - .adr(adr) - .revision(1) - .namespace("invalid") - .build(); - - verify(mockAdrStore, times(1)).createAdrForNamespace(expectedAdr); + static Stream provideParametersForCreateAdrTests() { + return Stream.of( + Arguments.of("invalid", new NamespaceNotFoundException(), 404, nullValue()), + Arguments.of("invalid", new JsonParseException(), 400, nullValue()), + Arguments.of("valid", null, 201, containsString("/calm/namespaces/valid/adrs/12/revisions/1")) + ); } - @Test - void return_a_400_when_invalid_adr_json_is_provided_on_create_adr() throws NamespaceNotFoundException { - when(mockAdrStore.createAdrForNamespace(any(Adr.class))) - .thenThrow(new JsonParseException()); - String adr = "{ \"test\": im invalid json"; + @ParameterizedTest + @MethodSource("provideParametersForCreateAdrTests") + void respond_correctly_when_creating_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode, Matcher locationHeader) throws NamespaceNotFoundException, JsonProcessingException { + if (exceptionToThrow != null) { + when(mockAdrStore.createAdrForNamespace(any(Adr.class))).thenThrow(exceptionToThrow); + } else { + Adr adr = AdrBuilder.builder() + .adrContent( + AdrContentBuilder.builder() + .title("My ADR") + .status(AdrStatus.DRAFT) + .creationDateTime(LocalDateTime.now()) + .updateDateTime(LocalDateTime.now()) + .build() + ) + .id(12) + .revision(1) + .namespace(namespace) + .build(); + when(mockAdrStore.createAdrForNamespace(any(Adr.class))).thenReturn(adr); + } + + String adr = "{ \"title\": \"My ADR\" }"; given() .header("Content-Type", "application/json") .body(adr) .when() - .post("/calm/namespaces/invalid/adrs") + .post("/calm/namespaces/" + namespace +"/adrs") .then() - .statusCode(400); + .statusCode(expectedStatusCode) + .header("Location", locationHeader); Adr expectedAdr = AdrBuilder.builder() - .adr(adr) - .namespace("invalid") - .revision(1) - .build(); - - verify(mockAdrStore, times(1)).createAdrForNamespace(expectedAdr); - } - - @Test - void return_a_created_with_location_of_adr_when_creating_adr() throws NamespaceNotFoundException { - String adrJson = "{ \"test\": \"json\" }"; - String namespace = "finos"; - - Adr stubbedReturnAdr = AdrBuilder.builder() - .adr(adrJson) + .adrContent( + AdrContentBuilder.builder() + .title("My ADR") + .status(AdrStatus.DRAFT) + .build()) .revision(1) - .id(12) - .namespace(namespace) - .build(); - - when(mockAdrStore.createAdrForNamespace(any(Adr.class))).thenReturn(stubbedReturnAdr); - - given() - .header("Content-Type", "application/json") - .body(adrJson) - .when() - .post("/calm/namespaces/finos/adrs") - .then() - .statusCode(201) - //Derived from stubbed adr in resource - .header("Location", containsString("/calm/namespaces/finos/adrs/12/revisions/1")); - - Adr expectedAdrToCreate = AdrBuilder.builder() - .adr(adrJson) .namespace(namespace) - .revision(1) .build(); - verify(mockAdrStore, times(1)).createAdrForNamespace(expectedAdrToCreate); + verify(mockAdrStore, times(1)).createAdrForNamespace(expectedAdr); } - //Update ADR Tests +// //Update ADR Tests static Stream provideParametersForUpdateAdrTests() { return Stream.of( @@ -157,14 +138,22 @@ static Stream provideParametersForUpdateAdrTests() { @ParameterizedTest @MethodSource("provideParametersForUpdateAdrTests") - void return_a_404_when_invalid_namespace_is_provided_on_update_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { - String adrJson = "{ \"test\": \"json\" }"; + void return_a_404_when_invalid_namespace_is_provided_on_update_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + String adrJson = "{ \"title\": \"My ADR\" }"; if (exceptionToThrow != null) { when(mockAdrStore.updateAdrForNamespace(any(Adr.class))).thenThrow(exceptionToThrow); } else { Adr adr = AdrBuilder.builder() - .adr(adrJson) - .id(2) + .adrContent( + AdrContentBuilder.builder() + .title("My ADR") + .status(AdrStatus.DRAFT) + .creationDateTime(LocalDateTime.now()) + .updateDateTime(LocalDateTime.now()) + .build() + ) + .id(1) + .revision(2) .namespace(namespace) .build(); when(mockAdrStore.updateAdrForNamespace(any(Adr.class))).thenReturn(adr); @@ -174,14 +163,17 @@ void return_a_404_when_invalid_namespace_is_provided_on_update_adr(String namesp .header("Content-Type", "application/json") .body(adrJson) .when() - .post("/calm/namespaces/invalid/adrs/1") + .post("/calm/namespaces/" + namespace +"/adrs/1") .then() .statusCode(expectedStatusCode); Adr expectedAdr = AdrBuilder.builder() - .adr(adrJson) + .adrContent( + AdrContentBuilder.builder() + .title("My ADR") + .build()) .id(1) - .namespace("invalid") + .namespace(namespace) .build(); verify(mockAdrStore, times(1)).updateAdrForNamespace(expectedAdr); diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java index e852eb581..ca4383ac7 100644 --- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java +++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java @@ -1,5 +1,8 @@ package org.finos.calm.store.mongo; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.mongodb.client.FindIterable; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; @@ -12,9 +15,10 @@ import io.quarkus.test.junit.QuarkusTest; import org.bson.Document; import org.bson.conversions.Bson; -import org.bson.json.JsonParseException; import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; +import org.finos.calm.domain.AdrContentBuilder; +import org.finos.calm.domain.AdrStatus; import org.finos.calm.domain.exception.AdrNotFoundException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; @@ -22,6 +26,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -51,12 +56,23 @@ public class TestMongoAdrStoreShould { @InjectMock MongoNamespaceStore namespaceStore; + private ObjectMapper objectMapper; + private MongoDatabase mongoDatabase; private MongoCollection adrCollection; private MongoAdrStore mongoAdrStore; private final String NAMESPACE = "finos"; - - private final String validJson = "{\"test\": \"test\"}"; + private final Adr simpleAdr = AdrBuilder.builder() + .namespace(NAMESPACE) + .id(42) + .revision(2) + .adrContent(AdrContentBuilder.builder() + .title("My ADR") + .status(AdrStatus.SUPERSEDED) + .creationDateTime(LocalDateTime.now()) + .updateDateTime(LocalDateTime.now()) + .build()) + .build(); @BeforeEach void setup() { @@ -66,6 +82,9 @@ void setup() { when(mongoClient.getDatabase("calmSchemas")).thenReturn(mongoDatabase); when(mongoDatabase.getCollection("adrs")).thenReturn(adrCollection); mongoAdrStore = new MongoAdrStore(mongoClient, counterStore, namespaceStore); + + this.objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); } @Test @@ -139,31 +158,21 @@ void return_a_namespace_exception_when_namespace_does_not_exist_when_creating_an } @Test - void return_a_json_parse_exception_when_an_invalid_json_object_is_presented_when_creating_an_adr() { - when(namespaceStore.namespaceExists(anyString())).thenReturn(true); - when(counterStore.getNextAdrSequenceValue()).thenReturn(42); - Adr adr = AdrBuilder.builder().namespace(NAMESPACE) - .adr("Invalid JSON") - .build(); - - assertThrows(JsonParseException.class, - () -> mongoAdrStore.createAdrForNamespace(adr)); - } - - @Test - void return_created_adr_when_parameters_are_valid() throws NamespaceNotFoundException { + void return_created_adr_when_parameters_are_valid() throws NamespaceNotFoundException, JsonProcessingException { String validNamespace = NAMESPACE; int sequenceNumber = 42; when(namespaceStore.namespaceExists(anyString())).thenReturn(true); when(counterStore.getNextAdrSequenceValue()).thenReturn(sequenceNumber); - Adr adrToCreate = AdrBuilder.builder().adr(validJson) + Adr adrToCreate = AdrBuilder.builder() + .adrContent(AdrContentBuilder.builder().build()) .namespace(validNamespace) .revision(1) .build(); Adr adr = mongoAdrStore.createAdrForNamespace(adrToCreate); - Adr expectedAdr = AdrBuilder.builder().adr(validJson) + Adr expectedAdr = AdrBuilder.builder() + .adrContent(AdrContentBuilder.builder().build()) .namespace(validNamespace) .revision(1) .id(sequenceNumber) @@ -171,7 +180,7 @@ void return_created_adr_when_parameters_are_valid() throws NamespaceNotFoundExce assertThat(adr, is(expectedAdr)); Document expectedDoc = new Document("adrId", adr.id()).append("revisions", - new Document("1", Document.parse(adr.adr()))); + new Document("1", Document.parse(objectMapper.writeValueAsString(adr.adrContent())))); verify(adrCollection).updateOne( eq(Filters.eq("namespace", validNamespace)), @@ -215,7 +224,7 @@ void get_adr_revisions_for_invalid_adr_throws_exception() { } @Test - void get_adr_revisions_for_valid_adr_returns_list_of_revisions() throws NamespaceNotFoundException, AdrNotFoundException { + void get_adr_revisions_for_valid_adr_returns_list_of_revisions() throws NamespaceNotFoundException, AdrNotFoundException, JsonProcessingException { mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder().namespace(NAMESPACE).id(42).build(); @@ -248,17 +257,17 @@ void throw_an_exception_for_an_invalid_adr_when_retrieving_adr_revision() { } @Test - void return_an_adr_revision() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + void return_an_adr_revision() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder().namespace(NAMESPACE) .id(42).revision(1).build(); String adrRevision = mongoAdrStore.getAdrRevision(adr); - assertThat(adrRevision, is(validJson)); + assertThat(adrRevision.replaceAll("\\s+", ""), is(objectMapper.writeValueAsString(simpleAdr.adrContent()).replaceAll("\\s+", ""))); } - private void mockSetupAdrDocumentWithRevisions() { + private void mockSetupAdrDocumentWithRevisions() throws JsonProcessingException { Document mainDocument = setupAdrRevisionDocument(); FindIterable findIterable = Mockito.mock(FindIterable.class); when(namespaceStore.namespaceExists(anyString())).thenReturn(true); @@ -268,10 +277,10 @@ private void mockSetupAdrDocumentWithRevisions() { when(findIterable.first()).thenReturn(mainDocument); } - private Document setupAdrRevisionDocument() { + private Document setupAdrRevisionDocument() throws JsonProcessingException { //Set up an ADR document with 2 ADRs in (one with a valid revision) Map revisionMap = new HashMap<>(); - revisionMap.put("1", Document.parse(validJson)); + revisionMap.put("1", Document.parse(objectMapper.writeValueAsString(simpleAdr.adrContent()))); Document targetStoredAdr = new Document("adrId", 42) .append("revisions", new Document(revisionMap)); @@ -282,7 +291,7 @@ private Document setupAdrRevisionDocument() { } @Test - void throw_an_exception_when_revision_of_adr_does_not_exist() { + void throw_an_exception_when_revision_of_adr_does_not_exist() throws JsonProcessingException { mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder().namespace(NAMESPACE) @@ -346,14 +355,14 @@ void get_adr_for_invalid_namespace_throws_exception() { } @Test - void return_the_latest_adr_revision() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + void return_the_latest_adr_revision() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder().namespace(NAMESPACE) .id(42).revision(1).build(); String adrRevision = mongoAdrStore.getAdr(adr); - assertThat(adrRevision, is(validJson)); + assertThat(adrRevision.replaceAll("\\s+", ""), is(objectMapper.writeValueAsString(simpleAdr.adrContent()).replaceAll("\\s+", ""))); } @Test @@ -372,7 +381,7 @@ void throw_an_exception_when_update_adr_with_a_namespace_that_doesnt_exists() { } @Test - void throw_an_exception_when_updating_an_adr_that_doesnt_exist() { + void throw_an_exception_when_updating_an_adr_that_doesnt_exist() throws JsonProcessingException { mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder() @@ -391,7 +400,7 @@ void throw_an_exception_when_updating_an_adr_but_no_revisions_exist() { Adr adr = AdrBuilder.builder() .namespace(NAMESPACE) .id(42) - .adr(validJson) + .adrContent(AdrContentBuilder.builder().build()) .build(); assertThrows(AdrRevisionNotFoundException.class, @@ -399,13 +408,13 @@ void throw_an_exception_when_updating_an_adr_but_no_revisions_exist() { } @Test - void return_successfully_when_correctly_updating_an_adr() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + void return_successfully_when_correctly_updating_an_adr() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder() .namespace(NAMESPACE) .id(42) .revision(2) - .adr(validJson) + .adrContent(AdrContentBuilder.builder().build()) .build(); mongoAdrStore.updateAdrForNamespace(adr); From ff9855c7f4e2b613f7214bcf7c4fc7e31b2a4e07 Mon Sep 17 00:00:00 2001 From: Graham Packer Date: Tue, 14 Jan 2025 14:13:58 +0000 Subject: [PATCH 07/12] Use ADR object when getting latest revision (#716) --- .../java/integration/MongoAdrIntegration.java | 16 +++++++++---- .../org/finos/calm/domain/AdrContent.java | 2 -- .../org/finos/calm/resources/AdrResource.java | 23 ++++++++++++++++++- .../java/org/finos/calm/store/AdrStore.java | 2 +- .../finos/calm/store/mongo/MongoAdrStore.java | 9 ++++++-- .../calm/resources/TestAdrResourceShould.java | 21 ++++++++++++----- .../store/mongo/TestMongoAdrStoreShould.java | 8 ++++--- 7 files changed, 61 insertions(+), 20 deletions(-) diff --git a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java index 560b9ed61..e56e71f0f 100644 --- a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java +++ b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java @@ -10,6 +10,7 @@ import org.bson.Document; import org.eclipse.microprofile.config.ConfigProvider; import org.finos.calm.domain.Adr; +import org.finos.calm.domain.AdrBuilder; import org.finos.calm.domain.AdrContent; import org.finos.calm.domain.AdrContentBuilder; import org.finos.calm.domain.AdrDecision; @@ -133,22 +134,27 @@ void end_to_end_verify_get_adr_revision() throws JsonProcessingException { .extract() .body() .as(AdrContent.class); - assertEquals(adrContent, result); } @Test @Order(4) void end_to_end_verify_get_adr() { - AdrContent result = given() + Adr result = given() .when().get("/calm/namespaces/finos/adrs/1") .then() .statusCode(200) .extract() .body() - .as(AdrContent.class); - - assertEquals(adrContent, result); + .as(Adr.class); + + Adr expectedAdr = AdrBuilder.builder() + .namespace("finos") + .id(1) + .revision(1) + .adrContent(adrContent) + .build(); + assertEquals(expectedAdr, result); } @Test diff --git a/calm-hub/src/main/java/org/finos/calm/domain/AdrContent.java b/calm-hub/src/main/java/org/finos/calm/domain/AdrContent.java index f8c231b09..5004ccd81 100644 --- a/calm-hub/src/main/java/org/finos/calm/domain/AdrContent.java +++ b/calm-hub/src/main/java/org/finos/calm/domain/AdrContent.java @@ -18,11 +18,9 @@ public record AdrContent( AdrStatus status, @JsonDeserialize(using = LocalDateTimeDeserializer.class) @JsonSerialize(using = LocalDateTimeSerializer.class) - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss:SSS") LocalDateTime creationDateTime, @JsonDeserialize(using = LocalDateTimeDeserializer.class) @JsonSerialize(using = LocalDateTimeSerializer.class) - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss:SSS") LocalDateTime updateDateTime, String contextAndProblemStatement, List decisionDrivers, diff --git a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java index b47ea1e1a..00c885980 100644 --- a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java +++ b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java @@ -62,7 +62,13 @@ public Response getAdrsForNamespace(@PathParam("namespace") String namespace) { } } - + /** + * Create a new ADR in the DRAFT state + * @param namespace the namespace to create the ADR in + * @param newAdr the new ADR to be created + * @return created response with Location header + * @throws URISyntaxException cannot produce location URL + */ @POST @Path("{namespace}/adrs") @Consumes(MediaType.APPLICATION_JSON) @@ -95,6 +101,14 @@ public Response createAdrForNamespace(@PathParam("namespace") String namespace, } } + /** + * Update an existing ADRs contents + * @param namespace the namespace the ADR is in + * @param adrId the ID of the ADR + * @param newAdrRevision the new ADR content + * @return created with a Location header + * @throws URISyntaxException cannot produce Location header + */ @POST @Path("{namespace}/adrs/{adrId}") @Consumes(MediaType.APPLICATION_JSON) @@ -131,6 +145,7 @@ public Response updateAdrForNamespace(@PathParam("namespace") String namespace, } } + @GET @Path("{namespace}/adrs/{adrId}") @Produces(MediaType.APPLICATION_JSON) @@ -155,6 +170,8 @@ public Response getAdr(@PathParam("namespace") String namespace, @PathParam("adr } catch (AdrRevisionNotFoundException e) { logger.error("Could not get latest revision of ADR [{}]", adrId, e); return invalidLatestRevisionResponse(adrId); + } catch(JsonProcessingException e) { + return invalidGetAdrResponse(adrId); } } @@ -222,6 +239,10 @@ private Response invalidAdrJsonResponse(String adrJson) { return Response.status(Response.Status.BAD_REQUEST).entity("The ADR JSON could not be parsed: " + adrJson).build(); } + private Response invalidGetAdrResponse(int adrId) { + return Response.status(Response.Status.NOT_FOUND).entity("Could not process ADR when getting it: " + adrId).build(); + } + private Response invalidAdrResponse(int adrId) { return Response.status(Response.Status.NOT_FOUND).entity("Invalid adrId provided: " + adrId).build(); } diff --git a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java index 516382bbb..dbf9332de 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java @@ -12,7 +12,7 @@ public interface AdrStore { List getAdrsForNamespace(String namespace) throws NamespaceNotFoundException; Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException, JsonProcessingException; - String getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; + Adr getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException; List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException; String getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException; diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java index 5ac3dacf1..04db58b34 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java @@ -89,7 +89,7 @@ public Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException, Jso } @Override - public String getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + public Adr getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { Document result = retrieveAdrRevisions(adr); List adrs = (List) result.get("adrs"); for (Document adrDoc : adrs) { @@ -101,7 +101,12 @@ public String getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundExce // Return the ADR JSON blob for the specified revision Document revisionDoc = (Document) revisions.get(String.valueOf(latestRevision)); log.info("RevisionDoc: [{}], Revision: [{}]", adrDoc.get("revisions"), latestRevision); - return revisionDoc.toJson(); + return AdrBuilder.builder() + .namespace(adr.namespace()) + .id(adr.id()) + .revision(latestRevision) + .adrContent(objectMapper.readValue(revisionDoc.toJson(), AdrContent.class)) + .build(); } } throw new AdrNotFoundException(); diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java index c4e9dc3d5..d37ec45ed 100644 --- a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java +++ b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java @@ -81,7 +81,7 @@ static Stream provideParametersForCreateAdrTests() { @ParameterizedTest @MethodSource("provideParametersForCreateAdrTests") - void respond_correctly_when_creating_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode, Matcher locationHeader) throws NamespaceNotFoundException, JsonProcessingException { + void respond_correctly_when_creating_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode, Matcher locationHeader) throws NamespaceNotFoundException, JsonProcessingException { if (exceptionToThrow != null) { when(mockAdrStore.createAdrForNamespace(any(Adr.class))).thenThrow(exceptionToThrow); } else { @@ -265,21 +265,30 @@ void respond_correctly_to_get_adr_revision(String namespace, Throwable exception @ParameterizedTest @MethodSource("provideParametersForGetAdrRevisionTests") - void respond_correctly_to_get_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + void respond_correctly_to_get_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + Adr adr = AdrBuilder.builder() + .namespace(namespace) + .id(12) + .revision(2) + .adrContent(AdrContentBuilder.builder().title("My title").build()) + .build(); + if (exceptionToThrow != null) { when(mockAdrStore.getAdr(any(Adr.class))).thenThrow(exceptionToThrow); } else { - String adr = "{ \"test\": \"json\" }"; when(mockAdrStore.getAdr(any(Adr.class))).thenReturn(adr); } if (expectedStatusCode == 200) { - given() + Adr actualAdr = given() .when() .get("/calm/namespaces/" + namespace + "/adrs/12") .then() .statusCode(expectedStatusCode) - .body(equalTo("{ \"test\": \"json\" }")); + .extract() + .body() + .as(Adr.class); + assertEquals(adr, actualAdr); } else { given() .when() @@ -301,7 +310,7 @@ private void verifyExpectedGetAdrRevision(String namespace) throws NamespaceNotF verify(mockAdrStore, times(1)).getAdrRevision(expectedAdrToRetrieve); } - private void verifyExpectedGetAdr(String namespace) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + private void verifyExpectedGetAdr(String namespace) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { Adr expectedAdrToRetrieve = AdrBuilder.builder() .namespace(namespace) .id(12) diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java index ca4383ac7..e5f04ce88 100644 --- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java +++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java @@ -36,6 +36,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -359,10 +360,11 @@ void return_the_latest_adr_revision() throws NamespaceNotFoundException, AdrNotF mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder().namespace(NAMESPACE) - .id(42).revision(1).build(); + .id(42).build(); - String adrRevision = mongoAdrStore.getAdr(adr); - assertThat(adrRevision.replaceAll("\\s+", ""), is(objectMapper.writeValueAsString(simpleAdr.adrContent()).replaceAll("\\s+", ""))); + Adr latestAdr = mongoAdrStore.getAdr(adr); + Adr expectedAdr = AdrBuilder.builder(simpleAdr).revision(1).build(); + assertEquals(expectedAdr, latestAdr); } @Test From baa31548098fce20496b17a41a59372536bf2f59 Mon Sep 17 00:00:00 2001 From: Graham Packer Date: Tue, 14 Jan 2025 21:07:23 +0000 Subject: [PATCH 08/12] Refactor ADRStore (#716 ) --- .../java/integration/MongoAdrIntegration.java | 13 +- .../org/finos/calm/resources/AdrResource.java | 18 ++ .../java/org/finos/calm/store/AdrStore.java | 2 +- .../finos/calm/store/mongo/MongoAdrStore.java | 192 ++++++++---------- .../calm/resources/TestAdrResourceShould.java | 20 +- .../store/mongo/TestMongoAdrStoreShould.java | 5 +- 6 files changed, 132 insertions(+), 118 deletions(-) diff --git a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java index e56e71f0f..5c0233c60 100644 --- a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java +++ b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java @@ -127,14 +127,21 @@ void end_to_end_verify_create_an_adr() throws JsonProcessingException { @Test @Order(3) void end_to_end_verify_get_adr_revision() throws JsonProcessingException { - AdrContent result = given() + Adr expectedAdr = AdrBuilder.builder() + .namespace("finos") + .id(1) + .revision(1) + .adrContent(adrContent) + .build(); + + Adr actualAdr = given() .when().get("/calm/namespaces/finos/adrs/1/revisions/1") .then() .statusCode(200) .extract() .body() - .as(AdrContent.class); - assertEquals(adrContent, result); + .as(Adr.class); + assertEquals(expectedAdr, actualAdr); } @Test diff --git a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java index 00c885980..75e0e5836 100644 --- a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java +++ b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java @@ -11,6 +11,10 @@ import jakarta.ws.rs.core.Response; import org.bson.json.JsonParseException; import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; import org.finos.calm.domain.AdrContent; @@ -153,6 +157,12 @@ public Response updateAdrForNamespace(@PathParam("namespace") String namespace, summary = "Retrieve the latest revision of an ADR", description = "Retrieve the latest revision of an ADR" ) + @APIResponses(value = { + @APIResponse( + responseCode = "200", + content = @Content(schema = @Schema(implementation = Adr.class)) + ) + }) public Response getAdr(@PathParam("namespace") String namespace, @PathParam("adrId") int adrId) { Adr adr = AdrBuilder.builder() .namespace(namespace) @@ -206,6 +216,12 @@ public Response getAdrRevisions(@PathParam("namespace") String namespace, @PathP summary = "Retrieve a specific revision of an ADR", description = "Retrieve a specific revision of an ADR" ) + @APIResponses(value = { + @APIResponse( + responseCode = "200", + content = @Content(schema = @Schema(implementation = Adr.class)) + ) + }) public Response getAdrRevision(@PathParam("namespace") String namespace, @PathParam("adrId") int adrId, @PathParam("revision") int revision) { Adr adr = AdrBuilder.builder() .namespace(namespace) @@ -224,6 +240,8 @@ public Response getAdrRevision(@PathParam("namespace") String namespace, @PathPa } catch (AdrRevisionNotFoundException e) { logger.error("Invalid revision [{}] when getting an ADR", revision, e); return invalidRevisionResponse(revision); + } catch(JsonProcessingException e) { + return invalidGetAdrResponse(adrId); } } diff --git a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java index dbf9332de..458e0449b 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java @@ -14,6 +14,6 @@ public interface AdrStore { Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException, JsonProcessingException; Adr getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException; List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException; - String getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; + Adr getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException; Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException; } diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java index 04db58b34..3d623f968 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java @@ -25,8 +25,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.Set; @ApplicationScoped @@ -61,13 +61,8 @@ public List getAdrsForNamespace(String namespace) throws NamespaceNotFo } List adrs = namespaceDocument.getList("adrs", Document.class); - List adrIds = new ArrayList<>(); - for (Document adr : adrs) { - adrIds.add(adr.getInteger("adrId")); - } - - return adrIds; + return adrs.stream().map(adr -> adr.getInteger("adrId")).toList(); } @Override @@ -85,134 +80,117 @@ public Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException, Jso Updates.push("adrs", adrDocument), new UpdateOptions().upsert(true)); - return AdrBuilder.builder(adr).id(id).build(); + return AdrBuilder.builder(adr).id(id).build(); } @Override public Adr getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { - Document result = retrieveAdrRevisions(adr); - List adrs = (List) result.get("adrs"); - for (Document adrDoc : adrs) { - if (adr.id() == adrDoc.getInteger("adrId")) { - // Extract the revisions map from the matching adrContent - Document revisions = (Document) adrDoc.get("revisions"); - int latestRevision = getLatestRevision(adr, revisions); - - // Return the ADR JSON blob for the specified revision - Document revisionDoc = (Document) revisions.get(String.valueOf(latestRevision)); - log.info("RevisionDoc: [{}], Revision: [{}]", adrDoc.get("revisions"), latestRevision); - return AdrBuilder.builder() - .namespace(adr.namespace()) - .id(adr.id()) - .revision(latestRevision) - .adrContent(objectMapper.readValue(revisionDoc.toJson(), AdrContent.class)) - .build(); - } - } - throw new AdrNotFoundException(); - } - - private int getLatestRevision(Adr adr, Document revisionsDoc) throws AdrRevisionNotFoundException { - if(revisionsDoc == null || revisionsDoc.isEmpty()) { - log.error("Could not find the latest revision of ADR [{}]", adr.id()); - throw new AdrRevisionNotFoundException(); - } - - Set revisionKeys = revisionsDoc.keySet(); - return revisionKeys.stream() - .map(Integer::parseInt) - .mapToInt(i -> i) - .max() - .getAsInt(); + Document adrDoc = retrieveAdrDoc(adr); + return retrieveLatestRevision(adr, adrDoc); } @Override public List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException { - Document result = retrieveAdrRevisions(adr); - - List adrs = (List) result.get("adrs"); - for (Document adrDoc : adrs) { - if (adr.id() == adrDoc.getInteger("adrId")) { - // Extract the revisions map from the matching adrContent - Document revisions = (Document) adrDoc.get("revisions"); - Set revisionKeys = revisions.keySet(); - - return revisionKeys.stream().map(Integer::parseInt).toList(); - } + try { + Document adrDoc = retrieveAdrDoc(adr); + Document revisions = retrieveRevisionsDoc(adrDoc, adr); + return revisions.keySet() + .stream() + .map(Integer::parseInt) + .toList(); + + } catch (AdrRevisionNotFoundException e) { + return List.of(); } - - throw new AdrNotFoundException(); } @Override - public String getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { - Document result = retrieveAdrRevisions(adr); - - List adrs = (List) result.get("adrs"); - for (Document adrDoc : adrs) { - if (adr.id() == adrDoc.getInteger("adrId")) { - // Retrieve the revisions map from the matching adrContent - Document revisions = (Document) adrDoc.get("revisions"); - - if(revisions == null || revisions.isEmpty()) { - //ADR Revisions collection not initialized - throw new AdrRevisionNotFoundException(); - } - - // Return the ADR JSON blob for the specified revision - Document revisionDoc = (Document) revisions.get(String.valueOf(adr.revision())); - log.info("RevisionDoc: [{}], Revision: [{}]", adrDoc.get("revisions"), adr.revision()); - if(revisionDoc == null) { - throw new AdrRevisionNotFoundException(); - } - return revisionDoc.toJson(); - } + public Adr getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + Document adrDoc = retrieveAdrDoc(adr); + Document revisionsDoc = retrieveRevisionsDoc(adrDoc, adr); + + // Return the ADR JSON blob for the specified revision + Document revisionDoc = (Document) revisionsDoc.get(String.valueOf(adr.revision())); + log.info("RevisionDoc: [{}], Revision: [{}]", adrDoc.get("revisions"), adr.revision()); + if(revisionDoc == null) { + throw new AdrRevisionNotFoundException(); } - //ADR Revisions is empty, no revisions to find - throw new AdrRevisionNotFoundException(); + return AdrBuilder.builder(adr) + .adrContent(objectMapper.readValue(revisionDoc.toJson(), AdrContent.class)) + .build(); } @Override public Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { - Document result = retrieveAdrRevisions(adr); - List adrs = (List) result.get("adrs"); - for (Document adrDoc : adrs) { - if (adr.id() == adrDoc.getInteger("adrId")) { - // Extract the revisions map from the matching adrContent - Document revisions = (Document) adrDoc.get("revisions"); - int latestRevision = getLatestRevision(adr, revisions); - Document revisionDoc = (Document) revisions.get(String.valueOf(latestRevision)); - AdrContent latestAdrContent = objectMapper.readValue(revisionDoc.toJson(), AdrContent.class); - AdrContent newAdrContent = AdrContentBuilder.builder(adr.adrContent()) - .status(latestAdrContent.status()) - .creationDateTime(latestAdrContent.creationDateTime()) - .build(); - - Adr newAdr = AdrBuilder.builder(adr).adrContent(newAdrContent).revision(latestRevision + 1).build(); - - writeAdrToMongo(newAdr); - return newAdr; - - } - } - throw new AdrNotFoundException(); + Document adrDoc = retrieveAdrDoc(adr); + Adr latestRevision = retrieveLatestRevision(adr, adrDoc); + + int newRevision = latestRevision.revision() + 1; + Adr newAdr = AdrBuilder.builder(adr) + .adrContent(AdrContentBuilder.builder(adr.adrContent()) + .status(latestRevision.adrContent().status()) + .creationDateTime(latestRevision.adrContent().creationDateTime()) + .build()) + .revision(newRevision) + .build(); + + writeAdrToMongo(newAdr); + return newAdr; } - private Document retrieveAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException { - if(!namespaceStore.namespaceExists(adr.namespace())) { + private List retrieveAdrsDocs(String namespace) throws NamespaceNotFoundException, AdrNotFoundException { + if(!namespaceStore.namespaceExists(namespace)) { throw new NamespaceNotFoundException(); } - Bson filter = new Document("namespace", adr.namespace()); + Bson filter = new Document("namespace", namespace); Bson projection = Projections.fields(Projections.include("adrs")); Document result = adrCollection.find(filter).projection(projection).first(); - if (result == null) { + if(result == null) { throw new AdrNotFoundException(); } - return result; + return (List) result.get("adrs"); + } + + private Document retrieveAdrDoc(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException { + List adrsDoc = retrieveAdrsDocs(adr.namespace()); + Optional adrOpt = adrsDoc.stream().filter(adrDoc -> adr.id() == adrDoc.getInteger("adrId")).findAny(); + return adrOpt.orElseThrow(AdrNotFoundException::new); + } + + private Document retrieveRevisionsDoc(Document adrDoc, Adr adr) throws AdrRevisionNotFoundException { + Document revisionsDoc = (Document) adrDoc.get("revisions"); + + if(revisionsDoc == null || revisionsDoc.isEmpty()) { + log.error("Could not find the latest revision of ADR [{}]", adr.id()); + throw new AdrRevisionNotFoundException(); + } + + return revisionsDoc; + } + + private Adr retrieveLatestRevision(Adr adr, Document adrDoc) throws AdrRevisionNotFoundException, JsonProcessingException { + Document revisionsDoc = retrieveRevisionsDoc(adrDoc, adr); + + Set revisionKeys = revisionsDoc.keySet(); + int latestRevision = revisionKeys.stream() + .map(Integer::parseInt) + .mapToInt(i -> i) + .max() + .getAsInt(); + + // Return the ADR JSON blob for the specified revision + Document revisionDoc = (Document) revisionsDoc.get(String.valueOf(latestRevision)); + log.info("RevisionDoc: [{}], Revision: [{}]", revisionDoc, latestRevision); + return AdrBuilder.builder() + .namespace(adr.namespace()) + .id(adr.id()) + .revision(latestRevision) + .adrContent(objectMapper.readValue(revisionDoc.toJson(), AdrContent.class)) + .build(); } private void writeAdrToMongo(Adr adr) throws AdrNotFoundException, JsonProcessingException { @@ -225,7 +203,7 @@ private void writeAdrToMongo(Adr adr) throws AdrNotFoundException, JsonProcessin try { adrCollection.updateOne(filter, update, new UpdateOptions().upsert(true)); - } catch (MongoWriteException ex) { + } catch(MongoWriteException ex) { log.error("Failed to write ADR to mongo [{}]", adr, ex); throw new AdrNotFoundException(); } diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java index d37ec45ed..57e77cf56 100644 --- a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java +++ b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java @@ -237,21 +237,31 @@ static Stream provideParametersForGetAdrRevisionTests() { @ParameterizedTest @MethodSource("provideParametersForGetAdrRevisionTests") - void respond_correctly_to_get_adr_revision(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + void respond_correctly_to_get_adr_revision(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + Adr adr = AdrBuilder.builder() + .namespace(namespace) + .id(12) + .revision(1) + .adrContent(AdrContentBuilder.builder().title("My title").build()) + .build(); + if (exceptionToThrow != null) { when(mockAdrStore.getAdrRevision(any(Adr.class))).thenThrow(exceptionToThrow); } else { - String adr = "{ \"test\": \"json\" }"; when(mockAdrStore.getAdrRevision(any(Adr.class))).thenReturn(adr); } if (expectedStatusCode == 200) { - given() + Adr actualAdr = given() .when() .get("/calm/namespaces/" + namespace + "/adrs/12/revisions/1") .then() .statusCode(expectedStatusCode) - .body(equalTo("{ \"test\": \"json\" }")); + .extract() + .body() + .as(Adr.class); + + assertEquals(adr, actualAdr); } else { given() .when() @@ -300,7 +310,7 @@ void respond_correctly_to_get_adr(String namespace, Throwable exceptionToThrow, verifyExpectedGetAdr(namespace); } - private void verifyExpectedGetAdrRevision(String namespace) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + private void verifyExpectedGetAdrRevision(String namespace) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { Adr expectedAdrToRetrieve = AdrBuilder.builder() .namespace(namespace) .id(12) diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java index e5f04ce88..987c211ea 100644 --- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java +++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java @@ -264,8 +264,9 @@ void return_an_adr_revision() throws NamespaceNotFoundException, AdrNotFoundExce Adr adr = AdrBuilder.builder().namespace(NAMESPACE) .id(42).revision(1).build(); - String adrRevision = mongoAdrStore.getAdrRevision(adr); - assertThat(adrRevision.replaceAll("\\s+", ""), is(objectMapper.writeValueAsString(simpleAdr.adrContent()).replaceAll("\\s+", ""))); + Adr adrRevision = mongoAdrStore.getAdrRevision(adr); + Adr expectedAdrRevision = AdrBuilder.builder(simpleAdr).revision(1).build(); + assertEquals(expectedAdrRevision, adrRevision); } private void mockSetupAdrDocumentWithRevisions() throws JsonProcessingException { From a2198dbc998203f57d167f665662add401ed1152 Mon Sep 17 00:00:00 2001 From: Graham Packer Date: Wed, 15 Jan 2025 13:40:14 +0000 Subject: [PATCH 09/12] Add endpoint to change ADR status. Added missing tests. Refactored. (#716) --- .../java/integration/MongoAdrIntegration.java | 24 ++++- .../domain/exception/AdrPersistenceError.java | 4 + .../org/finos/calm/resources/AdrResource.java | 77 ++++++++++++++- .../java/org/finos/calm/store/AdrStore.java | 7 +- .../finos/calm/store/mongo/MongoAdrStore.java | 43 ++++++--- .../calm/resources/TestAdrResourceShould.java | 64 ++++++++++++- .../store/mongo/TestMongoAdrStoreShould.java | 94 ++++++++++++++++++- 7 files changed, 287 insertions(+), 26 deletions(-) create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/exception/AdrPersistenceError.java diff --git a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java index 5c0233c60..6615b0d70 100644 --- a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java +++ b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java @@ -12,7 +12,6 @@ import org.finos.calm.domain.Adr; import org.finos.calm.domain.AdrBuilder; import org.finos.calm.domain.AdrContent; -import org.finos.calm.domain.AdrContentBuilder; import org.finos.calm.domain.AdrDecision; import org.finos.calm.domain.AdrDecisionBuilder; import org.finos.calm.domain.AdrLinkBuilder; @@ -47,10 +46,8 @@ public class MongoAdrIntegration { ObjectMapper objectMapper; private static final Logger logger = LoggerFactory.getLogger(MongoAdrIntegration.class); - public static final String ADR = "{\"name\": \"test-adrContent\"}"; private final String TITLE = "My ADR"; - private final String DRAFT = "DRAFT"; private final String PROBLEM_STATEMENT = "My problem is..."; private final List DECISION_DRIVERS = List.of("a", "b", "c"); private final AdrOption OPTION_A = AdrOptionBuilder.builder().name("Option 1").description("optionDescription") @@ -189,4 +186,25 @@ void end_to_end_verify_get_revisions() { } + @Test + @Order(7) + void end_to_end_verify_update_an_adr_status() throws JsonProcessingException { + given() + .when().post("/calm/namespaces/finos/adrs/1/status/PROPOSED") + .then() + .statusCode(201) + .header("Location", containsString("calm/namespaces/finos/adrs/1")); + } + + @Test + @Order(8) + void end_to_end_verify_status_changed() throws JsonProcessingException { + + given() + .when().get("/calm/namespaces/finos/adrs/1/revisions/3") + .then() + .statusCode(200) + .body("adrContent.status", equalTo("PROPOSED")); + } + } \ No newline at end of file diff --git a/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrPersistenceError.java b/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrPersistenceError.java new file mode 100644 index 000000000..31d055653 --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrPersistenceError.java @@ -0,0 +1,4 @@ +package org.finos.calm.domain.exception; + +public class AdrPersistenceError extends Throwable { +} diff --git a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java index 75e0e5836..291fe513a 100644 --- a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java +++ b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java @@ -22,6 +22,7 @@ import org.finos.calm.domain.NewAdr; import org.finos.calm.domain.ValueWrapper; import org.finos.calm.domain.exception.AdrNotFoundException; +import org.finos.calm.domain.exception.AdrPersistenceError; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; @@ -146,10 +147,18 @@ public Response updateAdrForNamespace(@PathParam("namespace") String namespace, } catch(AdrRevisionNotFoundException e) { logger.error("No existing revision of ADR [{}] found", adrId, e); return invalidLatestRevisionResponse(adrId); + } catch(AdrPersistenceError e) { + logger.error("Error saving updated ADR [{}]", adr); + return serverErrorSavingAdr(adrId); } } - + /** + * Gets the latest revision of an ADR + * @param namespace the namespace the ADR belongs to + * @param adrId the ID of the requested ADR + * @return the requested ADR document + */ @GET @Path("{namespace}/adrs/{adrId}") @Produces(MediaType.APPLICATION_JSON) @@ -185,6 +194,12 @@ public Response getAdr(@PathParam("namespace") String namespace, @PathParam("adr } } + /** + * Gets the list of revisions of an ADR + * @param namespace the namespace the ADR belongs to + * @param adrId the ID of the requested ADR + * @return a list of revision numbers + */ @GET @Path("{namespace}/adrs/{adrId}/revisions") @Produces(MediaType.APPLICATION_JSON) @@ -206,9 +221,19 @@ public Response getAdrRevisions(@PathParam("namespace") String namespace, @PathP } catch (AdrNotFoundException e) { logger.error("Invalid ADR [{}] when getting versions of ADR", adrId, e); return invalidAdrResponse(adrId); + } catch(AdrRevisionNotFoundException e) { + logger.error("Could not find any revisions of ADR: [{}]", adrId, e); + return invalidAdrRevisions(adrId); } } + /** + * Gets a specific revision of an ADR + * @param namespace the namespace the ADR belongs to + * @param adrId the ID of the requested ADR + * @param revision the revision of the ADR being requested + * @return the requested revision of the requested ADR + */ @GET @Path("{namespace}/adrs/{adrId}/revisions/{revision}") @Produces(MediaType.APPLICATION_JSON) @@ -245,6 +270,48 @@ public Response getAdrRevision(@PathParam("namespace") String namespace, @PathPa } } + /** + * Update the status of an existing ADR + * @param namespace the namespace the ADR is in + * @param adrId the ID of the ADR + * @param adrStatus the new status of the ADR + * @return created with a Location header + * @throws URISyntaxException cannot produce Location header + */ + @POST + @Path("{namespace}/adrs/{adrId}/status/{adrStatus}") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Update the status of ADR for namespace", + description = "Updates the status of an ADR for a given namespace. Creates a new revision." + ) + public Response updateAdrStatusForNamespace(@PathParam("namespace") String namespace, @PathParam("adrId") int adrId, @PathParam("adrStatus") AdrStatus adrStatus) throws URISyntaxException { + + Adr adr = AdrBuilder.builder() + .namespace(namespace) + .id(adrId) + .build(); + + try { + return adrWithLocationResponse(store.updateAdrStatus(adr, adrStatus)); + } catch (NamespaceNotFoundException e) { + logger.error("Invalid namespace [{}] when updating status of ADR", namespace, e); + return invalidNamespaceResponse(namespace); + } catch (JsonParseException | JsonProcessingException e) { + logger.error("Cannot parse existing ADR Revision for namespace [{}] while updating status of ADR [{}].", namespace, adrId, e); + return invalidAdrJsonResponse(namespace); + } catch(AdrNotFoundException e) { + logger.error("Invalid ADR [{}] when updating the status of ADR", adrId, e); + return invalidAdrResponse(adrId); + } catch(AdrRevisionNotFoundException e) { + logger.error("No existing revision of ADR [{}] found", adrId, e); + return invalidLatestRevisionResponse(adrId); + } catch(AdrPersistenceError e) { + logger.error("Error saving updated ADR [{}]", adr); + return serverErrorSavingAdr(adrId); + } + } + private Response adrWithLocationResponse(Adr adr) throws URISyntaxException { return Response.created(new URI("/calm/namespaces/" + adr.namespace() + "/adrs/" + adr.id() + "/revisions/" + adr.revision())).build(); } @@ -272,4 +339,12 @@ private Response invalidRevisionResponse(int revision) { private Response invalidLatestRevisionResponse(int adrId) { return Response.status(Response.Status.NOT_FOUND).entity("Latest revision not found for ADR: [{}]: " + adrId).build(); } + + private Response invalidAdrRevisions(int adrId) { + return Response.status(Response.Status.NOT_FOUND).entity("Revisions not found for ADR: [{}]: " + adrId).build(); + } + + private Response serverErrorSavingAdr(int adrId) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Could not save update of ADR: [{}]:" + adrId).build(); + } } diff --git a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java index 458e0449b..700516a85 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java @@ -2,7 +2,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import org.finos.calm.domain.Adr; +import org.finos.calm.domain.AdrStatus; import org.finos.calm.domain.exception.AdrNotFoundException; +import org.finos.calm.domain.exception.AdrPersistenceError; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; @@ -13,7 +15,8 @@ public interface AdrStore { List getAdrsForNamespace(String namespace) throws NamespaceNotFoundException; Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException, JsonProcessingException; Adr getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException; - List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException; + List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; Adr getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException; - Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException; + Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError; + Adr updateAdrStatus(Adr adr, AdrStatus adrStatus) throws AdrNotFoundException, NamespaceNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError; } diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java index 3d623f968..e0ec07b3d 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java @@ -18,7 +18,9 @@ import org.finos.calm.domain.AdrBuilder; import org.finos.calm.domain.AdrContent; import org.finos.calm.domain.AdrContentBuilder; +import org.finos.calm.domain.AdrStatus; import org.finos.calm.domain.exception.AdrNotFoundException; +import org.finos.calm.domain.exception.AdrPersistenceError; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; @@ -90,18 +92,13 @@ public Adr getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundExcepti } @Override - public List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException { - try { - Document adrDoc = retrieveAdrDoc(adr); - Document revisions = retrieveRevisionsDoc(adrDoc, adr); - return revisions.keySet() - .stream() - .map(Integer::parseInt) - .toList(); - - } catch (AdrRevisionNotFoundException e) { - return List.of(); - } + public List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { + Document adrDoc = retrieveAdrDoc(adr); + Document revisions = retrieveRevisionsDoc(adrDoc, adr); + return revisions.keySet() + .stream() + .map(Integer::parseInt) + .toList(); } @Override @@ -121,7 +118,7 @@ public Adr getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoun } @Override - public Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + public Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { Document adrDoc = retrieveAdrDoc(adr); Adr latestRevision = retrieveLatestRevision(adr, adrDoc); @@ -138,6 +135,22 @@ public Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, Adr return newAdr; } + @Override + public Adr updateAdrStatus(Adr adr, AdrStatus adrStatus) throws AdrNotFoundException, NamespaceNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { + Document adrDoc = retrieveAdrDoc(adr); + Adr latestRevision = retrieveLatestRevision(adr, adrDoc); + + int newRevisionNum = latestRevision.revision() + 1; + Adr newRevision = AdrBuilder.builder(latestRevision) + .revision(newRevisionNum) + .adrContent(AdrContentBuilder.builder(latestRevision.adrContent()) + .status(adrStatus) + .build()) + .build(); + writeAdrToMongo(newRevision); + return newRevision; + } + private List retrieveAdrsDocs(String namespace) throws NamespaceNotFoundException, AdrNotFoundException { if(!namespaceStore.namespaceExists(namespace)) { throw new NamespaceNotFoundException(); @@ -193,7 +206,7 @@ private Adr retrieveLatestRevision(Adr adr, Document adrDoc) throws AdrRevisionN .build(); } - private void writeAdrToMongo(Adr adr) throws AdrNotFoundException, JsonProcessingException { + private void writeAdrToMongo(Adr adr) throws JsonProcessingException, AdrPersistenceError { Document adrDocument = Document.parse(objectMapper.writeValueAsString(adr.adrContent())); Document filter = new Document("namespace", adr.namespace()) @@ -205,7 +218,7 @@ private void writeAdrToMongo(Adr adr) throws AdrNotFoundException, JsonProcessin adrCollection.updateOne(filter, update, new UpdateOptions().upsert(true)); } catch(MongoWriteException ex) { log.error("Failed to write ADR to mongo [{}]", adr, ex); - throw new AdrNotFoundException(); + throw new AdrPersistenceError(); } } } diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java index 57e77cf56..7eea5e2fb 100644 --- a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java +++ b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java @@ -1,6 +1,7 @@ package org.finos.calm.resources; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; import org.bson.json.JsonParseException; @@ -9,6 +10,7 @@ import org.finos.calm.domain.AdrContentBuilder; import org.finos.calm.domain.AdrStatus; import org.finos.calm.domain.exception.AdrNotFoundException; +import org.finos.calm.domain.exception.AdrPersistenceError; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; @@ -132,13 +134,15 @@ static Stream provideParametersForUpdateAdrTests() { Arguments.of("invalid", new NamespaceNotFoundException(), 404), Arguments.of("valid", new AdrNotFoundException(), 404), Arguments.of("valid", new AdrRevisionNotFoundException(), 404), + Arguments.of("valid", new JsonParseException(), 400), + Arguments.of("valid", new AdrPersistenceError(), 500), Arguments.of("valid", null, 201) ); } @ParameterizedTest @MethodSource("provideParametersForUpdateAdrTests") - void return_a_404_when_invalid_namespace_is_provided_on_update_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + void return_a_404_when_invalid_namespace_is_provided_on_update_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { String adrJson = "{ \"title\": \"My ADR\" }"; if (exceptionToThrow != null) { when(mockAdrStore.updateAdrForNamespace(any(Adr.class))).thenThrow(exceptionToThrow); @@ -183,13 +187,14 @@ static Stream provideParametersForAdrRevisionTests() { return Stream.of( Arguments.of("invalid", new NamespaceNotFoundException(), 404), Arguments.of("valid", new AdrNotFoundException(), 404), + Arguments.of("valid", new AdrRevisionNotFoundException(), 404), Arguments.of("valid", null, 200) ); } @ParameterizedTest @MethodSource("provideParametersForAdrRevisionTests") - void respond_correctly_to_get_adr_revisions_query(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException { + void respond_correctly_to_get_adr_revisions_query(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { var revisions = List.of(1, 2); if (exceptionToThrow != null) { when(mockAdrStore.getAdrRevisions(any(Adr.class))).thenThrow(exceptionToThrow); @@ -216,7 +221,7 @@ void respond_correctly_to_get_adr_revisions_query(String namespace, Throwable ex verifyExpectedAdrRevisions(namespace); } - private void verifyExpectedAdrRevisions(String namespace) throws NamespaceNotFoundException, AdrNotFoundException { + private void verifyExpectedAdrRevisions(String namespace) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException { Adr expectedAdrToRetrieve = AdrBuilder.builder() .namespace(namespace) .id(12) @@ -231,6 +236,7 @@ static Stream provideParametersForGetAdrRevisionTests() { Arguments.of("invalid", new NamespaceNotFoundException(), 404), Arguments.of("valid", new AdrNotFoundException(), 404), Arguments.of("valid", new AdrRevisionNotFoundException(), 404), + Arguments.of("valid", new InvalidFormatException("error", "value", String.class), 404), Arguments.of("valid", null, 200) ); } @@ -310,6 +316,58 @@ void respond_correctly_to_get_adr(String namespace, Throwable exceptionToThrow, verifyExpectedGetAdr(namespace); } + // Update ADR Status tests + + static Stream provideParametersForUpdateAdrStatusTests() { + return Stream.of( + Arguments.of("invalid", new NamespaceNotFoundException(), 404), + Arguments.of("valid", new AdrNotFoundException(), 404), + Arguments.of("valid", new AdrRevisionNotFoundException(), 404), + Arguments.of("valid", new JsonParseException(), 400), + Arguments.of("valid", new AdrPersistenceError(), 500), + Arguments.of("valid", null, 201) + ); + } + + @ParameterizedTest + @MethodSource("provideParametersForUpdateAdrStatusTests") + void respond_correctly_on_update_adr_status(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { + String adrJson = "{ \"title\": \"My ADR\" }"; + if (exceptionToThrow != null) { + when(mockAdrStore.updateAdrStatus(any(Adr.class), any(AdrStatus.class))).thenThrow(exceptionToThrow); + } else { + Adr adr = AdrBuilder.builder() + .adrContent( + AdrContentBuilder.builder() + .title("My ADR") + .status(AdrStatus.PROPOSED) + .creationDateTime(LocalDateTime.now()) + .updateDateTime(LocalDateTime.now()) + .build() + ) + .id(1) + .revision(2) + .namespace(namespace) + .build(); + when(mockAdrStore.updateAdrStatus(any(Adr.class), any(AdrStatus.class))).thenReturn(adr); + } + + given() + .header("Content-Type", "application/json") + .body(adrJson) + .when() + .post("/calm/namespaces/" + namespace +"/adrs/1/status/PROPOSED") + .then() + .statusCode(expectedStatusCode); + + Adr expectedAdr = AdrBuilder.builder() + .id(1) + .namespace(namespace) + .build(); + + verify(mockAdrStore, times(1)).updateAdrStatus(expectedAdr, AdrStatus.PROPOSED); + } + private void verifyExpectedGetAdrRevision(String namespace) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { Adr expectedAdrToRetrieve = AdrBuilder.builder() .namespace(namespace) diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java index 987c211ea..8885ef83e 100644 --- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java +++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java @@ -3,6 +3,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.mongodb.MongoWriteException; +import com.mongodb.ServerAddress; +import com.mongodb.WriteError; import com.mongodb.client.FindIterable; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; @@ -13,6 +16,7 @@ import com.mongodb.client.model.Updates; import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; +import org.bson.BsonDocument; import org.bson.Document; import org.bson.conversions.Bson; import org.finos.calm.domain.Adr; @@ -20,6 +24,7 @@ import org.finos.calm.domain.AdrContentBuilder; import org.finos.calm.domain.AdrStatus; import org.finos.calm.domain.exception.AdrNotFoundException; +import org.finos.calm.domain.exception.AdrPersistenceError; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.junit.jupiter.api.BeforeEach; @@ -225,7 +230,7 @@ void get_adr_revisions_for_invalid_adr_throws_exception() { } @Test - void get_adr_revisions_for_valid_adr_returns_list_of_revisions() throws NamespaceNotFoundException, AdrNotFoundException, JsonProcessingException { + void get_adr_revisions_for_valid_adr_returns_list_of_revisions() throws NamespaceNotFoundException, AdrNotFoundException, JsonProcessingException, AdrRevisionNotFoundException { mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder().namespace(NAMESPACE).id(42).build(); @@ -411,7 +416,23 @@ void throw_an_exception_when_updating_an_adr_but_no_revisions_exist() { } @Test - void return_successfully_when_correctly_updating_an_adr() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + void throw_an_exception_when_updating_an_adr_but_mongo_cannot_write_update() throws JsonProcessingException { + mockSetupAdrDocumentWithRevisions(); + when(adrCollection.updateOne(any(Bson.class), any(Bson.class), any(UpdateOptions.class))) + .thenThrow(new MongoWriteException(new WriteError(1, "error", new BsonDocument()), new ServerAddress(), List.of())); + + Adr adr = AdrBuilder.builder() + .namespace(NAMESPACE) + .id(42) + .adrContent(AdrContentBuilder.builder().build()) + .build(); + + assertThrows(AdrPersistenceError.class, + () -> mongoAdrStore.updateAdrForNamespace(adr)); + } + + @Test + void return_successfully_when_correctly_updating_an_adr() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder() .namespace(NAMESPACE) @@ -424,4 +445,73 @@ void return_successfully_when_correctly_updating_an_adr() throws NamespaceNotFou verify(adrCollection, times(1)).updateOne(any(Bson.class), any(Bson.class), any(UpdateOptions.class)); } + + @Test + void throw_an_exception_when_updating_status_with_a_namespace_that_doesnt_exists() { + when(namespaceStore.namespaceExists(anyString())).thenReturn(false); + + Adr adr = AdrBuilder.builder() + .namespace(NAMESPACE) + .id(42) + .build(); + + assertThrows(NamespaceNotFoundException.class, + () -> mongoAdrStore.updateAdrStatus(adr, AdrStatus.ACCEPTED)); + + verify(namespaceStore, times(1)).namespaceExists(adr.namespace()); + } + + @Test + void throw_an_exception_when_updating_the_status_of_an_adr_that_doesnt_exist() throws JsonProcessingException { + mockSetupAdrDocumentWithRevisions(); + + Adr adr = AdrBuilder.builder() + .namespace(NAMESPACE) + .id(22) + .build(); + + assertThrows(AdrNotFoundException.class, + () -> mongoAdrStore.updateAdrStatus(adr, AdrStatus.ACCEPTED)); + } + + @Test + void throw_an_exception_when_updating_the_status_of_an_adr_but_no_revisions_exist() { + mockSetupAdrDocumentWithNoRevisions(); + + Adr adr = AdrBuilder.builder() + .namespace(NAMESPACE) + .id(42) + .build(); + + assertThrows(AdrRevisionNotFoundException.class, + () -> mongoAdrStore.updateAdrStatus(adr, AdrStatus.ACCEPTED)); + } + + @Test + void throw_an_exception_when_updating_the_status_of_an_adr_but_mongo_cannot_write_update() throws JsonProcessingException { + mockSetupAdrDocumentWithRevisions(); + when(adrCollection.updateOne(any(Bson.class), any(Bson.class), any(UpdateOptions.class))) + .thenThrow(new MongoWriteException(new WriteError(1, "error", new BsonDocument()), new ServerAddress(), List.of())); + + Adr adr = AdrBuilder.builder() + .namespace(NAMESPACE) + .id(42) + .build(); + + assertThrows(AdrPersistenceError.class, + () -> mongoAdrStore.updateAdrStatus(adr, AdrStatus.PROPOSED)); + } + + @Test + void return_successfully_when_correctly_updating_the_status_of_an_adr() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { + mockSetupAdrDocumentWithRevisions(); + Adr adr = AdrBuilder.builder() + .namespace(NAMESPACE) + .id(42) + .build(); + + mongoAdrStore.updateAdrStatus(adr, AdrStatus.ACCEPTED); + + verify(adrCollection, times(1)).updateOne(any(Bson.class), any(Bson.class), any(UpdateOptions.class)); + } } From 78ff101078bbf0d4fdde4a8e36a4eda94f22a86c Mon Sep 17 00:00:00 2001 From: Graham Packer Date: Wed, 15 Jan 2025 13:45:26 +0000 Subject: [PATCH 10/12] Rename paramatarized test (#716) --- .../java/org/finos/calm/resources/TestAdrResourceShould.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java index 7eea5e2fb..0c1de5352 100644 --- a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java +++ b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java @@ -142,7 +142,7 @@ static Stream provideParametersForUpdateAdrTests() { @ParameterizedTest @MethodSource("provideParametersForUpdateAdrTests") - void return_a_404_when_invalid_namespace_is_provided_on_update_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { + void respond_correctly_to_update_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { String adrJson = "{ \"title\": \"My ADR\" }"; if (exceptionToThrow != null) { when(mockAdrStore.updateAdrForNamespace(any(Adr.class))).thenThrow(exceptionToThrow); From 4fc0f4cb7f5e00f2e7d202f13bc281ab7627b51d Mon Sep 17 00:00:00 2001 From: Graham Packer Date: Fri, 17 Jan 2025 18:19:26 +0000 Subject: [PATCH 11/12] Refactoring to create exception handler. Also creating adr domain package, (#716) --- .../java/integration/MongoAdrIntegration.java | 30 +-- .../java/org/finos/calm/domain/AdrStatus.java | 10 - .../org/finos/calm/domain/{ => adr}/Adr.java | 2 +- .../calm/domain/{ => adr}/AdrContent.java | 12 +- .../calm/domain/{ => adr}/AdrDecision.java | 2 +- .../finos/calm/domain/{ => adr}/AdrLink.java | 2 +- .../calm/domain/{ => adr}/AdrOption.java | 2 +- .../org/finos/calm/domain/adr/AdrStatus.java | 10 + .../finos/calm/domain/{ => adr}/NewAdr.java | 3 +- .../domain/exception/AdrParseException.java | 4 + .../domain/exception/AdrPersistenceError.java | 4 - .../exception/AdrPersistenceException.java | 4 + .../org/finos/calm/resources/AdrResource.java | 189 ++++++++---------- .../java/org/finos/calm/store/AdrStore.java | 18 +- .../finos/calm/store/mongo/MongoAdrStore.java | 84 +++++--- .../calm/resources/TestAdrResourceShould.java | 52 +++-- .../store/mongo/TestMongoAdrStoreShould.java | 39 ++-- 17 files changed, 236 insertions(+), 231 deletions(-) delete mode 100644 calm-hub/src/main/java/org/finos/calm/domain/AdrStatus.java rename calm-hub/src/main/java/org/finos/calm/domain/{ => adr}/Adr.java (90%) rename calm-hub/src/main/java/org/finos/calm/domain/{ => adr}/AdrContent.java (78%) rename calm-hub/src/main/java/org/finos/calm/domain/{ => adr}/AdrDecision.java (84%) rename calm-hub/src/main/java/org/finos/calm/domain/{ => adr}/AdrLink.java (82%) rename calm-hub/src/main/java/org/finos/calm/domain/{ => adr}/AdrOption.java (88%) create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/adr/AdrStatus.java rename calm-hub/src/main/java/org/finos/calm/domain/{ => adr}/NewAdr.java (84%) create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/exception/AdrParseException.java delete mode 100644 calm-hub/src/main/java/org/finos/calm/domain/exception/AdrPersistenceError.java create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/exception/AdrPersistenceException.java diff --git a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java index 6615b0d70..12078ecdc 100644 --- a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java +++ b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java @@ -9,17 +9,17 @@ import io.quarkus.test.junit.TestProfile; import org.bson.Document; import org.eclipse.microprofile.config.ConfigProvider; -import org.finos.calm.domain.Adr; -import org.finos.calm.domain.AdrBuilder; -import org.finos.calm.domain.AdrContent; -import org.finos.calm.domain.AdrDecision; -import org.finos.calm.domain.AdrDecisionBuilder; -import org.finos.calm.domain.AdrLinkBuilder; -import org.finos.calm.domain.AdrOption; -import org.finos.calm.domain.AdrOptionBuilder; -import org.finos.calm.domain.AdrStatus; -import org.finos.calm.domain.NewAdr; -import org.finos.calm.domain.NewAdrBuilder; +import org.finos.calm.domain.adr.Adr; +import org.finos.calm.domain.adr.AdrBuilder; +import org.finos.calm.domain.adr.AdrContent; +import org.finos.calm.domain.adr.AdrDecision; +import org.finos.calm.domain.adr.AdrDecisionBuilder; +import org.finos.calm.domain.adr.AdrLinkBuilder; +import org.finos.calm.domain.adr.AdrOption; +import org.finos.calm.domain.adr.AdrOptionBuilder; +import org.finos.calm.domain.adr.AdrStatus; +import org.finos.calm.domain.adr.NewAdr; +import org.finos.calm.domain.adr.NewAdrBuilder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; @@ -43,7 +43,7 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class MongoAdrIntegration { - ObjectMapper objectMapper; + private ObjectMapper objectMapper; private static final Logger logger = LoggerFactory.getLogger(MongoAdrIntegration.class); @@ -70,7 +70,7 @@ public class MongoAdrIntegration { .links(List.of(AdrLinkBuilder.builder().rel("abc").href("http://abc.com").build())) .build(); - private final AdrContent adrContent = AdrContent.builderFromNewAdr(newAdr).status(AdrStatus.DRAFT).build(); + private final AdrContent adrContent = AdrContent.builderFromNewAdr(newAdr).status(AdrStatus.draft).build(); @BeforeEach public void setupAdrs() { @@ -190,7 +190,7 @@ void end_to_end_verify_get_revisions() { @Order(7) void end_to_end_verify_update_an_adr_status() throws JsonProcessingException { given() - .when().post("/calm/namespaces/finos/adrs/1/status/PROPOSED") + .when().post("/calm/namespaces/finos/adrs/1/status/proposed") .then() .statusCode(201) .header("Location", containsString("calm/namespaces/finos/adrs/1")); @@ -204,7 +204,7 @@ void end_to_end_verify_status_changed() throws JsonProcessingException { .when().get("/calm/namespaces/finos/adrs/1/revisions/3") .then() .statusCode(200) - .body("adrContent.status", equalTo("PROPOSED")); + .body("adrContent.status", equalTo("proposed")); } } \ No newline at end of file diff --git a/calm-hub/src/main/java/org/finos/calm/domain/AdrStatus.java b/calm-hub/src/main/java/org/finos/calm/domain/AdrStatus.java deleted file mode 100644 index 322cb116b..000000000 --- a/calm-hub/src/main/java/org/finos/calm/domain/AdrStatus.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.finos.calm.domain; - -public enum AdrStatus { - DRAFT, - PROPOSED, - ACCEPTED, - SUPERSEDED, - REJECTED, - DEPRECATED; -} diff --git a/calm-hub/src/main/java/org/finos/calm/domain/Adr.java b/calm-hub/src/main/java/org/finos/calm/domain/adr/Adr.java similarity index 90% rename from calm-hub/src/main/java/org/finos/calm/domain/Adr.java rename to calm-hub/src/main/java/org/finos/calm/domain/adr/Adr.java index b9624fa3e..3c7779a2c 100644 --- a/calm-hub/src/main/java/org/finos/calm/domain/Adr.java +++ b/calm-hub/src/main/java/org/finos/calm/domain/adr/Adr.java @@ -1,4 +1,4 @@ -package org.finos.calm.domain; +package org.finos.calm.domain.adr; import io.soabase.recordbuilder.core.RecordBuilder; diff --git a/calm-hub/src/main/java/org/finos/calm/domain/AdrContent.java b/calm-hub/src/main/java/org/finos/calm/domain/adr/AdrContent.java similarity index 78% rename from calm-hub/src/main/java/org/finos/calm/domain/AdrContent.java rename to calm-hub/src/main/java/org/finos/calm/domain/adr/AdrContent.java index 5004ccd81..968bfa804 100644 --- a/calm-hub/src/main/java/org/finos/calm/domain/AdrContent.java +++ b/calm-hub/src/main/java/org/finos/calm/domain/adr/AdrContent.java @@ -1,6 +1,5 @@ -package org.finos.calm.domain; +package org.finos.calm.domain.adr; -import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; @@ -40,12 +39,19 @@ public static AdrContentBuilder builderFromNewAdr(NewAdr newAdr) { .links(newAdr.links()); } + // does not include datetimes in equals @Override public boolean equals(Object o) { if(this == o) return true; if(o == null || getClass() != o.getClass()) return false; AdrContent that = (AdrContent) o; - return Objects.equals(title, that.title) && status == that.status && Objects.equals(contextAndProblemStatement, that.contextAndProblemStatement) && Objects.equals(decisionDrivers, that.decisionDrivers) && Objects.equals(consideredOptions, that.consideredOptions) && Objects.equals(decisionOutcome, that.decisionOutcome) && Objects.equals(links, that.links); + return Objects.equals(title, that.title) && + status == that.status && + Objects.equals(contextAndProblemStatement, that.contextAndProblemStatement) && + Objects.equals(decisionDrivers, that.decisionDrivers) && + Objects.equals(consideredOptions, that.consideredOptions) && + Objects.equals(decisionOutcome, that.decisionOutcome) && + Objects.equals(links, that.links); } @Override diff --git a/calm-hub/src/main/java/org/finos/calm/domain/AdrDecision.java b/calm-hub/src/main/java/org/finos/calm/domain/adr/AdrDecision.java similarity index 84% rename from calm-hub/src/main/java/org/finos/calm/domain/AdrDecision.java rename to calm-hub/src/main/java/org/finos/calm/domain/adr/AdrDecision.java index 693edf9eb..40ba27445 100644 --- a/calm-hub/src/main/java/org/finos/calm/domain/AdrDecision.java +++ b/calm-hub/src/main/java/org/finos/calm/domain/adr/AdrDecision.java @@ -1,4 +1,4 @@ -package org.finos.calm.domain; +package org.finos.calm.domain.adr; import io.soabase.recordbuilder.core.RecordBuilder; diff --git a/calm-hub/src/main/java/org/finos/calm/domain/AdrLink.java b/calm-hub/src/main/java/org/finos/calm/domain/adr/AdrLink.java similarity index 82% rename from calm-hub/src/main/java/org/finos/calm/domain/AdrLink.java rename to calm-hub/src/main/java/org/finos/calm/domain/adr/AdrLink.java index 99c5c8088..ab0e19a11 100644 --- a/calm-hub/src/main/java/org/finos/calm/domain/AdrLink.java +++ b/calm-hub/src/main/java/org/finos/calm/domain/adr/AdrLink.java @@ -1,4 +1,4 @@ -package org.finos.calm.domain; +package org.finos.calm.domain.adr; import io.soabase.recordbuilder.core.RecordBuilder; diff --git a/calm-hub/src/main/java/org/finos/calm/domain/AdrOption.java b/calm-hub/src/main/java/org/finos/calm/domain/adr/AdrOption.java similarity index 88% rename from calm-hub/src/main/java/org/finos/calm/domain/AdrOption.java rename to calm-hub/src/main/java/org/finos/calm/domain/adr/AdrOption.java index c307f6ebc..ea76d1f31 100644 --- a/calm-hub/src/main/java/org/finos/calm/domain/AdrOption.java +++ b/calm-hub/src/main/java/org/finos/calm/domain/adr/AdrOption.java @@ -1,4 +1,4 @@ -package org.finos.calm.domain; +package org.finos.calm.domain.adr; import io.soabase.recordbuilder.core.RecordBuilder; diff --git a/calm-hub/src/main/java/org/finos/calm/domain/adr/AdrStatus.java b/calm-hub/src/main/java/org/finos/calm/domain/adr/AdrStatus.java new file mode 100644 index 000000000..78c698e54 --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/domain/adr/AdrStatus.java @@ -0,0 +1,10 @@ +package org.finos.calm.domain.adr; + +public enum AdrStatus { + draft, + proposed, + accepted, + superseded, + rejected, + deprecated; +} diff --git a/calm-hub/src/main/java/org/finos/calm/domain/NewAdr.java b/calm-hub/src/main/java/org/finos/calm/domain/adr/NewAdr.java similarity index 84% rename from calm-hub/src/main/java/org/finos/calm/domain/NewAdr.java rename to calm-hub/src/main/java/org/finos/calm/domain/adr/NewAdr.java index 5ade2afbb..237467467 100644 --- a/calm-hub/src/main/java/org/finos/calm/domain/NewAdr.java +++ b/calm-hub/src/main/java/org/finos/calm/domain/adr/NewAdr.java @@ -1,8 +1,7 @@ -package org.finos.calm.domain; +package org.finos.calm.domain.adr; import io.soabase.recordbuilder.core.RecordBuilder; -import java.time.LocalDateTime; import java.util.List; @RecordBuilder.Options(enableWither = false) diff --git a/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrParseException.java b/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrParseException.java new file mode 100644 index 000000000..f727c6121 --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrParseException.java @@ -0,0 +1,4 @@ +package org.finos.calm.domain.exception; + +public class AdrParseException extends Exception { +} diff --git a/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrPersistenceError.java b/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrPersistenceError.java deleted file mode 100644 index 31d055653..000000000 --- a/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrPersistenceError.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.finos.calm.domain.exception; - -public class AdrPersistenceError extends Throwable { -} diff --git a/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrPersistenceException.java b/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrPersistenceException.java new file mode 100644 index 000000000..078f3f450 --- /dev/null +++ b/calm-hub/src/main/java/org/finos/calm/domain/exception/AdrPersistenceException.java @@ -0,0 +1,4 @@ +package org.finos.calm.domain.exception; + +public class AdrPersistenceException extends Exception { +} diff --git a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java index 291fe513a..05b4a877f 100644 --- a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java +++ b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java @@ -15,14 +15,15 @@ import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; -import org.finos.calm.domain.Adr; -import org.finos.calm.domain.AdrBuilder; -import org.finos.calm.domain.AdrContent; -import org.finos.calm.domain.AdrStatus; -import org.finos.calm.domain.NewAdr; +import org.finos.calm.domain.adr.Adr; +import org.finos.calm.domain.adr.AdrBuilder; +import org.finos.calm.domain.adr.AdrContent; +import org.finos.calm.domain.adr.AdrStatus; +import org.finos.calm.domain.adr.NewAdr; import org.finos.calm.domain.ValueWrapper; import org.finos.calm.domain.exception.AdrNotFoundException; -import org.finos.calm.domain.exception.AdrPersistenceError; +import org.finos.calm.domain.exception.AdrParseException; +import org.finos.calm.domain.exception.AdrPersistenceException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; @@ -32,6 +33,8 @@ import java.net.URI; import java.net.URISyntaxException; import java.time.LocalDateTime; +import java.util.Map; +import java.util.function.Function; /** * Resource for managing ADRs in a given namespace @@ -48,6 +51,7 @@ public AdrResource(AdrStore store) { /** * Retrieve a list of ADRs in a given namespace + * * @param namespace the namespace to retrieve ADRs for * @return a list of ADRs in the given namespace */ @@ -61,18 +65,17 @@ public AdrResource(AdrStore store) { public Response getAdrsForNamespace(@PathParam("namespace") String namespace) { try { return Response.ok(new ValueWrapper<>(store.getAdrsForNamespace(namespace))).build(); - } catch (NamespaceNotFoundException e) { - logger.error("Invalid namespace [{}] when retrieving ADRs", namespace, e); - return invalidNamespaceResponse(namespace); + } catch(Exception e) { + return handleException(e, namespace); } } /** * Create a new ADR in the DRAFT state + * * @param namespace the namespace to create the ADR in - * @param newAdr the new ADR to be created + * @param newAdr the new ADR to be created * @return created response with Location header - * @throws URISyntaxException cannot produce location URL */ @POST @Path("{namespace}/adrs") @@ -82,9 +85,9 @@ public Response getAdrsForNamespace(@PathParam("namespace") String namespace) { summary = "Create ADR for namespace", description = "Creates an ADR for a given namespace with an allocated ID and revision 1" ) - public Response createAdrForNamespace(@PathParam("namespace") String namespace, NewAdr newAdr) throws URISyntaxException { + public Response createAdrForNamespace(@PathParam("namespace") String namespace, NewAdr newAdr) { AdrContent adrContent = AdrContent.builderFromNewAdr(newAdr) - .status(AdrStatus.DRAFT) + .status(AdrStatus.draft) .creationDateTime(LocalDateTime.now()) .updateDateTime(LocalDateTime.now()) .build(); @@ -97,22 +100,18 @@ public Response createAdrForNamespace(@PathParam("namespace") String namespace, try { return adrWithLocationResponse(store.createAdrForNamespace(adr)); - } catch (NamespaceNotFoundException e) { - logger.error("Invalid namespace [{}] when creating ADR", namespace, e); - return invalidNamespaceResponse(namespace); - } catch (JsonParseException | JsonProcessingException e) { - logger.error("Cannot parse ADR for namespace [{}]. ADR: [{}]", namespace, newAdr, e); - return invalidAdrJsonResponse(namespace); + } catch(Exception e) { + return handleException(e, namespace); } } /** * Update an existing ADRs contents - * @param namespace the namespace the ADR is in - * @param adrId the ID of the ADR + * + * @param namespace the namespace the ADR is in + * @param adrId the ID of the ADR * @param newAdrRevision the new ADR content * @return created with a Location header - * @throws URISyntaxException cannot produce Location header */ @POST @Path("{namespace}/adrs/{adrId}") @@ -122,7 +121,7 @@ public Response createAdrForNamespace(@PathParam("namespace") String namespace, summary = "Update ADR for namespace", description = "Updates an ADR for a given namespace. Creates a new revision." ) - public Response updateAdrForNamespace(@PathParam("namespace") String namespace, @PathParam("adrId") int adrId, NewAdr newAdrRevision) throws URISyntaxException { + public Response updateAdrForNamespace(@PathParam("namespace") String namespace, @PathParam("adrId") int adrId, NewAdr newAdrRevision) { AdrContent adrContent = AdrContent.builderFromNewAdr(newAdrRevision) .updateDateTime(LocalDateTime.now()) .build(); @@ -135,28 +134,16 @@ public Response updateAdrForNamespace(@PathParam("namespace") String namespace, try { return adrWithLocationResponse(store.updateAdrForNamespace(adr)); - } catch (NamespaceNotFoundException e) { - logger.error("Invalid namespace [{}] when creating ADR", namespace, e); - return invalidNamespaceResponse(namespace); - } catch (JsonParseException | JsonProcessingException e) { - logger.error("Cannot parse new ADR Revision for namespace [{}]. New ADR Revision: [{}]", namespace, newAdrRevision, e); - return invalidAdrJsonResponse(namespace); - } catch(AdrNotFoundException e) { - logger.error("Invalid ADR [{}] when creating new revision of ADR", adrId, e); - return invalidAdrResponse(adrId); - } catch(AdrRevisionNotFoundException e) { - logger.error("No existing revision of ADR [{}] found", adrId, e); - return invalidLatestRevisionResponse(adrId); - } catch(AdrPersistenceError e) { - logger.error("Error saving updated ADR [{}]", adr); - return serverErrorSavingAdr(adrId); + } catch(Exception e) { + return handleException(e, namespace); } } /** * Gets the latest revision of an ADR + * * @param namespace the namespace the ADR belongs to - * @param adrId the ID of the requested ADR + * @param adrId the ID of the requested ADR * @return the requested ADR document */ @GET @@ -180,24 +167,16 @@ public Response getAdr(@PathParam("namespace") String namespace, @PathParam("adr try { return Response.ok(store.getAdr(adr)).build(); - } catch (NamespaceNotFoundException e) { - logger.error("Invalid namespace [{}] when getting an ADR", namespace, e); - return invalidNamespaceResponse(namespace); - } catch (AdrNotFoundException e) { - logger.error("Invalid ADR [{}] when getting an ADR", adrId, e); - return invalidAdrResponse(adrId); - } catch (AdrRevisionNotFoundException e) { - logger.error("Could not get latest revision of ADR [{}]", adrId, e); - return invalidLatestRevisionResponse(adrId); - } catch(JsonProcessingException e) { - return invalidGetAdrResponse(adrId); + } catch(Exception e) { + return handleException(e, namespace, adrId); } } /** * Gets the list of revisions of an ADR + * * @param namespace the namespace the ADR belongs to - * @param adrId the ID of the requested ADR + * @param adrId the ID of the requested ADR * @return a list of revision numbers */ @GET @@ -215,23 +194,17 @@ public Response getAdrRevisions(@PathParam("namespace") String namespace, @PathP try { return Response.ok(new ValueWrapper<>(store.getAdrRevisions(adr))).build(); - } catch (NamespaceNotFoundException e) { - logger.error("Invalid namespace [{}] when getting revisions of ADR", namespace, e); - return invalidNamespaceResponse(namespace); - } catch (AdrNotFoundException e) { - logger.error("Invalid ADR [{}] when getting versions of ADR", adrId, e); - return invalidAdrResponse(adrId); - } catch(AdrRevisionNotFoundException e) { - logger.error("Could not find any revisions of ADR: [{}]", adrId, e); - return invalidAdrRevisions(adrId); + } catch(NamespaceNotFoundException | AdrNotFoundException | AdrRevisionNotFoundException e) { + return handleException(e, namespace, adrId); } } /** * Gets a specific revision of an ADR + * * @param namespace the namespace the ADR belongs to - * @param adrId the ID of the requested ADR - * @param revision the revision of the ADR being requested + * @param adrId the ID of the requested ADR + * @param revision the revision of the ADR being requested * @return the requested revision of the requested ADR */ @GET @@ -256,36 +229,28 @@ public Response getAdrRevision(@PathParam("namespace") String namespace, @PathPa try { return Response.ok(store.getAdrRevision(adr)).build(); - } catch (NamespaceNotFoundException e) { - logger.error("Invalid namespace [{}] when getting an ADR", namespace, e); - return invalidNamespaceResponse(namespace); - } catch (AdrNotFoundException e) { - logger.error("Invalid ADR [{}] when getting an ADR revision", adrId, e); - return invalidAdrResponse(adrId); - } catch (AdrRevisionNotFoundException e) { - logger.error("Invalid revision [{}] when getting an ADR", revision, e); - return invalidRevisionResponse(revision); - } catch(JsonProcessingException e) { - return invalidGetAdrResponse(adrId); + } catch(Exception e) { + return handleException(e, namespace, adrId, revision); } } /** * Update the status of an existing ADR + * * @param namespace the namespace the ADR is in - * @param adrId the ID of the ADR + * @param adrId the ID of the ADR * @param adrStatus the new status of the ADR * @return created with a Location header * @throws URISyntaxException cannot produce Location header */ @POST - @Path("{namespace}/adrs/{adrId}/status/{adrStatus}") + @Path("{namespace}/adrs/{adrId}/status/{status}") @Produces(MediaType.APPLICATION_JSON) @Operation( summary = "Update the status of ADR for namespace", description = "Updates the status of an ADR for a given namespace. Creates a new revision." ) - public Response updateAdrStatusForNamespace(@PathParam("namespace") String namespace, @PathParam("adrId") int adrId, @PathParam("adrStatus") AdrStatus adrStatus) throws URISyntaxException { + public Response updateAdrStatusForNamespace(@PathParam("namespace") String namespace, @PathParam("adrId") int adrId, @PathParam("status") AdrStatus adrStatus) throws URISyntaxException { Adr adr = AdrBuilder.builder() .namespace(namespace) @@ -294,21 +259,8 @@ public Response updateAdrStatusForNamespace(@PathParam("namespace") String names try { return adrWithLocationResponse(store.updateAdrStatus(adr, adrStatus)); - } catch (NamespaceNotFoundException e) { - logger.error("Invalid namespace [{}] when updating status of ADR", namespace, e); - return invalidNamespaceResponse(namespace); - } catch (JsonParseException | JsonProcessingException e) { - logger.error("Cannot parse existing ADR Revision for namespace [{}] while updating status of ADR [{}].", namespace, adrId, e); - return invalidAdrJsonResponse(namespace); - } catch(AdrNotFoundException e) { - logger.error("Invalid ADR [{}] when updating the status of ADR", adrId, e); - return invalidAdrResponse(adrId); - } catch(AdrRevisionNotFoundException e) { - logger.error("No existing revision of ADR [{}] found", adrId, e); - return invalidLatestRevisionResponse(adrId); - } catch(AdrPersistenceError e) { - logger.error("Error saving updated ADR [{}]", adr); - return serverErrorSavingAdr(adrId); + } catch(Exception e) { + return handleException(e, namespace, adrId); } } @@ -316,35 +268,56 @@ private Response adrWithLocationResponse(Adr adr) throws URISyntaxException { return Response.created(new URI("/calm/namespaces/" + adr.namespace() + "/adrs/" + adr.id() + "/revisions/" + adr.revision())).build(); } - private Response invalidNamespaceResponse(String namespace) { - return Response.status(Response.Status.NOT_FOUND).entity("Invalid namespace provided: " + namespace).build(); + private Response handleException(Exception e, String namespace) { + return handleException(e, namespace, 0, 0); } - private Response invalidAdrJsonResponse(String adrJson) { - return Response.status(Response.Status.BAD_REQUEST).entity("The ADR JSON could not be parsed: " + adrJson).build(); + private Response handleException(Exception e, String namespace, int adrId) { + return handleException(e, namespace, adrId, 0); } - private Response invalidGetAdrResponse(int adrId) { - return Response.status(Response.Status.NOT_FOUND).entity("Could not process ADR when getting it: " + adrId).build(); + private Response handleException(Exception e, String namespace, int adrId, int revision) { + Map, Function> handlers = Map.of( + NamespaceNotFoundException.class, ex -> handleNamespaceNotFoundException(namespace, ex), + AdrNotFoundException.class, ex -> handleAdrNotFoundException(adrId, ex), + AdrRevisionNotFoundException.class, ex -> handleAdrRevisionNotFoundException(adrId, revision, ex), + AdrParseException.class, this::handleAdrParseException, + AdrPersistenceException.class, ex -> handleAdrPersistenceException(adrId, ex) + ); + + return handlers.getOrDefault(e.getClass(), ex -> { + logger.error("Unexpected exception occurred", ex); + return Response.serverError().build(); + }).apply(e); } - private Response invalidAdrResponse(int adrId) { - return Response.status(Response.Status.NOT_FOUND).entity("Invalid adrId provided: " + adrId).build(); + private Response handleNamespaceNotFoundException(String namespace, Exception ex) { + logger.error("Could not find namespace [{}]", namespace, ex); + return Response.status(Response.Status.NOT_FOUND).entity("Could not find namespace: " + namespace).build(); } - private Response invalidRevisionResponse(int revision) { - return Response.status(Response.Status.NOT_FOUND).entity("Invalid revision provided: " + revision).build(); + private Response handleAdrNotFoundException(int adrId, Exception ex) { + logger.error("Could not find ADR [{}]", adrId, ex); + return Response.status(Response.Status.NOT_FOUND).entity("Could not find ADR: " + adrId).build(); } - private Response invalidLatestRevisionResponse(int adrId) { - return Response.status(Response.Status.NOT_FOUND).entity("Latest revision not found for ADR: [{}]: " + adrId).build(); + private Response handleAdrRevisionNotFoundException(int adrId, int revision, Exception ex) { + if(revision == 0) { + logger.error("Could not find latest revision of ADR [{}]", adrId, ex); + return Response.status(Response.Status.NOT_FOUND).entity("Latest revision not found for ADR: [{}]: " + adrId).build(); + } else { + logger.error("Could not find revision [{}] of ADR [{}]", revision, adrId, ex); + return Response.status(Response.Status.NOT_FOUND).entity("Revision " + revision + " not found for ADR: [{}]: " + adrId).build(); + } } - private Response invalidAdrRevisions(int adrId) { - return Response.status(Response.Status.NOT_FOUND).entity("Revisions not found for ADR: [{}]: " + adrId).build(); + private Response handleAdrParseException(Exception ex) { + logger.error("Could not parse ADR JSON", ex); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Could not parse ADR JSON").build(); } - private Response serverErrorSavingAdr(int adrId) { - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Could not save update of ADR: [{}]:" + adrId).build(); + private Response handleAdrPersistenceException(int adrId, Exception ex) { + logger.error("Could not persist update of ADR [{}]", adrId, ex); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Could not persist update of ADR: [{}]:" + adrId).build(); } -} +} \ No newline at end of file diff --git a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java index 700516a85..4bba68a1e 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java @@ -1,10 +1,10 @@ package org.finos.calm.store; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.finos.calm.domain.Adr; -import org.finos.calm.domain.AdrStatus; +import org.finos.calm.domain.adr.Adr; +import org.finos.calm.domain.adr.AdrStatus; import org.finos.calm.domain.exception.AdrNotFoundException; -import org.finos.calm.domain.exception.AdrPersistenceError; +import org.finos.calm.domain.exception.AdrParseException; +import org.finos.calm.domain.exception.AdrPersistenceException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; @@ -13,10 +13,10 @@ public interface AdrStore { List getAdrsForNamespace(String namespace) throws NamespaceNotFoundException; - Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException, JsonProcessingException; - Adr getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException; + Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrParseException; + Adr getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, AdrParseException; List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException; - Adr getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException; - Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError; - Adr updateAdrStatus(Adr adr, AdrStatus adrStatus) throws AdrNotFoundException, NamespaceNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError; + Adr getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, AdrParseException; + Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, AdrPersistenceException, AdrParseException; + Adr updateAdrStatus(Adr adr, AdrStatus adrStatus) throws AdrNotFoundException, NamespaceNotFoundException, AdrRevisionNotFoundException, AdrPersistenceException, AdrParseException; } diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java index e0ec07b3d..c7b317dac 100644 --- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java +++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java @@ -14,13 +14,14 @@ import jakarta.enterprise.context.ApplicationScoped; import org.bson.Document; import org.bson.conversions.Bson; -import org.finos.calm.domain.Adr; -import org.finos.calm.domain.AdrBuilder; -import org.finos.calm.domain.AdrContent; -import org.finos.calm.domain.AdrContentBuilder; -import org.finos.calm.domain.AdrStatus; +import org.finos.calm.domain.adr.Adr; +import org.finos.calm.domain.adr.AdrBuilder; +import org.finos.calm.domain.adr.AdrContent; +import org.finos.calm.domain.adr.AdrContentBuilder; +import org.finos.calm.domain.adr.AdrStatus; import org.finos.calm.domain.exception.AdrNotFoundException; -import org.finos.calm.domain.exception.AdrPersistenceError; +import org.finos.calm.domain.exception.AdrParseException; +import org.finos.calm.domain.exception.AdrPersistenceException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; @@ -68,14 +69,22 @@ public List getAdrsForNamespace(String namespace) throws NamespaceNotFo } @Override - public Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException, JsonProcessingException { + public Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrParseException { if(!namespaceStore.namespaceExists(adr.namespace())) { throw new NamespaceNotFoundException(); } + String adrContentStr; + try { + adrContentStr = objectMapper.writeValueAsString(adr.adrContent()); + } catch(JsonProcessingException e) { + log.error("Could not write ADR Content to String", e); + throw new AdrParseException(); + } + int id = counterStore.getNextAdrSequenceValue(); Document adrDocument = new Document("adrId", id).append("revisions", - new Document(String.valueOf(adr.revision()), Document.parse(objectMapper.writeValueAsString(adr.adrContent())))); + new Document(String.valueOf(adr.revision()), Document.parse(adrContentStr))); adrCollection.updateOne( Filters.eq("namespace", adr.namespace()), @@ -86,7 +95,7 @@ public Adr createAdrForNamespace(Adr adr) throws NamespaceNotFoundException, Jso } @Override - public Adr getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + public Adr getAdr(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, AdrParseException { Document adrDoc = retrieveAdrDoc(adr); return retrieveLatestRevision(adr, adrDoc); } @@ -102,7 +111,7 @@ public List getAdrRevisions(Adr adr) throws NamespaceNotFoundException, } @Override - public Adr getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + public Adr getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, AdrParseException { Document adrDoc = retrieveAdrDoc(adr); Document revisionsDoc = retrieveRevisionsDoc(adrDoc, adr); @@ -112,13 +121,18 @@ public Adr getAdrRevision(Adr adr) throws NamespaceNotFoundException, AdrNotFoun if(revisionDoc == null) { throw new AdrRevisionNotFoundException(); } - return AdrBuilder.builder(adr) - .adrContent(objectMapper.readValue(revisionDoc.toJson(), AdrContent.class)) - .build(); + try { + return AdrBuilder.builder(adr) + .adrContent(objectMapper.readValue(revisionDoc.toJson(), AdrContent.class)) + .build(); + } catch(JsonProcessingException e) { + log.error("Could not parse stored ADR to ADR Content.", e); + throw new AdrParseException(); + } } @Override - public Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { + public Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, AdrPersistenceException, AdrParseException { Document adrDoc = retrieveAdrDoc(adr); Adr latestRevision = retrieveLatestRevision(adr, adrDoc); @@ -136,7 +150,7 @@ public Adr updateAdrForNamespace(Adr adr) throws NamespaceNotFoundException, Adr } @Override - public Adr updateAdrStatus(Adr adr, AdrStatus adrStatus) throws AdrNotFoundException, NamespaceNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { + public Adr updateAdrStatus(Adr adr, AdrStatus adrStatus) throws AdrNotFoundException, NamespaceNotFoundException, AdrRevisionNotFoundException, AdrPersistenceException, AdrParseException { Document adrDoc = retrieveAdrDoc(adr); Adr latestRevision = retrieveLatestRevision(adr, adrDoc); @@ -185,7 +199,7 @@ private Document retrieveRevisionsDoc(Document adrDoc, Adr adr) throws AdrRevisi return revisionsDoc; } - private Adr retrieveLatestRevision(Adr adr, Document adrDoc) throws AdrRevisionNotFoundException, JsonProcessingException { + private Adr retrieveLatestRevision(Adr adr, Document adrDoc) throws AdrRevisionNotFoundException, AdrParseException { Document revisionsDoc = retrieveRevisionsDoc(adrDoc, adr); Set revisionKeys = revisionsDoc.keySet(); @@ -198,27 +212,37 @@ private Adr retrieveLatestRevision(Adr adr, Document adrDoc) throws AdrRevisionN // Return the ADR JSON blob for the specified revision Document revisionDoc = (Document) revisionsDoc.get(String.valueOf(latestRevision)); log.info("RevisionDoc: [{}], Revision: [{}]", revisionDoc, latestRevision); - return AdrBuilder.builder() - .namespace(adr.namespace()) - .id(adr.id()) - .revision(latestRevision) - .adrContent(objectMapper.readValue(revisionDoc.toJson(), AdrContent.class)) - .build(); + try { + return AdrBuilder.builder() + .namespace(adr.namespace()) + .id(adr.id()) + .revision(latestRevision) + .adrContent(objectMapper.readValue(revisionDoc.toJson(), AdrContent.class)) + .build(); + } catch(JsonProcessingException e) { + log.error("Could not parse stored ADR to ADR Content.", e); + throw new AdrParseException(); + } } - private void writeAdrToMongo(Adr adr) throws JsonProcessingException, AdrPersistenceError { - - Document adrDocument = Document.parse(objectMapper.writeValueAsString(adr.adrContent())); - Document filter = new Document("namespace", adr.namespace()) - .append("adrs.adrId", adr.id()); - Document update = new Document("$set", - new Document("adrs.$.revisions." + adr.revision(), adrDocument)); + private void writeAdrToMongo(Adr adr) throws AdrPersistenceException, AdrParseException { try { + Document adrDocument = Document.parse(objectMapper.writeValueAsString(adr.adrContent())); + Document filter = new Document("namespace", adr.namespace()) + .append("adrs.adrId", adr.id()); + Document update = new Document("$set", + new Document("adrs.$.revisions." + adr.revision(), adrDocument)); + adrCollection.updateOne(filter, update, new UpdateOptions().upsert(true)); + } catch(MongoWriteException ex) { log.error("Failed to write ADR to mongo [{}]", adr, ex); - throw new AdrPersistenceError(); + throw new AdrPersistenceException(); + } catch(JsonProcessingException e) { + log.error("Could not write ADR Content to String", e); + throw new AdrParseException(); } + } } diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java index 0c1de5352..511a8d7ce 100644 --- a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java +++ b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java @@ -1,16 +1,15 @@ package org.finos.calm.resources; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.exc.InvalidFormatException; import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; -import org.bson.json.JsonParseException; -import org.finos.calm.domain.Adr; -import org.finos.calm.domain.AdrBuilder; -import org.finos.calm.domain.AdrContentBuilder; -import org.finos.calm.domain.AdrStatus; +import org.finos.calm.domain.adr.Adr; +import org.finos.calm.domain.adr.AdrBuilder; +import org.finos.calm.domain.adr.AdrContentBuilder; +import org.finos.calm.domain.adr.AdrStatus; import org.finos.calm.domain.exception.AdrNotFoundException; -import org.finos.calm.domain.exception.AdrPersistenceError; +import org.finos.calm.domain.exception.AdrParseException; +import org.finos.calm.domain.exception.AdrPersistenceException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.finos.calm.store.AdrStore; @@ -75,15 +74,14 @@ void return_list_of_adr_ids_when_valid_namespace_provided_on_get_adrs() throws N static Stream provideParametersForCreateAdrTests() { return Stream.of( Arguments.of("invalid", new NamespaceNotFoundException(), 404, nullValue()), - Arguments.of("invalid", new JsonParseException(), 400, nullValue()), + Arguments.of("invalid", new AdrParseException(), 500, nullValue()), Arguments.of("valid", null, 201, containsString("/calm/namespaces/valid/adrs/12/revisions/1")) ); } - @ParameterizedTest @MethodSource("provideParametersForCreateAdrTests") - void respond_correctly_when_creating_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode, Matcher locationHeader) throws NamespaceNotFoundException, JsonProcessingException { + void respond_correctly_when_creating_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode, Matcher locationHeader) throws NamespaceNotFoundException, AdrParseException { if (exceptionToThrow != null) { when(mockAdrStore.createAdrForNamespace(any(Adr.class))).thenThrow(exceptionToThrow); } else { @@ -91,7 +89,7 @@ void respond_correctly_when_creating_adr(String namespace, Throwable exceptionTo .adrContent( AdrContentBuilder.builder() .title("My ADR") - .status(AdrStatus.DRAFT) + .status(AdrStatus.draft) .creationDateTime(LocalDateTime.now()) .updateDateTime(LocalDateTime.now()) .build() @@ -118,7 +116,7 @@ void respond_correctly_when_creating_adr(String namespace, Throwable exceptionTo .adrContent( AdrContentBuilder.builder() .title("My ADR") - .status(AdrStatus.DRAFT) + .status(AdrStatus.draft) .build()) .revision(1) .namespace(namespace) @@ -134,15 +132,15 @@ static Stream provideParametersForUpdateAdrTests() { Arguments.of("invalid", new NamespaceNotFoundException(), 404), Arguments.of("valid", new AdrNotFoundException(), 404), Arguments.of("valid", new AdrRevisionNotFoundException(), 404), - Arguments.of("valid", new JsonParseException(), 400), - Arguments.of("valid", new AdrPersistenceError(), 500), + Arguments.of("valid", new AdrParseException(), 500), + Arguments.of("valid", new AdrPersistenceException(), 500), Arguments.of("valid", null, 201) ); } @ParameterizedTest @MethodSource("provideParametersForUpdateAdrTests") - void respond_correctly_to_update_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { + void respond_correctly_to_update_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, AdrPersistenceException, AdrParseException { String adrJson = "{ \"title\": \"My ADR\" }"; if (exceptionToThrow != null) { when(mockAdrStore.updateAdrForNamespace(any(Adr.class))).thenThrow(exceptionToThrow); @@ -151,7 +149,7 @@ void respond_correctly_to_update_adr(String namespace, Throwable exceptionToThro .adrContent( AdrContentBuilder.builder() .title("My ADR") - .status(AdrStatus.DRAFT) + .status(AdrStatus.draft) .creationDateTime(LocalDateTime.now()) .updateDateTime(LocalDateTime.now()) .build() @@ -236,14 +234,14 @@ static Stream provideParametersForGetAdrRevisionTests() { Arguments.of("invalid", new NamespaceNotFoundException(), 404), Arguments.of("valid", new AdrNotFoundException(), 404), Arguments.of("valid", new AdrRevisionNotFoundException(), 404), - Arguments.of("valid", new InvalidFormatException("error", "value", String.class), 404), + Arguments.of("valid", new AdrParseException(), 500), Arguments.of("valid", null, 200) ); } @ParameterizedTest @MethodSource("provideParametersForGetAdrRevisionTests") - void respond_correctly_to_get_adr_revision(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + void respond_correctly_to_get_adr_revision(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, AdrParseException { Adr adr = AdrBuilder.builder() .namespace(namespace) .id(12) @@ -281,7 +279,7 @@ void respond_correctly_to_get_adr_revision(String namespace, Throwable exception @ParameterizedTest @MethodSource("provideParametersForGetAdrRevisionTests") - void respond_correctly_to_get_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + void respond_correctly_to_get_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrParseException { Adr adr = AdrBuilder.builder() .namespace(namespace) .id(12) @@ -323,15 +321,15 @@ static Stream provideParametersForUpdateAdrStatusTests() { Arguments.of("invalid", new NamespaceNotFoundException(), 404), Arguments.of("valid", new AdrNotFoundException(), 404), Arguments.of("valid", new AdrRevisionNotFoundException(), 404), - Arguments.of("valid", new JsonParseException(), 400), - Arguments.of("valid", new AdrPersistenceError(), 500), + Arguments.of("valid", new AdrParseException(), 500), + Arguments.of("valid", new AdrPersistenceException(), 500), Arguments.of("valid", null, 201) ); } @ParameterizedTest @MethodSource("provideParametersForUpdateAdrStatusTests") - void respond_correctly_on_update_adr_status(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { + void respond_correctly_on_update_adr_status(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, AdrPersistenceException, AdrParseException { String adrJson = "{ \"title\": \"My ADR\" }"; if (exceptionToThrow != null) { when(mockAdrStore.updateAdrStatus(any(Adr.class), any(AdrStatus.class))).thenThrow(exceptionToThrow); @@ -340,7 +338,7 @@ void respond_correctly_on_update_adr_status(String namespace, Throwable exceptio .adrContent( AdrContentBuilder.builder() .title("My ADR") - .status(AdrStatus.PROPOSED) + .status(AdrStatus.proposed) .creationDateTime(LocalDateTime.now()) .updateDateTime(LocalDateTime.now()) .build() @@ -356,7 +354,7 @@ void respond_correctly_on_update_adr_status(String namespace, Throwable exceptio .header("Content-Type", "application/json") .body(adrJson) .when() - .post("/calm/namespaces/" + namespace +"/adrs/1/status/PROPOSED") + .post("/calm/namespaces/" + namespace +"/adrs/1/status/proposed") .then() .statusCode(expectedStatusCode); @@ -365,10 +363,10 @@ void respond_correctly_on_update_adr_status(String namespace, Throwable exceptio .namespace(namespace) .build(); - verify(mockAdrStore, times(1)).updateAdrStatus(expectedAdr, AdrStatus.PROPOSED); + verify(mockAdrStore, times(1)).updateAdrStatus(expectedAdr, AdrStatus.proposed); } - private void verifyExpectedGetAdrRevision(String namespace) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + private void verifyExpectedGetAdrRevision(String namespace) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, AdrParseException { Adr expectedAdrToRetrieve = AdrBuilder.builder() .namespace(namespace) .id(12) @@ -378,7 +376,7 @@ private void verifyExpectedGetAdrRevision(String namespace) throws NamespaceNotF verify(mockAdrStore, times(1)).getAdrRevision(expectedAdrToRetrieve); } - private void verifyExpectedGetAdr(String namespace) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + private void verifyExpectedGetAdr(String namespace) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrParseException { Adr expectedAdrToRetrieve = AdrBuilder.builder() .namespace(namespace) .id(12) diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java index 8885ef83e..c795e6aad 100644 --- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java +++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java @@ -19,12 +19,13 @@ import org.bson.BsonDocument; import org.bson.Document; import org.bson.conversions.Bson; -import org.finos.calm.domain.Adr; -import org.finos.calm.domain.AdrBuilder; -import org.finos.calm.domain.AdrContentBuilder; -import org.finos.calm.domain.AdrStatus; +import org.finos.calm.domain.adr.Adr; +import org.finos.calm.domain.adr.AdrBuilder; +import org.finos.calm.domain.adr.AdrContentBuilder; +import org.finos.calm.domain.adr.AdrStatus; import org.finos.calm.domain.exception.AdrNotFoundException; -import org.finos.calm.domain.exception.AdrPersistenceError; +import org.finos.calm.domain.exception.AdrParseException; +import org.finos.calm.domain.exception.AdrPersistenceException; import org.finos.calm.domain.exception.AdrRevisionNotFoundException; import org.finos.calm.domain.exception.NamespaceNotFoundException; import org.junit.jupiter.api.BeforeEach; @@ -74,7 +75,7 @@ public class TestMongoAdrStoreShould { .revision(2) .adrContent(AdrContentBuilder.builder() .title("My ADR") - .status(AdrStatus.SUPERSEDED) + .status(AdrStatus.superseded) .creationDateTime(LocalDateTime.now()) .updateDateTime(LocalDateTime.now()) .build()) @@ -164,7 +165,7 @@ void return_a_namespace_exception_when_namespace_does_not_exist_when_creating_an } @Test - void return_created_adr_when_parameters_are_valid() throws NamespaceNotFoundException, JsonProcessingException { + void return_created_adr_when_parameters_are_valid() throws NamespaceNotFoundException, JsonProcessingException, AdrParseException { String validNamespace = NAMESPACE; int sequenceNumber = 42; when(namespaceStore.namespaceExists(anyString())).thenReturn(true); @@ -230,7 +231,7 @@ void get_adr_revisions_for_invalid_adr_throws_exception() { } @Test - void get_adr_revisions_for_valid_adr_returns_list_of_revisions() throws NamespaceNotFoundException, AdrNotFoundException, JsonProcessingException, AdrRevisionNotFoundException { + void get_adr_revisions_for_valid_adr_returns_list_of_revisions() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder().namespace(NAMESPACE).id(42).build(); @@ -263,7 +264,7 @@ void throw_an_exception_for_an_invalid_adr_when_retrieving_adr_revision() { } @Test - void return_an_adr_revision() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + void return_an_adr_revision() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, AdrParseException, JsonProcessingException { mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder().namespace(NAMESPACE) @@ -362,7 +363,7 @@ void get_adr_for_invalid_namespace_throws_exception() { } @Test - void return_the_latest_adr_revision() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException { + void return_the_latest_adr_revision() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrParseException { mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder().namespace(NAMESPACE) @@ -427,12 +428,12 @@ void throw_an_exception_when_updating_an_adr_but_mongo_cannot_write_update() thr .adrContent(AdrContentBuilder.builder().build()) .build(); - assertThrows(AdrPersistenceError.class, + assertThrows(AdrPersistenceException.class, () -> mongoAdrStore.updateAdrForNamespace(adr)); } @Test - void return_successfully_when_correctly_updating_an_adr() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { + void return_successfully_when_correctly_updating_an_adr() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceException, AdrParseException { mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder() .namespace(NAMESPACE) @@ -456,7 +457,7 @@ void throw_an_exception_when_updating_status_with_a_namespace_that_doesnt_exists .build(); assertThrows(NamespaceNotFoundException.class, - () -> mongoAdrStore.updateAdrStatus(adr, AdrStatus.ACCEPTED)); + () -> mongoAdrStore.updateAdrStatus(adr, AdrStatus.accepted)); verify(namespaceStore, times(1)).namespaceExists(adr.namespace()); } @@ -471,7 +472,7 @@ void throw_an_exception_when_updating_the_status_of_an_adr_that_doesnt_exist() t .build(); assertThrows(AdrNotFoundException.class, - () -> mongoAdrStore.updateAdrStatus(adr, AdrStatus.ACCEPTED)); + () -> mongoAdrStore.updateAdrStatus(adr, AdrStatus.accepted)); } @Test @@ -484,7 +485,7 @@ void throw_an_exception_when_updating_the_status_of_an_adr_but_no_revisions_exis .build(); assertThrows(AdrRevisionNotFoundException.class, - () -> mongoAdrStore.updateAdrStatus(adr, AdrStatus.ACCEPTED)); + () -> mongoAdrStore.updateAdrStatus(adr, AdrStatus.accepted)); } @Test @@ -498,19 +499,19 @@ void throw_an_exception_when_updating_the_status_of_an_adr_but_mongo_cannot_writ .id(42) .build(); - assertThrows(AdrPersistenceError.class, - () -> mongoAdrStore.updateAdrStatus(adr, AdrStatus.PROPOSED)); + assertThrows(AdrPersistenceException.class, + () -> mongoAdrStore.updateAdrStatus(adr, AdrStatus.proposed)); } @Test - void return_successfully_when_correctly_updating_the_status_of_an_adr() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceError { + void return_successfully_when_correctly_updating_the_status_of_an_adr() throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, JsonProcessingException, AdrPersistenceException, AdrParseException { mockSetupAdrDocumentWithRevisions(); Adr adr = AdrBuilder.builder() .namespace(NAMESPACE) .id(42) .build(); - mongoAdrStore.updateAdrStatus(adr, AdrStatus.ACCEPTED); + mongoAdrStore.updateAdrStatus(adr, AdrStatus.accepted); verify(adrCollection, times(1)).updateOne(any(Bson.class), any(Bson.class), any(UpdateOptions.class)); } From a189af33a3b5a16f78a5f5ba8ec9bf2c9741031a Mon Sep 17 00:00:00 2001 From: Graham Packer Date: Tue, 21 Jan 2025 09:13:23 +0000 Subject: [PATCH 12/12] Rename ADR types (#716) --- .../java/integration/MongoAdrIntegration.java | 54 +++---- .../java/org/finos/calm/domain/adr/Adr.java | 58 ++++++- .../org/finos/calm/domain/adr/AdrContent.java | 61 -------- .../org/finos/calm/domain/adr/AdrMeta.java | 13 ++ .../adr/{AdrDecision.java => Decision.java} | 2 +- .../domain/adr/{AdrLink.java => Link.java} | 2 +- .../adr/{NewAdr.java => NewAdrRequest.java} | 8 +- .../adr/{AdrOption.java => Option.java} | 2 +- .../adr/{AdrStatus.java => Status.java} | 2 +- .../org/finos/calm/resources/AdrResource.java | 60 ++++--- .../java/org/finos/calm/store/AdrStore.java | 16 +- .../finos/calm/store/mongo/MongoAdrStore.java | 104 ++++++------ .../calm/resources/TestAdrResourceShould.java | 98 ++++++------ .../store/mongo/TestMongoAdrStoreShould.java | 148 +++++++++--------- 14 files changed, 313 insertions(+), 315 deletions(-) delete mode 100644 calm-hub/src/main/java/org/finos/calm/domain/adr/AdrContent.java create mode 100644 calm-hub/src/main/java/org/finos/calm/domain/adr/AdrMeta.java rename calm-hub/src/main/java/org/finos/calm/domain/adr/{AdrDecision.java => Decision.java} (68%) rename calm-hub/src/main/java/org/finos/calm/domain/adr/{AdrLink.java => Link.java} (75%) rename calm-hub/src/main/java/org/finos/calm/domain/adr/{NewAdr.java => NewAdrRequest.java} (69%) rename calm-hub/src/main/java/org/finos/calm/domain/adr/{AdrOption.java => Option.java} (57%) rename calm-hub/src/main/java/org/finos/calm/domain/adr/{AdrStatus.java => Status.java} (83%) diff --git a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java index 12078ecdc..1c3f15388 100644 --- a/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java +++ b/calm-hub/src/integration-test/java/integration/MongoAdrIntegration.java @@ -10,16 +10,16 @@ import org.bson.Document; import org.eclipse.microprofile.config.ConfigProvider; import org.finos.calm.domain.adr.Adr; -import org.finos.calm.domain.adr.AdrBuilder; -import org.finos.calm.domain.adr.AdrContent; -import org.finos.calm.domain.adr.AdrDecision; -import org.finos.calm.domain.adr.AdrDecisionBuilder; -import org.finos.calm.domain.adr.AdrLinkBuilder; -import org.finos.calm.domain.adr.AdrOption; -import org.finos.calm.domain.adr.AdrOptionBuilder; -import org.finos.calm.domain.adr.AdrStatus; -import org.finos.calm.domain.adr.NewAdr; -import org.finos.calm.domain.adr.NewAdrBuilder; +import org.finos.calm.domain.adr.AdrMeta; +import org.finos.calm.domain.adr.AdrMetaBuilder; +import org.finos.calm.domain.adr.Decision; +import org.finos.calm.domain.adr.DecisionBuilder; +import org.finos.calm.domain.adr.LinkBuilder; +import org.finos.calm.domain.adr.NewAdrRequest; +import org.finos.calm.domain.adr.NewAdrRequestBuilder; +import org.finos.calm.domain.adr.Option; +import org.finos.calm.domain.adr.OptionBuilder; +import org.finos.calm.domain.adr.Status; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; @@ -50,27 +50,27 @@ public class MongoAdrIntegration { private final String TITLE = "My ADR"; private final String PROBLEM_STATEMENT = "My problem is..."; private final List DECISION_DRIVERS = List.of("a", "b", "c"); - private final AdrOption OPTION_A = AdrOptionBuilder.builder().name("Option 1").description("optionDescription") + private final Option OPTION_A = OptionBuilder.builder().name("Option 1").description("optionDescription") .positiveConsequences(List.of("a")).negativeConsequences(List.of("b")).build(); - private final AdrOption OPTION_B = AdrOptionBuilder.builder().name("Option 2").description("optionDescription") + private final Option OPTION_B = OptionBuilder.builder().name("Option 2").description("optionDescription") .positiveConsequences(List.of("c")).negativeConsequences(List.of("d")).build(); - private final List CONSIDERED_OPTIONS = List.of(OPTION_A, OPTION_B); + private final List