From 5abf4a8204b020a8b3ebb4fa2ae6a6de53c5a949 Mon Sep 17 00:00:00 2001 From: Raja Kolli Date: Fri, 10 Nov 2023 14:45:53 +0000 Subject: [PATCH] feat : add post details table and update endpoints --- .../graphql/querydsl/entities/Post.java | 27 ++-- .../querydsl/entities/PostDetails.java | 67 ++++++++++ .../graphql/querydsl/mapper/PostMapper.java | 15 ++- .../model/request/CreatePostRequest.java | 9 ++ .../querydsl/services/PostService.java | 5 +- .../graphql/querydsl/utils/AppConstants.java | 5 + .../web/controllers/PostController.java | 5 +- ...ags_table.xml => 01-create_tags_table.xml} | 0 .../db/changelog/migration/01-init.xml | 9 -- .../03-create_post_details_table.xml | 35 +++++ .../querydsl/SchemaValidationTest.java | 12 +- .../web/controllers/PostControllerIT.java | 36 +++-- .../web/controllers/PostControllerTest.java | 125 +++++++++++------- 13 files changed, 264 insertions(+), 86 deletions(-) create mode 100644 graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/entities/PostDetails.java create mode 100644 graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/model/request/CreatePostRequest.java rename graphql/boot-graphql-querydsl/src/main/resources/db/changelog/migration/{03-create_tags_table.xml => 01-create_tags_table.xml} (100%) delete mode 100644 graphql/boot-graphql-querydsl/src/main/resources/db/changelog/migration/01-init.xml create mode 100644 graphql/boot-graphql-querydsl/src/main/resources/db/changelog/migration/03-create_post_details_table.xml diff --git a/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/entities/Post.java b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/entities/Post.java index f29f91d24..ba1fabe47 100644 --- a/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/entities/Post.java +++ b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/entities/Post.java @@ -1,7 +1,6 @@ package com.example.graphql.querydsl.entities; import jakarta.persistence.*; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -27,11 +26,13 @@ public class Post { @Column(nullable = false) private String content; - private LocalDateTime createdOn; - @OneToMany(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true) private List comments = new ArrayList<>(); + @JoinColumn(name = "details_Id") + @OneToOne(cascade = CascadeType.ALL, mappedBy = "post", orphanRemoval = true, fetch = FetchType.LAZY) + private PostDetails details; + public Post setId(Long id) { this.id = id; return this; @@ -47,11 +48,6 @@ public Post setContent(String content) { return this; } - public Post setCreatedOn(LocalDateTime createdOn) { - this.createdOn = createdOn; - return this; - } - public Post setComments(List comments) { if (comments == null) { comments = new ArrayList<>(); @@ -60,6 +56,11 @@ public Post setComments(List comments) { return this; } + public Post setDetails(PostDetails details) { + this.details = details; + return this; + } + public void addComment(PostComment comment) { this.comments.add(comment); comment.setPost(this); @@ -70,6 +71,16 @@ public void removeComment(PostComment comment) { comment.setPost(null); } + public void addDetails(PostDetails details) { + this.details = details; + details.setPost(this); + } + + public void removeDetails() { + this.details.setPost(null); + this.details = null; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/entities/PostDetails.java b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/entities/PostDetails.java new file mode 100644 index 000000000..d6fe0dced --- /dev/null +++ b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/entities/PostDetails.java @@ -0,0 +1,67 @@ +package com.example.graphql.querydsl.entities; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.Objects; +import lombok.Getter; +import lombok.Setter; + +@Entity(name = "PostDetails") +@Table(name = "post_details") +@Getter +@Setter +public class PostDetails { + + @Id + private Long id; + + @Column(name = "created_on", nullable = false, updatable = false) + private LocalDateTime createdOn; + + @Column(name = "created_by", nullable = false, updatable = false) + private String createdBy; + + @OneToOne(fetch = FetchType.LAZY) + @MapsId + private Post post; + + public PostDetails setId(Long id) { + this.id = id; + return this; + } + + public PostDetails setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public PostDetails setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public PostDetails setPost(Post post) { + this.post = post; + return this; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + PostDetails other = (PostDetails) obj; + return Objects.equals(this.createdBy, other.createdBy); + } + + @Override + public int hashCode() { + return Objects.hash(this.createdBy); + } +} diff --git a/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/mapper/PostMapper.java b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/mapper/PostMapper.java index 0bb492bb2..048e37f79 100644 --- a/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/mapper/PostMapper.java +++ b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/mapper/PostMapper.java @@ -1,8 +1,11 @@ package com.example.graphql.querydsl.mapper; import com.example.graphql.querydsl.entities.Post; +import com.example.graphql.querydsl.entities.PostDetails; +import com.example.graphql.querydsl.model.request.CreatePostRequest; import com.example.graphql.querydsl.model.request.PostRequest; import com.example.graphql.querydsl.model.response.PostResponse; +import java.time.LocalDateTime; import java.util.List; import org.mapstruct.*; @@ -10,14 +13,20 @@ public interface PostMapper { @Mapping(target = "id", ignore = true) - @Mapping(target = "createdOn", expression = "java(java.time.LocalDateTime.now())") - Post toEntity(PostRequest postRequest); + Post toEntity(CreatePostRequest createPostRequest); @Mapping(target = "id", ignore = true) - @Mapping(target = "createdOn", ignore = true) + // @Mapping(target = "createdOn", ignore = true) void mapPostWithRequest(PostRequest postRequest, @MappingTarget Post post); + @Mapping(target = "createdOn", source = "details.createdOn") PostResponse toResponse(Post post); List toResponseList(List postList); + + @AfterMapping + default void setAfterMappingToPost(CreatePostRequest createPostRequest, @MappingTarget Post post) { + post.addDetails( + new PostDetails().setCreatedBy(createPostRequest.createdBy()).setCreatedOn(LocalDateTime.now())); + } } diff --git a/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/model/request/CreatePostRequest.java b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/model/request/CreatePostRequest.java new file mode 100644 index 000000000..d9bb0a8c3 --- /dev/null +++ b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/model/request/CreatePostRequest.java @@ -0,0 +1,9 @@ +package com.example.graphql.querydsl.model.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; + +public record CreatePostRequest( + @NotEmpty(message = "Title cannot be empty") String title, + @NotBlank(message = "Content cannot be blank") String content, + @NotBlank(message = "CreatedBy cannot be blank") String createdBy) {} diff --git a/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/services/PostService.java b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/services/PostService.java index f9edcbb37..aef279f4c 100644 --- a/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/services/PostService.java +++ b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/services/PostService.java @@ -4,6 +4,7 @@ import com.example.graphql.querydsl.exception.PostNotFoundException; import com.example.graphql.querydsl.mapper.PostMapper; import com.example.graphql.querydsl.model.query.FindPostsQuery; +import com.example.graphql.querydsl.model.request.CreatePostRequest; import com.example.graphql.querydsl.model.request.PostRequest; import com.example.graphql.querydsl.model.response.PagedResult; import com.example.graphql.querydsl.model.response.PostResponse; @@ -52,8 +53,8 @@ public Optional findPostById(Long id) { } @Transactional - public PostResponse savePost(PostRequest postRequest) { - Post post = postMapper.toEntity(postRequest); + public PostResponse savePost(CreatePostRequest createPostRequest) { + Post post = postMapper.toEntity(createPostRequest); Post savedPost = postRepository.save(post); return postMapper.toResponse(savedPost); } diff --git a/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/utils/AppConstants.java b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/utils/AppConstants.java index 8e05b476d..1a978b822 100644 --- a/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/utils/AppConstants.java +++ b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/utils/AppConstants.java @@ -1,5 +1,7 @@ package com.example.graphql.querydsl.utils; +import java.time.format.DateTimeFormatter; + public final class AppConstants { public static final String PROFILE_PROD = "prod"; public static final String PROFILE_NOT_PROD = "!" + PROFILE_PROD; @@ -9,4 +11,7 @@ public final class AppConstants { public static final String DEFAULT_PAGE_SIZE = "10"; public static final String DEFAULT_SORT_BY = "id"; public static final String DEFAULT_SORT_DIRECTION = "asc"; + + public static final DateTimeFormatter formatterWithMillis = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS"); } diff --git a/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/web/controllers/PostController.java b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/web/controllers/PostController.java index f31821be5..32e15b924 100644 --- a/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/web/controllers/PostController.java +++ b/graphql/boot-graphql-querydsl/src/main/java/com/example/graphql/querydsl/web/controllers/PostController.java @@ -2,6 +2,7 @@ import com.example.graphql.querydsl.exception.PostNotFoundException; import com.example.graphql.querydsl.model.query.FindPostsQuery; +import com.example.graphql.querydsl.model.request.CreatePostRequest; import com.example.graphql.querydsl.model.request.PostRequest; import com.example.graphql.querydsl.model.response.PagedResult; import com.example.graphql.querydsl.model.response.PostResponse; @@ -52,8 +53,8 @@ public ResponseEntity getPostById(@PathVariable Long id) { } @PostMapping - public ResponseEntity createPost(@RequestBody @Validated PostRequest postRequest) { - PostResponse response = postService.savePost(postRequest); + public ResponseEntity createPost(@RequestBody @Validated CreatePostRequest createPostRequest) { + PostResponse response = postService.savePost(createPostRequest); URI location = ServletUriComponentsBuilder.fromCurrentRequest() .path("/api/posts/{id}") .buildAndExpand(response.id()) diff --git a/graphql/boot-graphql-querydsl/src/main/resources/db/changelog/migration/03-create_tags_table.xml b/graphql/boot-graphql-querydsl/src/main/resources/db/changelog/migration/01-create_tags_table.xml similarity index 100% rename from graphql/boot-graphql-querydsl/src/main/resources/db/changelog/migration/03-create_tags_table.xml rename to graphql/boot-graphql-querydsl/src/main/resources/db/changelog/migration/01-create_tags_table.xml diff --git a/graphql/boot-graphql-querydsl/src/main/resources/db/changelog/migration/01-init.xml b/graphql/boot-graphql-querydsl/src/main/resources/db/changelog/migration/01-init.xml deleted file mode 100644 index 9d3b8b9b3..000000000 --- a/graphql/boot-graphql-querydsl/src/main/resources/db/changelog/migration/01-init.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/graphql/boot-graphql-querydsl/src/main/resources/db/changelog/migration/03-create_post_details_table.xml b/graphql/boot-graphql-querydsl/src/main/resources/db/changelog/migration/03-create_post_details_table.xml new file mode 100644 index 000000000..83a437eac --- /dev/null +++ b/graphql/boot-graphql-querydsl/src/main/resources/db/changelog/migration/03-create_post_details_table.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/graphql/boot-graphql-querydsl/src/test/java/com/example/graphql/querydsl/SchemaValidationTest.java b/graphql/boot-graphql-querydsl/src/test/java/com/example/graphql/querydsl/SchemaValidationTest.java index afdb9faa6..691dd085b 100644 --- a/graphql/boot-graphql-querydsl/src/test/java/com/example/graphql/querydsl/SchemaValidationTest.java +++ b/graphql/boot-graphql-querydsl/src/test/java/com/example/graphql/querydsl/SchemaValidationTest.java @@ -1,7 +1,12 @@ package com.example.graphql.querydsl; +import static org.assertj.core.api.Assertions.assertThat; + import com.example.graphql.querydsl.common.ContainersConfig; +import com.zaxxer.hikari.HikariDataSource; +import javax.sql.DataSource; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; @@ -9,6 +14,11 @@ @Import(ContainersConfig.class) class SchemaValidationTest { + @Autowired + private DataSource dataSource; + @Test - void validateJpaMappingsWithDbSchema() {} + void validateJpaMappingsWithDbSchema() { + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + } } diff --git a/graphql/boot-graphql-querydsl/src/test/java/com/example/graphql/querydsl/web/controllers/PostControllerIT.java b/graphql/boot-graphql-querydsl/src/test/java/com/example/graphql/querydsl/web/controllers/PostControllerIT.java index 74c5ca7a0..4050d2c13 100644 --- a/graphql/boot-graphql-querydsl/src/test/java/com/example/graphql/querydsl/web/controllers/PostControllerIT.java +++ b/graphql/boot-graphql-querydsl/src/test/java/com/example/graphql/querydsl/web/controllers/PostControllerIT.java @@ -13,6 +13,8 @@ import com.example.graphql.querydsl.common.AbstractIntegrationTest; import com.example.graphql.querydsl.entities.Post; +import com.example.graphql.querydsl.entities.PostDetails; +import com.example.graphql.querydsl.model.request.CreatePostRequest; import com.example.graphql.querydsl.model.request.PostRequest; import com.example.graphql.querydsl.repositories.PostRepository; import java.time.LocalDateTime; @@ -33,12 +35,12 @@ class PostControllerIT extends AbstractIntegrationTest { @BeforeEach void setUp() { - postRepository.deleteAllInBatch(); + postRepository.deleteAll(); postList = new ArrayList<>(); - postList.add(new Post().setTitle("First Post").setContent("First Content")); - postList.add(new Post().setTitle("Second Post").setContent("Second Content")); - postList.add(new Post().setTitle("Third Post").setContent("Third Content")); + postList.add(getPost("First Post", "First Content")); + postList.add(getPost("Second Post", "Second Content")); + postList.add(getPost("Third Post", "Third Content")); postList = postRepository.saveAll(postList); } @@ -68,12 +70,12 @@ void shouldFindPostById() throws Exception { .andExpect(jsonPath("$.id", is(post.getId()), Long.class)) .andExpect(jsonPath("$.title", is(post.getTitle()))) .andExpect(jsonPath("$.content", is(post.getContent()))) - .andExpect(jsonPath("$.createdOn", is(post.getCreatedOn()), LocalDateTime.class)); + .andExpect(jsonPath("$.createdOn", is("2023-12-31T10:35:45"))); } @Test void shouldCreateNewPost() throws Exception { - PostRequest postRequest = new PostRequest("New Post", "New Content"); + CreatePostRequest postRequest = new CreatePostRequest("New Post", "New Content", "Junit"); this.mockMvc .perform(post("/api/posts") .contentType(MediaType.APPLICATION_JSON) @@ -87,12 +89,12 @@ void shouldCreateNewPost() throws Exception { @Test void shouldReturn400WhenCreateNewPostWithoutTitleAndContent() throws Exception { - PostRequest postRequest = new PostRequest(null, null); + CreatePostRequest createPostRequest = new CreatePostRequest(null, null, null); this.mockMvc .perform(post("/api/posts") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(postRequest))) + .content(objectMapper.writeValueAsString(createPostRequest))) .andExpect(status().isBadRequest()) .andExpect(header().string("Content-Type", is("application/problem+json"))) .andExpect(jsonPath("$.type", is("about:blank"))) @@ -100,11 +102,13 @@ void shouldReturn400WhenCreateNewPostWithoutTitleAndContent() throws Exception { .andExpect(jsonPath("$.status", is(400))) .andExpect(jsonPath("$.detail", is("Invalid request content."))) .andExpect(jsonPath("$.instance", is("/api/posts"))) - .andExpect(jsonPath("$.violations", hasSize(2))) + .andExpect(jsonPath("$.violations", hasSize(3))) .andExpect(jsonPath("$.violations[0].field", is("content"))) .andExpect(jsonPath("$.violations[0].message", is("Content cannot be blank"))) - .andExpect(jsonPath("$.violations[1].field", is("title"))) - .andExpect(jsonPath("$.violations[1].message", is("Title cannot be empty"))) + .andExpect(jsonPath("$.violations[1].field", is("createdBy"))) + .andExpect(jsonPath("$.violations[1].message", is("CreatedBy cannot be blank"))) + .andExpect(jsonPath("$.violations[2].field", is("title"))) + .andExpect(jsonPath("$.violations[2].message", is("Title cannot be empty"))) .andReturn(); } @@ -133,6 +137,14 @@ void shouldDeletePost() throws Exception { .andExpect(jsonPath("$.id", is(post.getId()), Long.class)) .andExpect(jsonPath("$.title", is(post.getTitle()))) .andExpect(jsonPath("$.content", is(post.getContent()))) - .andExpect(jsonPath("$.createdOn", is(post.getCreatedOn()), LocalDateTime.class)); + .andExpect(jsonPath("$.createdOn", is("2023-12-31T10:35:45"))); + } + + private Post getPost(String title, String content) { + Post post = new Post().setTitle(title).setContent(content); + post.addDetails(new PostDetails() + .setCreatedOn(LocalDateTime.of(2023, 12, 31, 10, 35, 45, 99)) + .setCreatedBy("appUser")); + return post; } } diff --git a/graphql/boot-graphql-querydsl/src/test/java/com/example/graphql/querydsl/web/controllers/PostControllerTest.java b/graphql/boot-graphql-querydsl/src/test/java/com/example/graphql/querydsl/web/controllers/PostControllerTest.java index 1df47618a..0eeb97e7e 100644 --- a/graphql/boot-graphql-querydsl/src/test/java/com/example/graphql/querydsl/web/controllers/PostControllerTest.java +++ b/graphql/boot-graphql-querydsl/src/test/java/com/example/graphql/querydsl/web/controllers/PostControllerTest.java @@ -1,6 +1,7 @@ package com.example.graphql.querydsl.web.controllers; import static com.example.graphql.querydsl.utils.AppConstants.PROFILE_TEST; +import static com.example.graphql.querydsl.utils.AppConstants.formatterWithMillis; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.Matchers.hasSize; @@ -17,19 +18,22 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.example.graphql.querydsl.entities.Post; +import com.example.graphql.querydsl.entities.PostDetails; import com.example.graphql.querydsl.exception.PostNotFoundException; import com.example.graphql.querydsl.model.query.FindPostsQuery; +import com.example.graphql.querydsl.model.request.CreatePostRequest; import com.example.graphql.querydsl.model.request.PostRequest; import com.example.graphql.querydsl.model.response.PagedResult; import com.example.graphql.querydsl.model.response.PostResponse; import com.example.graphql.querydsl.services.PostService; import com.fasterxml.jackson.databind.ObjectMapper; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -56,14 +60,24 @@ class PostControllerTest { private List postList; - DateTimeFormatter formatterWithMillis = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS"); - @BeforeEach void setUp() { this.postList = new ArrayList<>(); - this.postList.add(new Post().setId(1L).setTitle("title 1").setContent("content 1")); - this.postList.add(new Post().setId(2L).setTitle("title 2").setContent("content 2")); - this.postList.add(new Post().setId(3L).setTitle("title 3").setContent("content 3")); + this.postList.add(new Post() + .setId(1L) + .setTitle("title 1") + .setContent("content 1") + .setDetails(new PostDetails().setCreatedOn(getCreatedOn()))); + this.postList.add(new Post() + .setId(2L) + .setTitle("title 2") + .setContent("content 2") + .setDetails(new PostDetails().setCreatedOn(getCreatedOn()))); + this.postList.add(new Post() + .setId(3L) + .setTitle("title 3") + .setContent("content 3") + .setDetails(new PostDetails().setCreatedOn(getCreatedOn()))); } @Test @@ -90,7 +104,7 @@ void shouldFetchAllPosts() throws Exception { @Test void shouldFindPostById() throws Exception { Long postId = 1L; - PostResponse post = new PostResponse(postId, "text 1", "content 1", LocalDateTime.now()); + PostResponse post = new PostResponse(postId, "text 1", "content 1", getCreatedOn()); given(postService.findPostById(postId)).willReturn(Optional.of(post)); this.mockMvc @@ -120,9 +134,9 @@ void shouldReturn404WhenFetchingNonExistingPost() throws Exception { @Test void shouldCreateNewPost() throws Exception { - PostResponse post = new PostResponse(1L, "some text", "some content", LocalDateTime.now()); - PostRequest postRequest = new PostRequest("some title", "some content"); - given(postService.savePost(any(PostRequest.class))).willReturn(post); + PostResponse post = new PostResponse(1L, "some text", "some content", getCreatedOn()); + CreatePostRequest postRequest = new CreatePostRequest("some title", "some content", "appUser"); + given(postService.savePost(any(CreatePostRequest.class))).willReturn(post); this.mockMvc .perform(post("/api/posts") @@ -139,12 +153,12 @@ void shouldCreateNewPost() throws Exception { @Test void shouldReturn400WhenCreateNewPostWithoutTitleAndContent() throws Exception { - PostRequest postRequest = new PostRequest(null, null); + CreatePostRequest createPostRequest = new CreatePostRequest(null, null, null); this.mockMvc .perform(post("/api/posts") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(postRequest))) + .content(objectMapper.writeValueAsString(createPostRequest))) .andExpect(status().isBadRequest()) .andExpect(header().string("Content-Type", is("application/problem+json"))) .andExpect(jsonPath("$.type", is("about:blank"))) @@ -152,55 +166,60 @@ void shouldReturn400WhenCreateNewPostWithoutTitleAndContent() throws Exception { .andExpect(jsonPath("$.status", is(400))) .andExpect(jsonPath("$.detail", is("Invalid request content."))) .andExpect(jsonPath("$.instance", is("/api/posts"))) - .andExpect(jsonPath("$.violations", hasSize(2))) + .andExpect(jsonPath("$.violations", hasSize(3))) .andExpect(jsonPath("$.violations[0].field", is("content"))) .andExpect(jsonPath("$.violations[0].message", is("Content cannot be blank"))) - .andExpect(jsonPath("$.violations[1].field", is("title"))) - .andExpect(jsonPath("$.violations[1].message", is("Title cannot be empty"))) + .andExpect(jsonPath("$.violations[1].field", is("createdBy"))) + .andExpect(jsonPath("$.violations[1].message", is("CreatedBy cannot be blank"))) + .andExpect(jsonPath("$.violations[2].field", is("title"))) + .andExpect(jsonPath("$.violations[2].message", is("Title cannot be empty"))) .andReturn(); } - @Test - void shouldUpdatePost() throws Exception { - Long postId = 1L; - PostResponse post = new PostResponse(postId, "Updated text", "some content", LocalDateTime.now()); - PostRequest postRequest = new PostRequest("Updated text", "some content"); - given(postService.updatePost(eq(postId), any(PostRequest.class))).willReturn(post); + @Nested + @DisplayName("update Method") + class Update { + @Test + void shouldUpdatePost() throws Exception { + Long postId = 1L; + PostResponse post = new PostResponse(postId, "Updated text", "some content", getCreatedOn()); + PostRequest postRequest = new PostRequest("Updated text", "some content"); + given(postService.updatePost(eq(postId), any(PostRequest.class))).willReturn(post); - this.mockMvc - .perform(put("/api/posts/{id}", postId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(postRequest))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.id", is(postId), Long.class)) - .andExpect(jsonPath("$.title", is(post.title()))) - .andExpect(jsonPath("$.content", is(post.content()))) - .andExpect( - jsonPath("$.createdOn", is(post.createdOn().format(formatterWithMillis)), LocalDateTime.class)); - } + mockMvc.perform(put("/api/posts/{id}", postId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(postRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(postId), Long.class)) + .andExpect(jsonPath("$.title", is(post.title()))) + .andExpect(jsonPath("$.content", is(post.content()))) + .andExpect(jsonPath( + "$.createdOn", is(post.createdOn().format(formatterWithMillis)), LocalDateTime.class)); + } - @Test - void shouldReturn404WhenUpdatingNonExistingPost() throws Exception { - Long postId = 1L; - PostRequest postRequest = new PostRequest("Updated text", "some content"); - given(postService.updatePost(eq(postId), any(PostRequest.class))).willThrow(new PostNotFoundException(postId)); + @Test + void shouldReturn404WhenUpdatingNonExistingPost() throws Exception { + Long postId = 1L; + PostRequest postRequest = new PostRequest("Updated text", "some content"); + given(postService.updatePost(eq(postId), any(PostRequest.class))) + .willThrow(new PostNotFoundException(postId)); - this.mockMvc - .perform(put("/api/posts/{id}", postId) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(postRequest))) - .andExpect(status().isNotFound()) - .andExpect(header().string("Content-Type", is(MediaType.APPLICATION_PROBLEM_JSON_VALUE))) - .andExpect(jsonPath("$.type", is("http://api.boot-graphql-querydsl.com/errors/not-found"))) - .andExpect(jsonPath("$.title", is("Not Found"))) - .andExpect(jsonPath("$.status", is(404))) - .andExpect(jsonPath("$.detail").value("Post with Id '%d' not found".formatted(postId))); + mockMvc.perform(put("/api/posts/{id}", postId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(postRequest))) + .andExpect(status().isNotFound()) + .andExpect(header().string("Content-Type", is(MediaType.APPLICATION_PROBLEM_JSON_VALUE))) + .andExpect(jsonPath("$.type", is("http://api.boot-graphql-querydsl.com/errors/not-found"))) + .andExpect(jsonPath("$.title", is("Not Found"))) + .andExpect(jsonPath("$.status", is(404))) + .andExpect(jsonPath("$.detail").value("Post with Id '%d' not found".formatted(postId))); + } } @Test void shouldDeletePost() throws Exception { Long postId = 1L; - PostResponse post = new PostResponse(postId, "Some text", "some content", LocalDateTime.now()); + PostResponse post = new PostResponse(postId, "Some text", "some content", getCreatedOn()); given(postService.findPostById(postId)).willReturn(Optional.of(post)); doNothing().when(postService).deletePostById(postId); @@ -229,7 +248,15 @@ void shouldReturn404WhenDeletingNonExistingPost() throws Exception { List getPostResponseList() { return postList.stream() - .map(post -> new PostResponse(post.getId(), post.getTitle(), post.getContent(), post.getCreatedOn())) + .map(post -> new PostResponse( + post.getId(), + post.getTitle(), + post.getContent(), + post.getDetails().getCreatedOn())) .toList(); } + + private LocalDateTime getCreatedOn() { + return LocalDateTime.of(2023, 12, 31, 10, 35, 45, 99); + } }