diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/controller/composite/CompositeController.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/controller/composite/CompositeController.java index c84b272e..df91345d 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/controller/composite/CompositeController.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/controller/composite/CompositeController.java @@ -5,18 +5,17 @@ import com.netcracker.cloud.dbaas.entity.pg.composite.CompositeStructure; import com.netcracker.cloud.dbaas.exceptions.NamespaceCompositeValidationException; import com.netcracker.cloud.dbaas.service.composite.CompositeNamespaceService; -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import jakarta.annotation.security.RolesAllowed; import jakarta.transaction.Transactional; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.extern.slf4j.Slf4j; - import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -85,7 +84,10 @@ public Response getAllCompositeStructures() { log.info("Received request to get all composite structures"); List compositeStructures = compositeService.getAllCompositeStructures(); List compositeStructureResponse = compositeStructures.stream() - .map(compositeStructure -> new CompositeStructureDto(compositeStructure.getBaseline(), compositeStructure.getNamespaces())) + .map(compositeStructure -> CompositeStructureDto.builder() + .id(compositeStructure.getBaseline()) + .namespaces(compositeStructure.getNamespaces()) + .build()) .toList(); return Response.ok(compositeStructureResponse).build(); } @@ -108,7 +110,11 @@ public Response getCompositeById(@PathParam("compositeId") String compositeId) { if (composite.isEmpty()) { return getNotFoundTmfErrorResponse(compositeId); } - return Response.ok(new CompositeStructureDto(composite.get().getBaseline(), composite.get().getNamespaces())).build(); + return Response.ok(CompositeStructureDto.builder() + .id(composite.get().getBaseline()) + .namespaces(composite.get().getNamespaces()) + .build()) + .build(); } @NotNull diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dao/jpa/CompositeNamespaceModifyIndexesDbaasRepositoryImpl.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dao/jpa/CompositeNamespaceModifyIndexesDbaasRepositoryImpl.java new file mode 100644 index 00000000..4d0c77af --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dao/jpa/CompositeNamespaceModifyIndexesDbaasRepositoryImpl.java @@ -0,0 +1,28 @@ +package com.netcracker.cloud.dbaas.dao.jpa; + +import com.netcracker.cloud.dbaas.entity.pg.composite.CompositeProperties; +import com.netcracker.cloud.dbaas.repositories.dbaas.CompositeNamespaceModifyIndexesDbaasRepository; +import com.netcracker.cloud.dbaas.repositories.pg.jpa.CompositeNamespaceModifyIndexesRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; +import lombok.AllArgsConstructor; + +import java.util.Optional; + +@AllArgsConstructor +@ApplicationScoped +public class CompositeNamespaceModifyIndexesDbaasRepositoryImpl implements CompositeNamespaceModifyIndexesDbaasRepository { + + private CompositeNamespaceModifyIndexesRepository compositeNamespaceModifyIndexesRepository; + + @Override + public Optional findByBaselineName(String baselineName) { + return compositeNamespaceModifyIndexesRepository.findByBaseline(baselineName); + } + + @Transactional + @Override + public void save(CompositeProperties compositeNamespacesModifyIndex) { + compositeNamespaceModifyIndexesRepository.persist(compositeNamespacesModifyIndex); + } +} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/composite/CompositeStructureDto.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/composite/CompositeStructureDto.java index 8737fc4e..d5159405 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/composite/CompositeStructureDto.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/dto/composite/CompositeStructureDto.java @@ -1,12 +1,15 @@ package com.netcracker.cloud.dbaas.dto.composite; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.*; import org.eclipse.microprofile.openapi.annotations.media.Schema; -import lombok.Data; -import lombok.NonNull; +import java.math.BigDecimal; import java.util.Set; @Data +@AllArgsConstructor +@Builder public class CompositeStructureDto { @Schema(description = "Composite identifier. Usually it's baseline or origin baseline in blue-green scheme", required = true) @@ -16,4 +19,8 @@ public class CompositeStructureDto { @Schema(description = "Namespaces that are included in composite structure (baseline and satellites)", required = true) @NonNull private Set namespaces; + + @Schema(description = "Index of composite structure (changes on each composite struct modification)", required = true) + @PositiveOrZero + private Long modifyIndex; } diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/composite/CompositeProperties.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/composite/CompositeProperties.java new file mode 100644 index 00000000..c2a7692e --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/entity/pg/composite/CompositeProperties.java @@ -0,0 +1,30 @@ +package com.netcracker.cloud.dbaas.entity.pg.composite; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Data +@NoArgsConstructor +@Table(name = "composite_properties") +@Entity(name = "CompositeProperties") +public class CompositeProperties { + @Id + @Column(name = "composite_namespace_id") + public UUID id; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + @JoinColumn(name = "composite_namespace_id") + private CompositeNamespace compositeNamespace; + + @Column(name = "modify_index", nullable = false) + private long modifyIndex; + + public CompositeProperties(CompositeNamespace compositeNamespace, long modifyIndex) { + this.compositeNamespace = compositeNamespace; + this.modifyIndex = modifyIndex; + } +} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/dbaas/CompositeNamespaceModifyIndexesDbaasRepository.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/dbaas/CompositeNamespaceModifyIndexesDbaasRepository.java new file mode 100644 index 00000000..d9c19e17 --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/dbaas/CompositeNamespaceModifyIndexesDbaasRepository.java @@ -0,0 +1,12 @@ +package com.netcracker.cloud.dbaas.repositories.dbaas; + +import com.netcracker.cloud.dbaas.entity.pg.composite.CompositeProperties; + +import java.util.Optional; + +public interface CompositeNamespaceModifyIndexesDbaasRepository { + Optional findByBaselineName(String baselineName); + + void save(CompositeProperties compositeNamespacesModifyIndex); + +} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/pg/jpa/CompositeNamespaceModifyIndexesRepository.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/pg/jpa/CompositeNamespaceModifyIndexesRepository.java new file mode 100644 index 00000000..d99b8bcc --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/repositories/pg/jpa/CompositeNamespaceModifyIndexesRepository.java @@ -0,0 +1,19 @@ +package com.netcracker.cloud.dbaas.repositories.pg.jpa; + +import com.netcracker.cloud.dbaas.entity.pg.composite.CompositeProperties; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.Optional; +import java.util.UUID; + +@ApplicationScoped +public class CompositeNamespaceModifyIndexesRepository implements PanacheRepositoryBase { + + public Optional findByBaseline(String baseline) { + return find( + "compositeNamespace.baseline", + baseline + ).firstResultOptional(); + } +} diff --git a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/service/composite/CompositeNamespaceService.java b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/service/composite/CompositeNamespaceService.java index d082827f..fbb6789f 100644 --- a/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/service/composite/CompositeNamespaceService.java +++ b/dbaas/dbaas-aggregator/src/main/java/com/netcracker/cloud/dbaas/service/composite/CompositeNamespaceService.java @@ -1,12 +1,16 @@ package com.netcracker.cloud.dbaas.service.composite; +import com.netcracker.cloud.dbaas.dto.Source; import com.netcracker.cloud.dbaas.dto.composite.CompositeStructureDto; import com.netcracker.cloud.dbaas.entity.pg.composite.CompositeNamespace; +import com.netcracker.cloud.dbaas.entity.pg.composite.CompositeProperties; import com.netcracker.cloud.dbaas.entity.pg.composite.CompositeStructure; +import com.netcracker.cloud.dbaas.exceptions.NamespaceCompositeValidationException; import com.netcracker.cloud.dbaas.repositories.dbaas.CompositeNamespaceDbaasRepository; +import com.netcracker.cloud.dbaas.repositories.dbaas.CompositeNamespaceModifyIndexesDbaasRepository; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.EntityManager; import jakarta.transaction.Transactional; - import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; @@ -19,14 +23,28 @@ @ApplicationScoped public class CompositeNamespaceService { - private CompositeNamespaceDbaasRepository compositeNamespaceDbaasRepository; + private final CompositeNamespaceDbaasRepository compositeNamespaceDbaasRepository; + private final CompositeNamespaceModifyIndexesDbaasRepository compositeNamespaceModifyIndexesDbaasRepository; + private final EntityManager entityManager; - public CompositeNamespaceService(CompositeNamespaceDbaasRepository compositeNamespaceDbaasRepository) { + public CompositeNamespaceService(CompositeNamespaceDbaasRepository compositeNamespaceDbaasRepository, + CompositeNamespaceModifyIndexesDbaasRepository compositeNamespaceModifyIndexesDbaasRepository, + EntityManager entityManager) { this.compositeNamespaceDbaasRepository = compositeNamespaceDbaasRepository; + this.compositeNamespaceModifyIndexesDbaasRepository = compositeNamespaceModifyIndexesDbaasRepository; + this.entityManager = entityManager; } @Transactional public void saveOrUpdateCompositeStructure(CompositeStructureDto compositeRequest) { + if (compositeRequest.getModifyIndex() != null) { + txLock(compositeRequest.getId()); + Optional currentModifyIndex = compositeNamespaceModifyIndexesDbaasRepository.findByBaselineName(compositeRequest.getId()); + if (currentModifyIndex.isPresent() && compositeRequest.getModifyIndex() < currentModifyIndex.get().getModifyIndex()) { + throw new NamespaceCompositeValidationException(Source.builder().pointer("/modifyIndex").build(), "new modify index '%s' should be greater than current index '%s'".formatted(compositeRequest.getModifyIndex(), currentModifyIndex.get().getModifyIndex())); + } + } + deleteCompositeStructure(compositeRequest.getId()); compositeNamespaceDbaasRepository.flush(); // need to flush because jpa first tries to save data without deleting it compositeRequest.getNamespaces().add(compositeRequest.getId()); @@ -34,6 +52,10 @@ public void saveOrUpdateCompositeStructure(CompositeStructureDto compositeReques .map(ns -> buildCompositeNamespace(compositeRequest, ns)) .toList(); compositeNamespaceDbaasRepository.saveAll(compositeNamespaces); + if (compositeRequest.getModifyIndex() != null) { + compositeNamespaceDbaasRepository.findBaselineByNamespace(compositeRequest.getId()) + .ifPresent(compositeNamespace -> compositeNamespaceModifyIndexesDbaasRepository.save(new CompositeProperties(compositeNamespace, compositeRequest.getModifyIndex()))); + } } @NotNull @@ -95,4 +117,12 @@ public Optional getBaselineByNamespace(String namespace) { return compositeNamespaceDbaasRepository.findBaselineByNamespace(namespace) .map(CompositeNamespace::getBaseline); } + + private void txLock(String baseline) { + entityManager.createNativeQuery( + "SELECT pg_advisory_xact_lock(hashtext(:baseline))" + ) + .setParameter("baseline", baseline) + .getSingleResult(); + } } diff --git a/dbaas/dbaas-aggregator/src/main/resources/db/migration/postgresql/V1.034__Composite_Namespace_Modify_Indexes.sql b/dbaas/dbaas-aggregator/src/main/resources/db/migration/postgresql/V1.034__Composite_Namespace_Modify_Indexes.sql new file mode 100644 index 00000000..b8097c47 --- /dev/null +++ b/dbaas/dbaas-aggregator/src/main/resources/db/migration/postgresql/V1.034__Composite_Namespace_Modify_Indexes.sql @@ -0,0 +1,7 @@ +create table if not exists composite_properties +( + composite_namespace_id uuid primary key + references composite_namespace(id) + on delete cascade, + modify_index numeric(20) not null check (modify_index >= 0) +); diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/controller/composite/CompositeControllerTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/controller/composite/CompositeControllerTest.java index 8603dd42..5356c543 100644 --- a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/controller/composite/CompositeControllerTest.java +++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/controller/composite/CompositeControllerTest.java @@ -6,27 +6,31 @@ import com.netcracker.cloud.dbaas.entity.pg.composite.CompositeStructure; import com.netcracker.cloud.dbaas.integration.config.PostgresqlContainerResource; import com.netcracker.cloud.dbaas.service.composite.CompositeNamespaceService; -import io.quarkus.test.InjectMock; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectSpy; +import io.restassured.RestAssured; import io.restassured.common.mapper.TypeRef; +import io.restassured.config.LogConfig; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; import jakarta.ws.rs.core.MediaType; - import org.junit.jupiter.api.Test; +import java.math.BigDecimal; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.IntStream; import static io.restassured.RestAssured.given; import static jakarta.ws.rs.core.Response.Status.*; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @QuarkusTest @@ -34,9 +38,12 @@ @TestHTTPEndpoint(CompositeController.class) class CompositeControllerTest { - @InjectMock + @InjectSpy CompositeNamespaceService compositeService; + @Inject + EntityManager entityManager; + @Test void testGetAllCompositeStructures_Success() { CompositeStructure expected = new CompositeStructure("ns-1", Set.of("ns-1", "ns-2")); @@ -67,8 +74,6 @@ void testGetAllCompositeStructures_EmptyList() { .body(is("[]")); verify(compositeService).getAllCompositeStructures(); - verifyNoMoreInteractions(compositeService); - } @Test @@ -121,7 +126,11 @@ void testGetCompositeById_InternalServerError() { @Test void testSaveOrUpdateComposite_Success() throws JsonProcessingException { - CompositeStructureDto request = new CompositeStructureDto("test-id", Set.of("ns-1", "ns-2")); + compositeService.deleteCompositeStructure("ns-1"); + CompositeStructureDto request = CompositeStructureDto.builder() + .id("ns-1") + .namespaces(Set.of("ns-1", "ns-2")) + .build(); given().auth().preemptive().basic("cluster-dba", "someDefaultPassword") .contentType(MediaType.APPLICATION_JSON) .body((new ObjectMapper()).writeValueAsString(request)) @@ -132,12 +141,14 @@ void testSaveOrUpdateComposite_Success() throws JsonProcessingException { verify(compositeService).saveOrUpdateCompositeStructure(request); verify(compositeService).getBaselineByNamespace("ns-1"); verify(compositeService).getBaselineByNamespace("ns-2"); - verifyNoMoreInteractions(compositeService); } @Test void testSaveOrUpdateComposite_IdBlank() throws JsonProcessingException { - CompositeStructureDto request = new CompositeStructureDto("", Set.of("ns-1", "ns-2")); + CompositeStructureDto request = CompositeStructureDto.builder() + .id("") + .namespaces(Set.of("ns-1", "ns-2")) + .build(); given().auth().preemptive().basic("cluster-dba", "someDefaultPassword") .contentType(MediaType.APPLICATION_JSON) @@ -152,7 +163,10 @@ void testSaveOrUpdateComposite_IdBlank() throws JsonProcessingException { @Test void testSaveOrUpdateComposite_NamespacesEmpty() throws JsonProcessingException { - CompositeStructureDto request = new CompositeStructureDto("test-id", Collections.emptySet()); + CompositeStructureDto request = CompositeStructureDto.builder() + .id("test-id") + .namespaces(Collections.emptySet()) + .build(); given().auth().preemptive().basic("cluster-dba", "someDefaultPassword") .contentType(MediaType.APPLICATION_JSON) @@ -167,7 +181,10 @@ void testSaveOrUpdateComposite_NamespacesEmpty() throws JsonProcessingException @Test void testSaveOrUpdateComposite_NamespaceConflict() throws JsonProcessingException { - CompositeStructureDto request = new CompositeStructureDto("test-id", Set.of("ns-1", "ns-2")); + CompositeStructureDto request = CompositeStructureDto.builder() + .id("test-id") + .namespaces(Set.of("ns-1", "ns-2")) + .build(); when(compositeService.getBaselineByNamespace("ns-2")).thenReturn(Optional.of("existing-id")); given().auth().preemptive().basic("cluster-dba", "someDefaultPassword") @@ -181,6 +198,28 @@ void testSaveOrUpdateComposite_NamespaceConflict() throws JsonProcessingExceptio verify(compositeService, never()).saveOrUpdateCompositeStructure(request); } + @Test + void testSaveOrUpdateComposite_WrongModifyIndex() throws JsonProcessingException { + given().auth().preemptive().basic("cluster-dba", "someDefaultPassword") + .contentType(MediaType.APPLICATION_JSON) + .body((new ObjectMapper()).writeValueAsString(new CompositeStructureDto("ns-1", Set.of("ns-1", "ns-2"), 1L))) + .when().post() + .then() + .statusCode(NO_CONTENT.getStatusCode()); + given().auth().preemptive().basic("cluster-dba", "someDefaultPassword") + .contentType(MediaType.APPLICATION_JSON) + .body((new ObjectMapper()).writeValueAsString(new CompositeStructureDto("ns-1", Set.of("ns-1", "ns-2"), 2L))) + .when().post() + .then() + .statusCode(NO_CONTENT.getStatusCode()); + given().auth().preemptive().basic("cluster-dba", "someDefaultPassword") + .contentType(MediaType.APPLICATION_JSON) + .body((new ObjectMapper()).writeValueAsString(new CompositeStructureDto("ns-1", Set.of("ns-1", "ns-2"), 1L))) + .when().post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .body("message", is("Validation error: 'new modify index '1' should be greater than current index '2''")); + } @Test void testDeleteCompositeById_Success() { @@ -194,7 +233,6 @@ void testDeleteCompositeById_Success() { verify(compositeService).getCompositeStructure("test-id"); verify(compositeService).deleteCompositeStructure("test-id"); - verifyNoMoreInteractions(compositeService); } @Test @@ -221,4 +259,38 @@ void testDeleteCompositeById_InternalServerError() { .statusCode(INTERNAL_SERVER_ERROR.getStatusCode()) .body("message", is("Internal Server Error")); } + + @Test + void saveOrUpdateComposite_concurrent() { + RestAssured.config = RestAssured.config() + .logConfig(LogConfig.logConfig().enablePrettyPrinting(false)); + List> futures = + IntStream.range(0, 100) + .mapToObj(i -> + CompletableFuture.runAsync(() -> { + CompositeStructureDto request = CompositeStructureDto.builder() + .id("base") + .namespaces(Set.of("ns-%d".formatted(i))) + .modifyIndex(i == 50 ? 1000 : (long) ThreadLocalRandom.current().nextInt(1000)) + .build(); + try { + given().auth().preemptive().basic("cluster-dba", "someDefaultPassword") + .contentType(MediaType.APPLICATION_JSON) + .body((new ObjectMapper()).writeValueAsString(request)) + .when().post(); + } catch (JsonProcessingException e) { + fail("something went wrong", e); + } + } + ) + ).toList(); + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + + Object singleResult = entityManager.createNativeQuery( + "SELECT MAX(modify_index) FROM composite_properties" + ) + .getSingleResult(); + assertEquals(BigDecimal.valueOf(1000), singleResult); + } } diff --git a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/service/composite/CompositeNamespaceServiceTest.java b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/service/composite/CompositeNamespaceServiceTest.java index fdca7ce2..8cf7ac06 100644 --- a/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/service/composite/CompositeNamespaceServiceTest.java +++ b/dbaas/dbaas-aggregator/src/test/java/com/netcracker/cloud/dbaas/service/composite/CompositeNamespaceServiceTest.java @@ -4,6 +4,9 @@ import com.netcracker.cloud.dbaas.entity.pg.composite.CompositeNamespace; import com.netcracker.cloud.dbaas.entity.pg.composite.CompositeStructure; import com.netcracker.cloud.dbaas.repositories.dbaas.CompositeNamespaceDbaasRepository; +import com.netcracker.cloud.dbaas.repositories.dbaas.CompositeNamespaceModifyIndexesDbaasRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -22,26 +25,40 @@ class CompositeNamespaceServiceTest { @Mock private CompositeNamespaceDbaasRepository compositeNamespaceDbaasRepository; + @Mock + private EntityManager entityManager; + + @Mock + private CompositeNamespaceModifyIndexesDbaasRepository compositeNamespaceModifyIndexesDbaasRepository; + @InjectMocks private CompositeNamespaceService compositeNamespaceService; @Test void testSaveOrUpdateCompositeStructure_Success() { - CompositeStructureDto compositeRequest = new CompositeStructureDto("test-id", new HashSet<>(Set.of("ns-1", "ns-2"))); - + CompositeStructureDto compositeRequest = CompositeStructureDto.builder() + .id("test-id") + .namespaces(new HashSet<>(Set.of("ns-1", "ns-2"))) + .modifyIndex(1L) + .build(); + + Query query = mock(Query.class); + when(query.setParameter(anyString(), anyString())).thenReturn(query); + when(entityManager.createNativeQuery("SELECT pg_advisory_xact_lock(hashtext(:baseline))")).thenReturn(query); assertDoesNotThrow(() -> compositeNamespaceService.saveOrUpdateCompositeStructure(compositeRequest)); - verify(compositeNamespaceDbaasRepository, times(1)).deleteByBaseline("test-id"); - ArgumentCaptor compositeNamespaceCaptureList = ArgumentCaptor.forClass(List.class); + ArgumentCaptor> compositeNamespaceCaptureList = ArgumentCaptor.forClass(List.class); verify(compositeNamespaceDbaasRepository, times(1)).saveAll(compositeNamespaceCaptureList.capture()); List compositeNamespaceList = compositeNamespaceCaptureList.getAllValues().stream().flatMap(Collection::stream).toList(); assertEquals(3, compositeNamespaceList.size()); - HashSet expectedNs = new HashSet(Arrays.asList("test-id", "ns-1", "ns-2")); + HashSet expectedNs = new HashSet<>(Arrays.asList("test-id", "ns-1", "ns-2")); for (CompositeNamespace compositeNamespace : compositeNamespaceList) { assertEquals("test-id", compositeNamespace.getBaseline()); assertTrue(expectedNs.remove(compositeNamespace.getNamespace()), "list does not contain %s but should".formatted(compositeNamespace.getNamespace())); } + + verify(compositeNamespaceModifyIndexesDbaasRepository).findByBaselineName("test-id"); } @Test diff --git a/docs/OpenAPI.json b/docs/OpenAPI.json index f45181c5..895c7e93 100644 --- a/docs/OpenAPI.json +++ b/docs/OpenAPI.json @@ -680,7 +680,8 @@ "type": "object", "required": [ "id", - "namespaces" + "namespaces", + "modifyIndex" ], "properties": { "id": { @@ -694,6 +695,11 @@ "type": "string" }, "description": "Namespaces that are included in composite structure (baseline and satellites)" + }, + "modifyIndex": { + "type": "number", + "description": "Index of composite structure (changes on each composite struct modification)", + "minimum": 0 } } },