diff --git a/pom.xml b/pom.xml
index 77b1c58..f3dd73c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -92,6 +92,10 @@
io.quarkus
quarkus-flyway
+
+ io.quarkus
+ quarkus-vertx
+
io.quarkus
@@ -203,6 +207,12 @@
2.30.1
test
+
+ org.awaitility
+ awaitility
+ 4.1.0
+ test
+
diff --git a/src/main/java/io/tackle/applicationinventory/dto/BulkReviewDto.java b/src/main/java/io/tackle/applicationinventory/dto/BulkReviewDto.java
new file mode 100644
index 0000000..ab6f5eb
--- /dev/null
+++ b/src/main/java/io/tackle/applicationinventory/dto/BulkReviewDto.java
@@ -0,0 +1,51 @@
+package io.tackle.applicationinventory.dto;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+public class BulkReviewDto {
+
+ private Long id;
+
+ @NotNull
+ private Long sourceReview;
+
+ @NotEmpty
+ private List targetApplications;
+
+ private boolean completed;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getSourceReview() {
+ return sourceReview;
+ }
+
+ public void setSourceReview(Long sourceReview) {
+ this.sourceReview = sourceReview;
+ }
+
+ public List getTargetApplications() {
+ return targetApplications;
+ }
+
+ public void setTargetApplications(List targetApplications) {
+ this.targetApplications = targetApplications;
+ }
+
+ public boolean isCompleted() {
+ return completed;
+ }
+
+ public void setCompleted(boolean completed) {
+ this.completed = completed;
+ }
+
+}
diff --git a/src/main/java/io/tackle/applicationinventory/dto/utils/EntityToDTO.java b/src/main/java/io/tackle/applicationinventory/dto/utils/EntityToDTO.java
new file mode 100644
index 0000000..5e15c5f
--- /dev/null
+++ b/src/main/java/io/tackle/applicationinventory/dto/utils/EntityToDTO.java
@@ -0,0 +1,23 @@
+package io.tackle.applicationinventory.dto.utils;
+
+import io.tackle.applicationinventory.dto.BulkReviewDto;
+import io.tackle.applicationinventory.entities.BulkCopyReview;
+
+import java.util.stream.Collectors;
+
+public class EntityToDTO {
+
+ public static BulkReviewDto toDTO(BulkCopyReview entity) {
+ BulkReviewDto dto = new BulkReviewDto();
+
+ dto.setId(entity.id);
+ dto.setCompleted(entity.completed);
+ dto.setSourceReview(entity.sourceReview.id);
+ dto.setTargetApplications(entity.targetApplications.stream()
+ .map(f -> f.application.id)
+ .collect(Collectors.toList())
+ );
+
+ return dto;
+ }
+}
diff --git a/src/main/java/io/tackle/applicationinventory/entities/BulkCopyReview.java b/src/main/java/io/tackle/applicationinventory/entities/BulkCopyReview.java
new file mode 100644
index 0000000..1d50c68
--- /dev/null
+++ b/src/main/java/io/tackle/applicationinventory/entities/BulkCopyReview.java
@@ -0,0 +1,26 @@
+package io.tackle.applicationinventory.entities;
+
+import io.tackle.commons.entities.AbstractEntity;
+import org.hibernate.annotations.ResultCheckStyle;
+import org.hibernate.annotations.SQLDelete;
+import org.hibernate.annotations.Where;
+
+import javax.persistence.*;
+import java.util.HashSet;
+import java.util.Set;
+
+@Entity
+@Table(name = "bulk_copy_review")
+@SQLDelete(sql = "UPDATE bulk_copy_review SET deleted = true WHERE id = ?", check = ResultCheckStyle.COUNT)
+@Where(clause = "deleted = false")
+public class BulkCopyReview extends AbstractEntity {
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(foreignKey = @ForeignKey, name = "review_id")
+ public Review sourceReview;
+
+ @OneToMany(fetch = FetchType.LAZY, cascade = {CascadeType.ALL}, orphanRemoval = true, mappedBy = "bulkCopyReview")
+ public Set targetApplications = new HashSet<>();
+
+ public boolean completed = false;
+}
diff --git a/src/main/java/io/tackle/applicationinventory/entities/BulkCopyReviewDetails.java b/src/main/java/io/tackle/applicationinventory/entities/BulkCopyReviewDetails.java
new file mode 100644
index 0000000..c7fb203
--- /dev/null
+++ b/src/main/java/io/tackle/applicationinventory/entities/BulkCopyReviewDetails.java
@@ -0,0 +1,24 @@
+package io.tackle.applicationinventory.entities;
+
+import io.tackle.commons.entities.AbstractEntity;
+import org.hibernate.annotations.ResultCheckStyle;
+import org.hibernate.annotations.SQLDelete;
+import org.hibernate.annotations.Where;
+
+import javax.persistence.*;
+
+@Entity
+@Table(name = "bulk_copy_review_details")
+@SQLDelete(sql = "UPDATE bulk_copy_review_details SET deleted = true WHERE id = ?", check = ResultCheckStyle.COUNT)
+@Where(clause = "deleted = false")
+public class BulkCopyReviewDetails extends AbstractEntity {
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(foreignKey = @ForeignKey, name = "bulk_copy_review_id")
+ public BulkCopyReview bulkCopyReview;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(foreignKey = @ForeignKey, name = "application_id")
+ public Application application;
+
+}
diff --git a/src/main/java/io/tackle/applicationinventory/entities/Review.java b/src/main/java/io/tackle/applicationinventory/entities/Review.java
index f3e8fe9..efa1560 100644
--- a/src/main/java/io/tackle/applicationinventory/entities/Review.java
+++ b/src/main/java/io/tackle/applicationinventory/entities/Review.java
@@ -26,4 +26,8 @@ public class Review extends AbstractEntity {
@OneToOne(fetch = FetchType.LAZY, optional = false)
@JsonIgnoreProperties(value = {"review"}, allowSetters = true)
public Application application;
+
+ // No foreign keys associated to this field since foreign keys might
+ // complicate DELETE operations.
+ public Long copiedFromReviewId;
}
diff --git a/src/main/java/io/tackle/applicationinventory/resources/ReviewBulkCopyResource.java b/src/main/java/io/tackle/applicationinventory/resources/ReviewBulkCopyResource.java
new file mode 100644
index 0000000..958ae66
--- /dev/null
+++ b/src/main/java/io/tackle/applicationinventory/resources/ReviewBulkCopyResource.java
@@ -0,0 +1,99 @@
+package io.tackle.applicationinventory.resources;
+
+import io.tackle.applicationinventory.dto.BulkReviewDto;
+import io.tackle.applicationinventory.dto.utils.EntityToDTO;
+import io.tackle.applicationinventory.entities.Application;
+import io.tackle.applicationinventory.entities.BulkCopyReview;
+import io.tackle.applicationinventory.entities.BulkCopyReviewDetails;
+import io.tackle.applicationinventory.entities.Review;
+import io.tackle.applicationinventory.services.BulkCopyReviewService;
+import io.vertx.core.eventbus.EventBus;
+
+import javax.inject.Inject;
+import javax.transaction.NotSupportedException;
+import javax.transaction.*;
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.*;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Path("/review/bulk")
+@Produces("application/json")
+@Consumes("application/json")
+public class ReviewBulkCopyResource {
+
+ @Inject
+ UserTransaction transaction;
+
+ @Inject
+ EventBus eventBus;
+
+ private void handleSimpleRollback(UserTransaction transaction) {
+ try {
+ transaction.rollback();
+ } catch (SystemException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @POST
+ public BulkReviewDto createBulkCopyReview(
+ @NotNull @Valid BulkReviewDto bulkReviewInput
+ ) throws SystemException, NotSupportedException, HeuristicRollbackException, HeuristicMixedException, RollbackException {
+ transaction.begin();
+
+ // Find source review
+ Long sourceReviewId = bulkReviewInput.getSourceReview();
+ Review sourceReview = Review.findByIdOptional(sourceReviewId)
+ .orElseThrow(() -> {
+ handleSimpleRollback(transaction);
+ return new BadRequestException("Source review not valid");
+ });
+
+ // Find target applications
+ List applicationsTarget = bulkReviewInput.getTargetApplications().stream()
+ .map(appId -> Application.findById(appId))
+ .collect(Collectors.toList());
+ if (applicationsTarget.stream().anyMatch(Objects::isNull)) {
+ handleSimpleRollback(transaction);
+ throw new BadRequestException("One or more target applications is not valid");
+ }
+
+ // Prepare JPA object to be persisted
+ BulkCopyReview bulkCopyReviewOutput = new BulkCopyReview();
+
+ bulkCopyReviewOutput.sourceReview = sourceReview;
+ bulkCopyReviewOutput.targetApplications = applicationsTarget.stream()
+ .map(application -> {
+ BulkCopyReviewDetails detail = new BulkCopyReviewDetails();
+ detail.bulkCopyReview = bulkCopyReviewOutput;
+ detail.application = application;
+
+ return detail;
+ })
+ .collect(Collectors.toSet());
+ bulkCopyReviewOutput.completed = false;
+
+ bulkCopyReviewOutput.persist();
+ transaction.commit();
+
+ // Fire bus event
+ eventBus.send(BulkCopyReviewService.BUS_EVENT, bulkCopyReviewOutput.id);
+
+ // Generate response
+ return EntityToDTO.toDTO(bulkCopyReviewOutput);
+ }
+
+ @GET
+ @Path("/{id}")
+ public BulkReviewDto getBulkCopyReview(@PathParam("id") Long id) {
+ BulkCopyReview entity = BulkCopyReview
+ .findByIdOptional(id)
+ .orElseThrow(NotFoundException::new);
+
+ return EntityToDTO.toDTO(entity);
+ }
+
+}
diff --git a/src/main/java/io/tackle/applicationinventory/services/BulkCopyReviewService.java b/src/main/java/io/tackle/applicationinventory/services/BulkCopyReviewService.java
new file mode 100644
index 0000000..f9f4981
--- /dev/null
+++ b/src/main/java/io/tackle/applicationinventory/services/BulkCopyReviewService.java
@@ -0,0 +1,53 @@
+package io.tackle.applicationinventory.services;
+
+import io.quarkus.vertx.ConsumeEvent;
+import io.smallrye.common.annotation.Blocking;
+import io.tackle.applicationinventory.entities.Application;
+import io.tackle.applicationinventory.entities.BulkCopyReview;
+import io.tackle.applicationinventory.entities.Review;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.transaction.Transactional;
+
+@ApplicationScoped
+public class BulkCopyReviewService {
+
+ public static final String BUS_EVENT = "process-bulk-review-copy";
+
+ @Transactional
+ @ConsumeEvent(BUS_EVENT)
+ @Blocking
+ public void processBulkCopyReview(Long bulkId) {
+ BulkCopyReview bulk = BulkCopyReview.findById(bulkId);
+
+ // Copy review to all apps
+ bulk.targetApplications.forEach(detail -> {
+ Application application = Application.findById(detail.application.id);
+
+ Review oldReview = Review.find("application.id", detail.application.id).firstResult();
+ if (oldReview != null) {
+ oldReview = copyReviewFromSourceToTarget(bulk.sourceReview, oldReview);
+ oldReview.persist();
+ } else {
+ Review newReview = copyReviewFromSourceToTarget(bulk.sourceReview, new Review());
+ newReview.application = application;
+ newReview.persist();
+ }
+ });
+
+ bulk.completed = true;
+ bulk.persist();
+ }
+
+ private Review copyReviewFromSourceToTarget(Review source, Review target) {
+ target.proposedAction = source.proposedAction;
+ target.effortEstimate = source.effortEstimate;
+ target.businessCriticality = source.businessCriticality;
+ target.workPriority = source.workPriority;
+ target.comments = source.comments;
+
+ target.copiedFromReviewId = source.id;
+ return target;
+ }
+
+}
diff --git a/src/main/resources/db/migration/V20211008.1__add_bulk_copy_review.sql b/src/main/resources/db/migration/V20211008.1__add_bulk_copy_review.sql
new file mode 100644
index 0000000..b9ff475
--- /dev/null
+++ b/src/main/resources/db/migration/V20211008.1__add_bulk_copy_review.sql
@@ -0,0 +1,39 @@
+create table bulk_copy_review
+(
+ id int8 not null,
+
+ createTime timestamp,
+ createUser varchar(255),
+ deleted boolean,
+ updateTime timestamp,
+ updateUser varchar(255),
+
+ completed boolean,
+ review_id int8 not null,
+ primary key (id)
+);
+
+create table bulk_copy_review_details
+(
+ id int8 not null,
+
+ createTime timestamp,
+ createUser varchar(255),
+ deleted boolean,
+ updateTime timestamp,
+ updateUser varchar(255),
+
+ bulk_copy_review_id int8 not null,
+ application_id int8 not null,
+ primary key (id)
+);
+
+alter table if exists bulk_copy_review_details
+ add constraint fk_bulk_copy_review_details_application
+ foreign key (application_id)
+ references application;
+
+-- Add audit data
+
+alter table if exists review
+ add column copiedFromReviewId int8;
diff --git a/src/test/java/io/tackle/applicationinventory/flyway/FlywayMigrationTest.java b/src/test/java/io/tackle/applicationinventory/flyway/FlywayMigrationTest.java
index 8456bbe..f615b4e 100644
--- a/src/test/java/io/tackle/applicationinventory/flyway/FlywayMigrationTest.java
+++ b/src/test/java/io/tackle/applicationinventory/flyway/FlywayMigrationTest.java
@@ -13,16 +13,16 @@
@QuarkusTest
@TestProfile(FlywayMigrationProfile.class)
public class FlywayMigrationTest {
-
+
@Inject
Flyway flyway;
-
+
@Test
public void testMigration() {
// check the number of migrations applied equals the number of files in resources/db/migration folder
- assertEquals(18, flyway.info().applied().length);
+ assertEquals(19, flyway.info().applied().length);
// check the current migration version is the one from the last file in resources/db/migration folder
- assertEquals("20210914.1", flyway.info().current().getVersion().toString());
+ assertEquals("20211008.1", flyway.info().current().getVersion().toString());
// just a basic test to double check the application started
// to prove the flyway scripts ran successfully during startup
given()
diff --git a/src/test/java/io/tackle/applicationinventory/resources/BulkCopyReviewTest.java b/src/test/java/io/tackle/applicationinventory/resources/BulkCopyReviewTest.java
new file mode 100644
index 0000000..1503e48
--- /dev/null
+++ b/src/test/java/io/tackle/applicationinventory/resources/BulkCopyReviewTest.java
@@ -0,0 +1,347 @@
+package io.tackle.applicationinventory.resources;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.common.ResourceArg;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.ContentType;
+import io.tackle.applicationinventory.dto.BulkReviewDto;
+import io.tackle.applicationinventory.entities.Application;
+import io.tackle.applicationinventory.entities.Review;
+import io.tackle.commons.testcontainers.KeycloakTestResource;
+import io.tackle.commons.testcontainers.PostgreSQLDatabaseTestResource;
+import io.tackle.commons.tests.SecuredResourceTest;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+import static io.restassured.RestAssured.given;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+@QuarkusTest
+@QuarkusTestResource(value = PostgreSQLDatabaseTestResource.class,
+ initArgs = {
+ @ResourceArg(name = PostgreSQLDatabaseTestResource.DB_NAME, value = "application_inventory_db"),
+ @ResourceArg(name = PostgreSQLDatabaseTestResource.USER, value = "application_inventory"),
+ @ResourceArg(name = PostgreSQLDatabaseTestResource.PASSWORD, value = "application_inventory")
+ }
+)
+@QuarkusTestResource(value = KeycloakTestResource.class,
+ initArgs = {
+ @ResourceArg(name = KeycloakTestResource.IMPORT_REALM_JSON_PATH, value = "keycloak/quarkus-realm.json"),
+ @ResourceArg(name = KeycloakTestResource.REALM_NAME, value = "quarkus")
+ }
+)
+public class BulkCopyReviewTest extends SecuredResourceTest {
+
+ @BeforeAll
+ public static void init() {
+ PATH = "/review/bulk";
+ }
+
+ public void deleteApplication(Application... applications) {
+ // Clean data
+ Arrays.asList(applications)
+ .forEach(app -> {
+ given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .when()
+ .delete("/application/" + app.id)
+ .then()
+ .statusCode(204);
+ });
+ }
+
+ @Test
+ public void createBulkCopyWithNonExistingSource() {
+ BulkReviewDto bulkCopy = new BulkReviewDto();
+ bulkCopy.setSourceReview(Long.MAX_VALUE); // Non existing source
+ bulkCopy.setTargetApplications(Arrays.asList(1L, 2L, 3L)); // Whether or not the app's id exists is irrelevant since the Source is invalid.
+
+ given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(bulkCopy)
+ .when()
+ .post(PATH)
+ .then()
+ .statusCode(400);
+ }
+
+ @Test
+ public void createBulkCopyWithNonExistingTargetApps() {
+ // Create app1
+ Application app1 = new Application();
+ app1.name = "app1";
+
+ app1 = given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(app1)
+ .when()
+ .post("/application")
+ .then()
+ .statusCode(201)
+ .extract().body().as(Application.class);
+
+ // Create review (source) for app1
+ Review review1 = new Review();
+ review1.workPriority = 1;
+ review1.businessCriticality = 1;
+ review1.effortEstimate = "low";
+ review1.proposedAction = "rehost";
+ review1.application = app1;
+
+ review1 = given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(review1)
+ .when()
+ .post("/review")
+ .then()
+ .statusCode(201)
+ .extract().body().as(Review.class);
+
+ // Create bulk copy using valid source but non existing targets
+ BulkReviewDto bulkCopy = new BulkReviewDto();
+ bulkCopy.setSourceReview(review1.id);
+ bulkCopy.setTargetApplications(Arrays.asList(Long.MAX_VALUE - 1, Long.MAX_VALUE)); // Non existing apps (target)
+
+ given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(bulkCopy)
+ .when()
+ .post("/review/bulk")
+ .then()
+ .statusCode(400);
+
+ // Clean data
+ deleteApplication(app1);
+ }
+
+ @Test
+ public void createBulkCopy() {
+ // Case: create app1, app2, app3, and app4. App1 and app2 will have a review manually created.
+ // TestPhase1: Copy review from app1 to app3 and app4
+ // TestPhase2: Copy (override) review from app2 to app3 and app4
+
+ // Create app1, app2, app3, and app4
+ Application app1 = new Application();
+ app1.name = "app1";
+
+ Application app2 = new Application();
+ app2.name = "app2";
+
+ Application app3 = new Application();
+ app3.name = "app3";
+
+ Application app4 = new Application();
+ app4.name = "app4";
+
+ app1 = given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(app1)
+ .when()
+ .post("/application")
+ .then()
+ .statusCode(201)
+ .extract().body().as(Application.class);
+
+ app2 = given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(app2)
+ .when()
+ .post("/application")
+ .then()
+ .statusCode(201)
+ .extract().body().as(Application.class);
+
+ app3 = given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(app3)
+ .when()
+ .post("/application")
+ .then()
+ .statusCode(201)
+ .extract().body().as(Application.class);
+
+ app4 = given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(app4)
+ .when()
+ .post("/application")
+ .then()
+ .statusCode(201)
+ .extract().body().as(Application.class);
+
+ // Create reviews for app1 and app2
+ Review review1 = new Review();
+ review1.workPriority = 1;
+ review1.businessCriticality = 1;
+ review1.effortEstimate = "low";
+ review1.proposedAction = "rehost";
+ review1.application = app1;
+
+ Review review2 = new Review();
+ review2.workPriority = 2;
+ review2.businessCriticality = 2;
+ review2.effortEstimate = "high";
+ review2.proposedAction = "replatform";
+ review2.application = app2;
+
+ review1 = given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(review1)
+ .when()
+ .post("/review")
+ .then()
+ .statusCode(201)
+ .extract().body().as(Review.class);
+
+ review2 = given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(review2)
+ .when()
+ .post("/review")
+ .then()
+ .statusCode(201)
+ .extract().body().as(Review.class);
+
+ // TestPhase1: Copy review from app1 to app3 and app4
+ BulkReviewDto bulkCopy = new BulkReviewDto();
+ bulkCopy.setSourceReview(review1.id);
+ bulkCopy.setTargetApplications(Arrays.asList(app3.id, app4.id));
+
+ BulkReviewDto bulkCopyToWatch1 = given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(bulkCopy)
+ .when()
+ .post("/review/bulk")
+ .then()
+ .statusCode(200)
+ .body("id", is(notNullValue()),
+ "completed", is(false)
+ )
+ .extract().body().as(BulkReviewDto.class);
+
+ await().atMost(20, TimeUnit.SECONDS).until(() -> given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .when()
+ .get(PATH + "/" + bulkCopyToWatch1.getId())
+ .then()
+ .statusCode(200)
+ .extract().body().as(BulkReviewDto.class)
+ .isCompleted()
+ );
+
+ given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .when()
+ .get("/application/" + app3.id)
+ .then()
+ .statusCode(200)
+ .body("id", is(notNullValue()),
+ "review", is(notNullValue()),
+ "review.workPriority", is(review1.workPriority),
+ "review.businessCriticality", is(review1.businessCriticality),
+ "review.effortEstimate", is(review1.effortEstimate),
+ "review.proposedAction", is(review1.proposedAction),
+ "review.copiedFromReviewId", is(review1.id.intValue())
+ );
+
+ given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .when()
+ .get("/application/" + app4.id)
+ .then()
+ .statusCode(200)
+ .body("id", is(notNullValue()),
+ "review", is(notNullValue()),
+ "review.workPriority", is(review1.workPriority),
+ "review.businessCriticality", is(review1.businessCriticality),
+ "review.effortEstimate", is(review1.effortEstimate),
+ "review.proposedAction", is(review1.proposedAction),
+ "review.copiedFromReviewId", is(review1.id.intValue())
+ );
+
+ // TestPhase2: Copy review from app2 to app3 and app4
+ bulkCopy = new BulkReviewDto();
+ bulkCopy.setSourceReview(review2.id);
+ bulkCopy.setTargetApplications(Arrays.asList(app3.id, app4.id));
+
+ BulkReviewDto bulkCopyToWatch2 = given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .body(bulkCopy)
+ .when()
+ .post("/review/bulk")
+ .then()
+ .statusCode(200)
+ .body("id", is(notNullValue()),
+ "completed", is(false)
+ )
+ .extract().body().as(BulkReviewDto.class);
+
+ await().atMost(20, TimeUnit.SECONDS).until(() -> given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .when()
+ .get(PATH + "/" + bulkCopyToWatch2.getId())
+ .then()
+ .statusCode(200)
+ .extract().body().as(BulkReviewDto.class)
+ .isCompleted()
+ );
+
+ given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .when()
+ .get("/application/" + app3.id)
+ .then()
+ .statusCode(200)
+ .body("id", is(notNullValue()),
+ "review", is(notNullValue()),
+ "review.workPriority", is(review2.workPriority),
+ "review.businessCriticality", is(review2.businessCriticality),
+ "review.effortEstimate", is(review2.effortEstimate),
+ "review.proposedAction", is(review2.proposedAction),
+ "review.copiedFromReviewId", is(review2.id.intValue())
+ );
+
+ given()
+ .contentType(ContentType.JSON)
+ .accept(ContentType.JSON)
+ .when()
+ .get("/application/" + app4.id)
+ .then()
+ .statusCode(200)
+ .body("id", is(notNullValue()),
+ "review", is(notNullValue()),
+ "review.workPriority", is(review2.workPriority),
+ "review.businessCriticality", is(review2.businessCriticality),
+ "review.effortEstimate", is(review2.effortEstimate),
+ "review.proposedAction", is(review2.proposedAction),
+ "review.copiedFromReviewId", is(review2.id.intValue())
+ );
+
+ // Clean data
+ deleteApplication(app1, app2, app3, app4);
+ }
+
+}
diff --git a/src/test/java/io/tackle/applicationinventory/resources/NativeBulkCopyReviewIT.java b/src/test/java/io/tackle/applicationinventory/resources/NativeBulkCopyReviewIT.java
new file mode 100644
index 0000000..90275c2
--- /dev/null
+++ b/src/test/java/io/tackle/applicationinventory/resources/NativeBulkCopyReviewIT.java
@@ -0,0 +1,7 @@
+package io.tackle.applicationinventory.resources;
+
+import io.quarkus.test.junit.NativeImageTest;
+
+@NativeImageTest
+public class NativeBulkCopyReviewIT extends BulkCopyReviewTest {
+}