From c0b68d81521d86ad02b6434a5aaad8ce6f5e9c1a Mon Sep 17 00:00:00 2001 From: p3t <3204560+p3t@users.noreply.github.com> Date: Mon, 13 May 2024 19:10:00 +0200 Subject: [PATCH] Added test for POST endpoint, fix for DataRecordAttribute - Metamodel properties cannot be used here when entity-manager not present - Added explicit dependency on spring-boot validation (error when missing) - Docu update --- .github/workflows/publish-githubpkg.yml | 4 ++ README.md | 8 ++- cursorpaging-jpa-api/build.gradle.kts | 1 + .../cursorpaging/jpa/api/DtoPageRequest.java | 19 ++++++- .../io/vigier/cursorpaging/jpa/Attribute.java | 11 ++++ .../api/controller/DataRecordController.java | 5 +- .../api/model/DataRecordAttribute.java | 27 ++++++---- .../controller/DataRecordControllerTest.java | 50 +++++++++++++++---- 8 files changed, 100 insertions(+), 25 deletions(-) diff --git a/.github/workflows/publish-githubpkg.yml b/.github/workflows/publish-githubpkg.yml index e099abe..6dfa4bd 100644 --- a/.github/workflows/publish-githubpkg.yml +++ b/.github/workflows/publish-githubpkg.yml @@ -23,10 +23,14 @@ jobs: publish: runs-on: ubuntu-latest + permissions: contents: read packages: write + outputs: + BUILD_VERSION: ${{ steps.release_version.outputs.BUILD_VERSION }} + steps: - name: "Release Version" id: release_version diff --git a/README.md b/README.md index 8ee1808..8c111eb 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,13 @@ There are two dependencies: ``` -Note: Currently the library is only available on gitHub-packages: https://maven.pkg.github.com/p3t/spring-cursorpaging +Note: Currently the library is only available on gitHub-packages: + +- Repository-URL: https://maven.pkg.github.com/p3t/spring-cursorpaging +- You need an personal-access token (classic) to read from any github-package + repo [see docu](https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages#authenticating-to-github-packages) + - Add the token to your `~/.m2/settings.xml` or `~/.gradle/gradle.properties` file as password + - username is you github-user ## Generate the JPA meta-model diff --git a/cursorpaging-jpa-api/build.gradle.kts b/cursorpaging-jpa-api/build.gradle.kts index 84ff71b..2f02bb8 100644 --- a/cursorpaging-jpa-api/build.gradle.kts +++ b/cursorpaging-jpa-api/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(project(":cursorpaging-jpa")) implementation("com.google.protobuf:protobuf-java:4.26.1") implementation("org.springframework:spring-core") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("jakarta.validation:jakarta.validation-api") implementation("jakarta.persistence:jakarta.persistence-api:3.1.0") diff --git a/cursorpaging-jpa-api/src/main/java/io/vigier/cursorpaging/jpa/api/DtoPageRequest.java b/cursorpaging-jpa-api/src/main/java/io/vigier/cursorpaging/jpa/api/DtoPageRequest.java index ea4fa16..f32c863 100644 --- a/cursorpaging-jpa-api/src/main/java/io/vigier/cursorpaging/jpa/api/DtoPageRequest.java +++ b/cursorpaging-jpa-api/src/main/java/io/vigier/cursorpaging/jpa/api/DtoPageRequest.java @@ -24,7 +24,22 @@ public class DtoPageRequest { @Min( 1 ) @Max( 100 ) - private final int pageSize = 100; + private int pageSize = 100; // Must not be final! + + public DtoPageRequest withPageSize( final int pageSize ) { + this.pageSize = pageSize; + return this; + } + + public DtoPageRequest withOrderBy( final String name, final Order order ) { + orderBy.put( name, order ); + return this; + } + + public DtoPageRequest withFilterBy( final String name, final String... values ) { + filterBy.put( name, List.of( values ) ); + return this; + } public PageRequest toPageRequest( final Function attributeProvider ) { return PageRequest.create( b -> { @@ -43,7 +58,7 @@ public PageRequest toPageRequest( final Function attri } ); } - public void addOrderByIfNotPresent( final String name, final Order order ) { + public void addOrderByIfAbsent( final String name, final Order order ) { if ( !orderBy.containsKey( name ) ) { orderBy.put( name, order ); } diff --git a/cursorpaging-jpa/src/main/java/io/vigier/cursorpaging/jpa/Attribute.java b/cursorpaging-jpa/src/main/java/io/vigier/cursorpaging/jpa/Attribute.java index 5f026ed..d8c07a8 100644 --- a/cursorpaging-jpa/src/main/java/io/vigier/cursorpaging/jpa/Attribute.java +++ b/cursorpaging-jpa/src/main/java/io/vigier/cursorpaging/jpa/Attribute.java @@ -63,6 +63,17 @@ public static Attribute path( final SingleAttribute... path ) { return new Attribute( Arrays.asList( path ) ); } + + public static Attribute path( final String name1, final Class type1, final String name2, final Class type2 ) { + return new Attribute( List.of( SingleAttribute.of( name1, type1 ), SingleAttribute.of( name2, type2 ) ) ); + } + + public static Attribute path( final String name1, final Class type1, final String name2, final Class type2, + final String name3, final Class type3 ) { + return new Attribute( List.of( SingleAttribute.of( name1, type1 ), SingleAttribute.of( name2, type2 ), + SingleAttribute.of( name3, type3 ) ) ); + } + Comparable valueOf( final Object entity ) { Object result = entity; for ( final SingleAttribute a : attributes ) { diff --git a/cursorpaging-testapp/src/main/java/io/vigier/cursorpaging/testapp/api/controller/DataRecordController.java b/cursorpaging-testapp/src/main/java/io/vigier/cursorpaging/testapp/api/controller/DataRecordController.java index 521908e..2c97f3f 100644 --- a/cursorpaging-testapp/src/main/java/io/vigier/cursorpaging/testapp/api/controller/DataRecordController.java +++ b/cursorpaging-testapp/src/main/java/io/vigier/cursorpaging/testapp/api/controller/DataRecordController.java @@ -134,9 +134,8 @@ public RepresentationModel getCursor( "pageSize": 100 }""" ) ) ) // @RequestBody final DtoPageRequest request ) { - request.addOrderByIfNotPresent( DataRecord_.ID, Order.ASC ); - final PageRequest pageRequest = request.toPageRequest( - name -> DataRecordAttribute.valueOf( name.toUpperCase() ).getAttribute() ); + request.addOrderByIfAbsent( DataRecord_.ID, Order.ASC ); + final PageRequest pageRequest = request.toPageRequest( DataRecordAttribute::forName ); return RepresentationModel.of( request ) .add( getLink( Optional.of( request.getPageSize() ), pageRequest, IanaLinkRelations.FIRST ) ); } diff --git a/cursorpaging-testapp/src/main/java/io/vigier/cursorpaging/testapp/api/model/DataRecordAttribute.java b/cursorpaging-testapp/src/main/java/io/vigier/cursorpaging/testapp/api/model/DataRecordAttribute.java index b04bd4c..cb3380e 100644 --- a/cursorpaging-testapp/src/main/java/io/vigier/cursorpaging/testapp/api/model/DataRecordAttribute.java +++ b/cursorpaging-testapp/src/main/java/io/vigier/cursorpaging/testapp/api/model/DataRecordAttribute.java @@ -1,21 +1,28 @@ package io.vigier.cursorpaging.testapp.api.model; import io.vigier.cursorpaging.jpa.Attribute; +import io.vigier.cursorpaging.testapp.model.AuditInfo; import io.vigier.cursorpaging.testapp.model.AuditInfo_; import io.vigier.cursorpaging.testapp.model.DataRecord_; +import java.time.Instant; +import java.util.UUID; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor +@Getter public enum DataRecordAttribute { - ID( Attribute.of( DataRecord_.id ) ), - NAME( Attribute.of( DataRecord_.name ) ), - CREATED_AT( Attribute.path( DataRecord_.auditInfo, AuditInfo_.createdAt ) ), - MODIFIED_AT( Attribute.path( DataRecord_.auditInfo, AuditInfo_.modifiedAt ) ); - private final Attribute attribute; + // Not using the "lowercase" properties here, because there are tests where no EntityManager is instantiated + // (i.e. the MockMVC test), but this one is needed to populate them :-( - DataRecordAttribute( final Attribute attribute ) { - this.attribute = attribute; - } + ID( Attribute.of( DataRecord_.NAME, UUID.class ) ), + NAME( Attribute.of( DataRecord_.NAME, String.class ) ), + CREATED_AT( Attribute.path( DataRecord_.AUDIT_INFO, AuditInfo.class, AuditInfo_.CREATED_AT, Instant.class ) ), + MODIFIED_AT( Attribute.path( DataRecord_.AUDIT_INFO, AuditInfo.class, AuditInfo_.MODIFIED_AT, Instant.class ) ); + + private final Attribute attribute; - public Attribute getAttribute() { - return attribute; + public static Attribute forName( final String name ) { + return valueOf( name.toUpperCase() ).getAttribute(); } } diff --git a/cursorpaging-testapp/src/test/java/io/vigier/cursorpaging/testapp/api/controller/DataRecordControllerTest.java b/cursorpaging-testapp/src/test/java/io/vigier/cursorpaging/testapp/api/controller/DataRecordControllerTest.java index f4890d5..258046f 100644 --- a/cursorpaging-testapp/src/test/java/io/vigier/cursorpaging/testapp/api/controller/DataRecordControllerTest.java +++ b/cursorpaging-testapp/src/test/java/io/vigier/cursorpaging/testapp/api/controller/DataRecordControllerTest.java @@ -1,35 +1,43 @@ package io.vigier.cursorpaging.testapp.api.controller; -import static io.vigier.cursorpaging.testapp.api.controller.DataRecordController.PATH; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - +import com.fasterxml.jackson.databind.ObjectMapper; import io.vigier.cursorpaging.jpa.Attribute; +import io.vigier.cursorpaging.jpa.Order; import io.vigier.cursorpaging.jpa.PageRequest; +import io.vigier.cursorpaging.jpa.api.DtoPageRequest; import io.vigier.cursorpaging.jpa.serializer.Base64String; import io.vigier.cursorpaging.jpa.serializer.EntitySerializer; import io.vigier.cursorpaging.testapp.api.model.mapper.DtoDataRecordMapper; import io.vigier.cursorpaging.testapp.model.DataRecord; +import io.vigier.cursorpaging.testapp.model.DataRecord_; import io.vigier.cursorpaging.testapp.repository.DataRecordRepository; import java.util.Base64; import java.util.UUID; +import lombok.extern.slf4j.Slf4j; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import static io.vigier.cursorpaging.testapp.api.controller.DataRecordController.PATH; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @WebMvcTest( DataRecordController.class ) //@ExtendWith( MockitoExtension.class ) +@Slf4j class DataRecordControllerTest { private static final String PATH_COUNT = PATH + DataRecordController.COUNT; - + private static final String CURSOR = "OW2T2rDudgONjtP04KHOUguDTrUGTCA7edByRquqlqus1TaSdcr1JwMLSwiDcW88hp7zSMqJrn9Q-W94P1GFGMuAQNeWfMZ5vfK6Mf712w"; @Autowired private MockMvc mockMvc; @@ -78,4 +86,28 @@ void shouldReturnCountUsingCursor() throws Exception { .andExpect( status().isOk() ) .andExpect( jsonPath( "$.totalElements" ).value( 4711 ) ); } + + @Test + void shouldCreateNewCursorOnPost() throws Exception { + final var request = new DtoPageRequest().withFilterBy( DataRecord_.NAME, "Tango", "Bravo" ) + .withOrderBy( DataRecord_.NAME, Order.ASC ) + .withPageSize( 10 ); + final String json = new ObjectMapper().writeValueAsString( request ); + log.debug( "Json:, {}", json ); + + when( serializer.toBase64( any() ) ).thenReturn( new Base64String( CURSOR ) ); + + mockMvc.perform( post( PATH + "/page" ) // + .contentType( MediaType.APPLICATION_JSON ) // + .content( json ) ) // + .andExpect( status().isCreated() ) // + .andExpect( jsonPath( "$.orderBy.name" ).value( "ASC" ) ) + .andExpect( jsonPath( "$.orderBy.id" ).value( "ASC" ) ) + .andExpect( jsonPath( "$.filterBy.name" ).isArray() ) + .andExpect( jsonPath( "$.filterBy.name[0]" ).value( "Tango" ) ) + .andExpect( jsonPath( "$.filterBy.name[1]" ).value( "Bravo" ) ) + .andExpect( jsonPath( "$.pageSize" ).value( 10 ) ) // + .andExpect( jsonPath( "$._links.first.href" ).exists() ) + .andExpect( jsonPath( "$._links.first.href" ).value( Matchers.containsString( CURSOR ) ) ); + } } \ No newline at end of file