From 96808657d0d3d785510f4cddacc6c1f4dcecf835 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Wed, 12 Jun 2024 11:29:53 +0300 Subject: [PATCH 01/26] Add .env file for private data Move all credentials for DB from .properties to .env Fix application.properties code style --- dev.env | 3 +++ src/main/resources/application.properties | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 dev.env diff --git a/dev.env b/dev.env new file mode 100644 index 0000000..06a7567 --- /dev/null +++ b/dev.env @@ -0,0 +1,3 @@ +DB_URL= +DB_USER= +DB_PASSWORD= \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 35b376a..61c273e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,10 +1,10 @@ ## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties) -spring.datasource.url=jdbc:postgresql://localhost:5432/postgres_demo -spring.datasource.username= postgres -spring.datasource.password= +spring.datasource.url=jdbc:postgresql://${DB_URL}:5432/${DB_USER} +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} # The SQL dialect makes Hibernate generate better SQL for the chosen database -spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect # Hibernate ddl auto (create, create-drop, validate, update) -spring.jpa.hibernate.ddl-auto = update +spring.jpa.hibernate.ddl-auto=update From 9c1c5ce46d967dcce66b790c9f57aed95edff12e Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Wed, 12 Jun 2024 11:30:07 +0300 Subject: [PATCH 02/26] Add docker-compose.yml That done for automation of creation postgres container and volume --- docker-compose.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..74a47d1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.5' + +services: + postgres-automaton: + container_name: postgres_demo_application + image: postgres:latest + ports: + - "5432:5432" + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + PGDATA: /data/postgres + volumes: + - ./postgres-db:/data/postgres \ No newline at end of file From 39f138837a26efc1310547432a334b84011ede31 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Wed, 12 Jun 2024 13:42:42 +0300 Subject: [PATCH 03/26] Add new .env veriable DB_NAME Add default DB_URL, DB_NAME, DB_USER values --- dev.env | 5 +++-- src/main/resources/application.properties | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dev.env b/dev.env index 06a7567..8ff9534 100644 --- a/dev.env +++ b/dev.env @@ -1,3 +1,4 @@ -DB_URL= -DB_USER= +DB_URL=localhost +DB_NAME=postgres_db +DB_USER=postgres DB_PASSWORD= \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 61c273e..969a5b6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,5 @@ ## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties) -spring.datasource.url=jdbc:postgresql://${DB_URL}:5432/${DB_USER} +spring.datasource.url=jdbc:postgresql://${DB_URL}:5432/${DB_NAME} spring.datasource.username=${DB_USER} spring.datasource.password=${DB_PASSWORD} From 3725e6060e176d03043072902b0912f7fd7d8954 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Wed, 12 Jun 2024 13:58:24 +0300 Subject: [PATCH 04/26] Refactor pom.sql and add h2 dependency --- pom.xml | 62 ++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/pom.xml b/pom.xml index 2c23e76..a079727 100644 --- a/pom.xml +++ b/pom.xml @@ -24,30 +24,43 @@ 11 - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-validation - - - org.postgresql - postgresql - runtime - - - org.springframework.boot - spring-boot-starter-test - test - - + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.postgresql + postgresql + runtime + + + com.h2database + h2 + runtime + + + + junit + junit + test + + @@ -58,5 +71,4 @@ - From d7a806e566d5fff69d229193193aa2b4a418b35b Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Thu, 13 Jun 2024 15:13:11 +0300 Subject: [PATCH 05/26] Bug: QuestionControllerTest have problem with Autowiring --- .../controller/QuestionControllerTest.java | 45 +++++++++++++++++++ src/test/resources/application.properties | 18 ++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java create mode 100644 src/test/resources/application.properties diff --git a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java new file mode 100644 index 0000000..66acbe8 --- /dev/null +++ b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java @@ -0,0 +1,45 @@ +package com.example.postgresdemo.controller; + +import com.example.postgresdemo.model.Question; +import com.example.postgresdemo.repository.QuestionRepository; +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.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.web.context.WebApplicationContext; + +import java.util.List; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +//@SpringBootTest +//@WebMvcTest +@WebMvcTest(QuestionController.class) +public class QuestionControllerTest { +// private QuestionRepository questionRepository; + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private MockMvc mockMvc; + + @Test + public void testCreateMockMvc() { + assertNotNull(mockMvc); + } + + @Test + public void testGetQuestions() throws Exception { +// when(questionRepository.findAll()).thenReturn(List.of(new Question())); + + this.mockMvc + .perform(MockMvcRequestBuilders.get("/questions")) + .andExpect(MockMvcResultMatchers.status().isOk()); + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..68e3f46 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,18 @@ +spring.datasource.url=jdbc:h2:mem:test;MODE=PostgreSQL; +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} +# We add the MySQL Dialect so that it understands and generates the query based on MySQL +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect + +spring.h2.console.enabled=true +#spring.jpa.defer-datasource-initialization=true +spring.jpa.hibernate.ddl-auto=update +#spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl +#spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl +spring.jpa.properties.hibernate.format_sql=true +#spring.jpa.hibernate.naming-strategy=org.hibernate.cfg.ImprovedNamingStrategy +spring.jpa.properties.hibernate.show_sql=true + + +spring.sql.init.mode=always From 95049a3f602bda9f4006bb66306fd6327276b353 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Thu, 13 Jun 2024 18:14:36 +0300 Subject: [PATCH 06/26] Fix pom.xml junit dependency --- pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pom.xml b/pom.xml index a079727..60941b5 100644 --- a/pom.xml +++ b/pom.xml @@ -55,11 +55,6 @@ runtime - - junit - junit - test - From 5b82e535af50ff26a0535a2a72860152d1e6fdf8 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Thu, 13 Jun 2024 18:15:29 +0300 Subject: [PATCH 07/26] Add application.properties for test directory --- src/test/resources/application.properties | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 68e3f46..6d94c03 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -6,13 +6,9 @@ spring.datasource.password=${DB_PASSWORD} spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.h2.console.enabled=true -#spring.jpa.defer-datasource-initialization=true spring.jpa.hibernate.ddl-auto=update -#spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl -#spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl spring.jpa.properties.hibernate.format_sql=true -#spring.jpa.hibernate.naming-strategy=org.hibernate.cfg.ImprovedNamingStrategy -spring.jpa.properties.hibernate.show_sql=true +#spring.jpa.properties.hibernate.show_sql=true spring.sql.init.mode=always From 37174b5c1ddeedd6f762394e8ff278da359f5033 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Thu, 13 Jun 2024 18:15:54 +0300 Subject: [PATCH 08/26] Add first correct test --- .../controller/QuestionControllerTest.java | 76 ++++++++++++++++--- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java index 66acbe8..4fcbda2 100644 --- a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java +++ b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java @@ -2,44 +2,100 @@ import com.example.postgresdemo.model.Question; import com.example.postgresdemo.repository.QuestionRepository; + +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.web.context.WebApplicationContext; + +import java.util.Arrays; import java.util.List; -import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.when; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest -//@SpringBootTest -//@WebMvcTest -@WebMvcTest(QuestionController.class) +@AutoConfigureMockMvc public class QuestionControllerTest { -// private QuestionRepository questionRepository; + @Autowired + private QuestionRepository questionRepository; @Autowired private WebApplicationContext webApplicationContext; @Autowired private MockMvc mockMvc; + @Autowired + private QuestionController questionController; @Test public void testCreateMockMvc() { assertNotNull(mockMvc); } + private boolean fillQuestions(Integer number) { + try { + for (int i = 0; i < number; i++) { + Question question = new Question(); + question.setTitle("Question " + i); + question.setDescription("Description " + i); + questionRepository.save(question); + } + return true; + } catch (Exception e) { + return false; + } + } + + private boolean deleteQuestions() { + try { + questionRepository.deleteAll(); + return true; + } catch (Exception e) { + return false; + } + } + @Test public void testGetQuestions() throws Exception { -// when(questionRepository.findAll()).thenReturn(List.of(new Question())); + for (int assertionNumber = 0; assertionNumber < 100; assertionNumber++) { + int pageSize = 20; - this.mockMvc - .perform(MockMvcRequestBuilders.get("/questions")) - .andExpect(MockMvcResultMatchers.status().isOk()); + if (!fillQuestions(assertionNumber)) { + throw new Exception("Failed to fill questions"); + } + + mockMvc.perform(get("/questions") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.content.length()", Matchers.equalTo(Math.min(assertionNumber, pageSize)))) + .andExpect(MockMvcResultMatchers.jsonPath("$.totalElements", Matchers.equalTo(assertionNumber))) // Assert total elements + .andExpect(MockMvcResultMatchers.jsonPath("$.totalPages", Matchers.equalTo((int) Math.ceil(assertionNumber / (double) pageSize)))); // Assert total pages for 10 items per page + + if (!deleteQuestions()) { + throw new Exception("Failed to delete questions"); + } + } } + } From bab3c3228d3c220621a6b51eb55064c6bd14f53c Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Fri, 14 Jun 2024 11:04:23 +0300 Subject: [PATCH 09/26] Add QuestionController test --- .../controller/QuestionControllerTest.java | 137 ++++++++++++++++-- 1 file changed, 124 insertions(+), 13 deletions(-) diff --git a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java index 4fcbda2..3abf16e 100644 --- a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java +++ b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java @@ -5,28 +5,19 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.web.context.WebApplicationContext; -import java.util.Arrays; -import java.util.List; +import java.nio.CharBuffer; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.when; import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -75,8 +66,16 @@ private boolean deleteQuestions() { } } + private void deleteQuestionsWithExceptionOnFail() throws Exception { + if (!deleteQuestions()) { + throw new Exception("Failed to delete questions"); + } + } + @Test public void testGetQuestions() throws Exception { + deleteQuestionsWithExceptionOnFail(); + for (int assertionNumber = 0; assertionNumber < 100; assertionNumber++) { int pageSize = 20; @@ -92,10 +91,122 @@ public void testGetQuestions() throws Exception { .andExpect(MockMvcResultMatchers.jsonPath("$.totalElements", Matchers.equalTo(assertionNumber))) // Assert total elements .andExpect(MockMvcResultMatchers.jsonPath("$.totalPages", Matchers.equalTo((int) Math.ceil(assertionNumber / (double) pageSize)))); // Assert total pages for 10 items per page - if (!deleteQuestions()) { - throw new Exception("Failed to delete questions"); - } + deleteQuestionsWithExceptionOnFail(); } } + @Test + public void testCreateCorrectQuestion() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Question 1\",\n" + + " \"description\": \"Description 1\"\n" + + "}")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.title", Matchers.equalTo("Question 1"))) + .andExpect(MockMvcResultMatchers.jsonPath("$.description", Matchers.equalTo("Description 1"))); + + deleteQuestionsWithExceptionOnFail(); + } + + @Test + public void testCreateQuestionWithoutTitle() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"description\": \"Description\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + + deleteQuestionsWithExceptionOnFail(); + } + + @Test + public void testCreateQuestionWithTitleLesThenThreeChars() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Te\",\n" + + " \"description\": \"Description\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + + deleteQuestionsWithExceptionOnFail(); + } + + @Test + public void testCreateQuestionWithTitleMoreThenHundredChars() throws Exception { + int numberOfChars = 101; + String title = CharBuffer.allocate(numberOfChars).toString().replace('\0', 'T'); + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"" + title + "\",\n" + + " \"description\": \"Description\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + + deleteQuestionsWithExceptionOnFail(); + } + + @Test + public void testCreateQuestionWithoutDescription() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Question 1\"\n" + + "}")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.title", Matchers.equalTo("Question 1"))) + .andExpect(MockMvcResultMatchers.jsonPath("$.description", Matchers.equalTo(null))); + + deleteQuestionsWithExceptionOnFail(); + } + + @Test + public void testUpdateQuestion() throws Exception { + deleteQuestionsWithExceptionOnFail(); + fillQuestions(1); + long questionId = questionRepository.findAll().get(0).getId(); + + mockMvc.perform(MockMvcRequestBuilders.put("/questions/" + questionId) + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Edited Question 1\",\n" + + " \"description\": \"Edited Description 1\"\n" + + "}")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.title", Matchers.equalTo("Edited Question 1"))) + .andExpect(MockMvcResultMatchers.jsonPath("$.description", Matchers.equalTo("Edited Description 1"))); + + deleteQuestionsWithExceptionOnFail(); + } + + @Test + public void testUpdateQuestionWithNonExistingId() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.put("/questions/1") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Edited Question 1\",\n" + + " \"description\": \"Edited Description 1\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + } + + @Test + public void testDeleteQuestion() throws Exception { + deleteQuestionsWithExceptionOnFail(); + fillQuestions(1); + long questionId = questionRepository.findAll().get(0).getId(); + + mockMvc.perform(MockMvcRequestBuilders.delete("/questions/" + questionId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + deleteQuestionsWithExceptionOnFail(); + } } From 27d8a89778d04ee432a63e2f8d0aee1770098e27 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Fri, 14 Jun 2024 11:27:57 +0300 Subject: [PATCH 10/26] Fix antipatterns in tests --- .../controller/QuestionControllerTest.java | 105 +++++++----------- 1 file changed, 43 insertions(+), 62 deletions(-) diff --git a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java index 3abf16e..4ce1cac 100644 --- a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java +++ b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java @@ -4,6 +4,8 @@ import com.example.postgresdemo.repository.QuestionRepository; import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -38,61 +40,52 @@ public class QuestionControllerTest { @Autowired private QuestionController questionController; - @Test - public void testCreateMockMvc() { - assertNotNull(mockMvc); - } - - private boolean fillQuestions(Integer number) { - try { - for (int i = 0; i < number; i++) { - Question question = new Question(); - question.setTitle("Question " + i); - question.setDescription("Description " + i); - questionRepository.save(question); - } - return true; - } catch (Exception e) { - return false; + private void fillQuestions(Integer number) { + for (int i = 0; i < number; i++) { + Question question = new Question(); + question.setTitle("Question " + i); + question.setDescription("Description " + i); + questionRepository.save(question); } } - private boolean deleteQuestions() { - try { - questionRepository.deleteAll(); - return true; - } catch (Exception e) { - return false; - } - } - - private void deleteQuestionsWithExceptionOnFail() throws Exception { - if (!deleteQuestions()) { - throw new Exception("Failed to delete questions"); - } + @BeforeEach + @AfterEach + public void deleteQuestions() { + questionRepository.deleteAll(); } @Test - public void testGetQuestions() throws Exception { - deleteQuestionsWithExceptionOnFail(); + public void testGetQuestionsWithAmountLessThanPageSize() throws Exception { + int assertionNumber = 10; + int pageSize = 20; - for (int assertionNumber = 0; assertionNumber < 100; assertionNumber++) { - int pageSize = 20; + fillQuestions(assertionNumber); - if (!fillQuestions(assertionNumber)) { - throw new Exception("Failed to fill questions"); - } + mockMvc.perform(get("/questions") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.content.length()", Matchers.equalTo(assertionNumber))) + .andExpect(MockMvcResultMatchers.jsonPath("$.totalElements", Matchers.equalTo(assertionNumber))) + .andExpect(MockMvcResultMatchers.jsonPath("$.totalPages", Matchers.equalTo(1))); + } - mockMvc.perform(get("/questions") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.jsonPath("$.content.length()", Matchers.equalTo(Math.min(assertionNumber, pageSize)))) - .andExpect(MockMvcResultMatchers.jsonPath("$.totalElements", Matchers.equalTo(assertionNumber))) // Assert total elements - .andExpect(MockMvcResultMatchers.jsonPath("$.totalPages", Matchers.equalTo((int) Math.ceil(assertionNumber / (double) pageSize)))); // Assert total pages for 10 items per page + @Test + public void testGetQuestionsWithAmountMoreThanPageSize() throws Exception { + int assertionNumber = 30; + int pageSize = 20; + int totalPages = (int) Math.ceil(assertionNumber / (double) pageSize); - deleteQuestionsWithExceptionOnFail(); - } + fillQuestions(assertionNumber); + + mockMvc.perform(get("/questions") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.content.length()", Matchers.equalTo(pageSize))) + .andExpect(MockMvcResultMatchers.jsonPath("$.totalElements", Matchers.equalTo(assertionNumber))) + .andExpect(MockMvcResultMatchers.jsonPath("$.totalPages", Matchers.equalTo(totalPages))); } @Test @@ -107,8 +100,6 @@ public void testCreateCorrectQuestion() throws Exception { .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.jsonPath("$.title", Matchers.equalTo("Question 1"))) .andExpect(MockMvcResultMatchers.jsonPath("$.description", Matchers.equalTo("Description 1"))); - - deleteQuestionsWithExceptionOnFail(); } @Test @@ -119,8 +110,6 @@ public void testCreateQuestionWithoutTitle() throws Exception { " \"description\": \"Description\"\n" + "}")) .andExpect(status().is4xxClientError()); - - deleteQuestionsWithExceptionOnFail(); } @Test @@ -132,14 +121,13 @@ public void testCreateQuestionWithTitleLesThenThreeChars() throws Exception { " \"description\": \"Description\"\n" + "}")) .andExpect(status().is4xxClientError()); - - deleteQuestionsWithExceptionOnFail(); } @Test public void testCreateQuestionWithTitleMoreThenHundredChars() throws Exception { int numberOfChars = 101; String title = CharBuffer.allocate(numberOfChars).toString().replace('\0', 'T'); + mockMvc.perform(MockMvcRequestBuilders.post("/questions") .contentType(MediaType.APPLICATION_JSON) .content("{\n" + @@ -147,8 +135,6 @@ public void testCreateQuestionWithTitleMoreThenHundredChars() throws Exception { " \"description\": \"Description\"\n" + "}")) .andExpect(status().is4xxClientError()); - - deleteQuestionsWithExceptionOnFail(); } @Test @@ -162,13 +148,10 @@ public void testCreateQuestionWithoutDescription() throws Exception { .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.jsonPath("$.title", Matchers.equalTo("Question 1"))) .andExpect(MockMvcResultMatchers.jsonPath("$.description", Matchers.equalTo(null))); - - deleteQuestionsWithExceptionOnFail(); } @Test public void testUpdateQuestion() throws Exception { - deleteQuestionsWithExceptionOnFail(); fillQuestions(1); long questionId = questionRepository.findAll().get(0).getId(); @@ -182,13 +165,14 @@ public void testUpdateQuestion() throws Exception { .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.jsonPath("$.title", Matchers.equalTo("Edited Question 1"))) .andExpect(MockMvcResultMatchers.jsonPath("$.description", Matchers.equalTo("Edited Description 1"))); - - deleteQuestionsWithExceptionOnFail(); } @Test public void testUpdateQuestionWithNonExistingId() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.put("/questions/1") + fillQuestions(1); + long questionId = questionRepository.findAll().get(0).getId(); + + mockMvc.perform(MockMvcRequestBuilders.put("/questions/" + (questionId + 1)) .contentType(MediaType.APPLICATION_JSON) .content("{\n" + " \"title\": \"Edited Question 1\",\n" + @@ -199,14 +183,11 @@ public void testUpdateQuestionWithNonExistingId() throws Exception { @Test public void testDeleteQuestion() throws Exception { - deleteQuestionsWithExceptionOnFail(); fillQuestions(1); long questionId = questionRepository.findAll().get(0).getId(); mockMvc.perform(MockMvcRequestBuilders.delete("/questions/" + questionId) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); - - deleteQuestionsWithExceptionOnFail(); } } From ae1f093fd03a7d13eb847b72def06e6625cd102b Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Fri, 14 Jun 2024 13:43:48 +0300 Subject: [PATCH 11/26] Fix tests methods --- .../controller/QuestionControllerTest.java | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java index 4ce1cac..7e238d9 100644 --- a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java +++ b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java @@ -49,14 +49,13 @@ private void fillQuestions(Integer number) { } } - @BeforeEach @AfterEach - public void deleteQuestions() { + void deleteQuestions() { questionRepository.deleteAll(); } @Test - public void testGetQuestionsWithAmountLessThanPageSize() throws Exception { + void testGetQuestionsWithAmountLessThanPageSize() throws Exception { int assertionNumber = 10; int pageSize = 20; @@ -72,7 +71,7 @@ public void testGetQuestionsWithAmountLessThanPageSize() throws Exception { } @Test - public void testGetQuestionsWithAmountMoreThanPageSize() throws Exception { + void testGetQuestionsWithAmountMoreThanPageSize() throws Exception { int assertionNumber = 30; int pageSize = 20; int totalPages = (int) Math.ceil(assertionNumber / (double) pageSize); @@ -89,7 +88,7 @@ public void testGetQuestionsWithAmountMoreThanPageSize() throws Exception { } @Test - public void testCreateCorrectQuestion() throws Exception { + void testCreateCorrectQuestion() throws Exception { mockMvc.perform(MockMvcRequestBuilders.post("/questions") .contentType(MediaType.APPLICATION_JSON) .content("{\n" + @@ -103,7 +102,7 @@ public void testCreateCorrectQuestion() throws Exception { } @Test - public void testCreateQuestionWithoutTitle() throws Exception { + void testCreateQuestionWithoutTitle() throws Exception { mockMvc.perform(MockMvcRequestBuilders.post("/questions") .contentType(MediaType.APPLICATION_JSON) .content("{\n" + @@ -113,7 +112,7 @@ public void testCreateQuestionWithoutTitle() throws Exception { } @Test - public void testCreateQuestionWithTitleLesThenThreeChars() throws Exception { + void testCreateQuestionWithTitleLesThenThreeChars() throws Exception { mockMvc.perform(MockMvcRequestBuilders.post("/questions") .contentType(MediaType.APPLICATION_JSON) .content("{\n" + @@ -124,7 +123,7 @@ public void testCreateQuestionWithTitleLesThenThreeChars() throws Exception { } @Test - public void testCreateQuestionWithTitleMoreThenHundredChars() throws Exception { + void testCreateQuestionWithTitleMoreThenHundredChars() throws Exception { int numberOfChars = 101; String title = CharBuffer.allocate(numberOfChars).toString().replace('\0', 'T'); @@ -138,7 +137,7 @@ public void testCreateQuestionWithTitleMoreThenHundredChars() throws Exception { } @Test - public void testCreateQuestionWithoutDescription() throws Exception { + void testCreateQuestionWithoutDescription() throws Exception { mockMvc.perform(MockMvcRequestBuilders.post("/questions") .contentType(MediaType.APPLICATION_JSON) .content("{\n" + @@ -151,7 +150,7 @@ public void testCreateQuestionWithoutDescription() throws Exception { } @Test - public void testUpdateQuestion() throws Exception { + void testUpdateQuestion() throws Exception { fillQuestions(1); long questionId = questionRepository.findAll().get(0).getId(); @@ -168,7 +167,7 @@ public void testUpdateQuestion() throws Exception { } @Test - public void testUpdateQuestionWithNonExistingId() throws Exception { + void testUpdateQuestionWithNonExistingId() throws Exception { fillQuestions(1); long questionId = questionRepository.findAll().get(0).getId(); @@ -182,7 +181,7 @@ public void testUpdateQuestionWithNonExistingId() throws Exception { } @Test - public void testDeleteQuestion() throws Exception { + void testDeleteQuestion() throws Exception { fillQuestions(1); long questionId = questionRepository.findAll().get(0).getId(); From 3cd121997b38706018499e96590ea01e75653b3a Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Fri, 14 Jun 2024 16:46:32 +0300 Subject: [PATCH 12/26] Clean tests class --- .../controller/QuestionControllerTest.java | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java index 7e238d9..15229c4 100644 --- a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java +++ b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java @@ -5,7 +5,6 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -14,13 +13,10 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; -import org.springframework.web.context.WebApplicationContext; import java.nio.CharBuffer; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -32,22 +28,8 @@ public class QuestionControllerTest { @Autowired private QuestionRepository questionRepository; - @Autowired - private WebApplicationContext webApplicationContext; - @Autowired private MockMvc mockMvc; - @Autowired - private QuestionController questionController; - - private void fillQuestions(Integer number) { - for (int i = 0; i < number; i++) { - Question question = new Question(); - question.setTitle("Question " + i); - question.setDescription("Description " + i); - questionRepository.save(question); - } - } @AfterEach void deleteQuestions() { @@ -189,4 +171,13 @@ void testDeleteQuestion() throws Exception { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); } + + private void fillQuestions(Integer number) { + for (int i = 0; i < number; i++) { + Question question = new Question(); + question.setTitle("Question " + i); + question.setDescription("Description " + i); + questionRepository.save(question); + } + } } From c02e4e90ff14bd7d64455dbf75d638ad42c97d8d Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Fri, 14 Jun 2024 16:49:15 +0300 Subject: [PATCH 13/26] Add questionDTOs and move all business logic from Controller to service --- .../controller/QuestionController.java | 38 +++++------ .../model/QuestionRequestDTO.java | 28 +++++++++ .../model/QuestionResponseDTO.java | 22 +++++++ .../postgresdemo/service/QuestionService.java | 63 +++++++++++++++++++ .../controller/QuestionControllerTest.java | 12 ++-- 5 files changed, 131 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java create mode 100644 src/main/java/com/example/postgresdemo/model/QuestionResponseDTO.java create mode 100644 src/main/java/com/example/postgresdemo/service/QuestionService.java diff --git a/src/main/java/com/example/postgresdemo/controller/QuestionController.java b/src/main/java/com/example/postgresdemo/controller/QuestionController.java index c231819..d9d5902 100644 --- a/src/main/java/com/example/postgresdemo/controller/QuestionController.java +++ b/src/main/java/com/example/postgresdemo/controller/QuestionController.java @@ -1,50 +1,40 @@ package com.example.postgresdemo.controller; -import com.example.postgresdemo.exception.ResourceNotFoundException; -import com.example.postgresdemo.model.Question; -import com.example.postgresdemo.repository.QuestionRepository; +import com.example.postgresdemo.model.QuestionRequestDTO; +import com.example.postgresdemo.model.QuestionResponseDTO; +import com.example.postgresdemo.service.QuestionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; + import javax.validation.Valid; @RestController -public class QuestionController { +public class QuestionController { @Autowired - private QuestionRepository questionRepository; + private QuestionService questionService; @GetMapping("/questions") - public Page getQuestions(Pageable pageable) { - return questionRepository.findAll(pageable); + public Page getQuestions(Pageable pageable) { + return questionService.findAll(pageable); } - @PostMapping("/questions") - public Question createQuestion(@Valid @RequestBody Question question) { - return questionRepository.save(question); + public QuestionResponseDTO createQuestion(@Valid @RequestBody QuestionRequestDTO question) { + return questionService.create(question); } @PutMapping("/questions/{questionId}") - public Question updateQuestion(@PathVariable Long questionId, - @Valid @RequestBody Question questionRequest) { - return questionRepository.findById(questionId) - .map(question -> { - question.setTitle(questionRequest.getTitle()); - question.setDescription(questionRequest.getDescription()); - return questionRepository.save(question); - }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); + public QuestionResponseDTO updateQuestion(@PathVariable Long questionId, + @Valid @RequestBody QuestionRequestDTO questionRequest) { + return questionService.update(questionId, questionRequest); } - @DeleteMapping("/questions/{questionId}") public ResponseEntity deleteQuestion(@PathVariable Long questionId) { - return questionRepository.findById(questionId) - .map(question -> { - questionRepository.delete(question); - return ResponseEntity.ok().build(); - }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); + return questionService.delete(questionId); } } diff --git a/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java b/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java new file mode 100644 index 0000000..7c3bc75 --- /dev/null +++ b/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java @@ -0,0 +1,28 @@ +package com.example.postgresdemo.model; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +public class QuestionRequestDTO { + @NotBlank + @Size(min = 3, max = 100) + private String title; + + private String description; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/src/main/java/com/example/postgresdemo/model/QuestionResponseDTO.java b/src/main/java/com/example/postgresdemo/model/QuestionResponseDTO.java new file mode 100644 index 0000000..e2adf5f --- /dev/null +++ b/src/main/java/com/example/postgresdemo/model/QuestionResponseDTO.java @@ -0,0 +1,22 @@ +package com.example.postgresdemo.model; + +public class QuestionResponseDTO { + private Long id; + private String body; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } +} diff --git a/src/main/java/com/example/postgresdemo/service/QuestionService.java b/src/main/java/com/example/postgresdemo/service/QuestionService.java new file mode 100644 index 0000000..bd70046 --- /dev/null +++ b/src/main/java/com/example/postgresdemo/service/QuestionService.java @@ -0,0 +1,63 @@ +package com.example.postgresdemo.service; + +import com.example.postgresdemo.exception.ResourceNotFoundException; +import com.example.postgresdemo.model.Question; +import com.example.postgresdemo.model.QuestionRequestDTO; +import com.example.postgresdemo.model.QuestionResponseDTO; +import com.example.postgresdemo.repository.QuestionRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +@Service +public class QuestionService { + @Autowired + private QuestionRepository questionRepository; + + public Page findAll(Pageable pageable) { + return questionRepository.findAll(pageable) + .map(this::toQuestionResponseDTO); + } + + public QuestionResponseDTO create(QuestionRequestDTO questionRequest) { + Question question = toQuestion(questionRequest); + return toQuestionResponseDTO(questionRepository.save(question)); + } + + public QuestionResponseDTO update(Long questionId, QuestionRequestDTO questionRequest) { + Question question = toQuestion(questionRequest); + return questionRepository.findById(questionId) + .map(foundQuestion -> { + foundQuestion.setTitle(question.getTitle()); + foundQuestion.setDescription(question.getDescription()); + return toQuestionResponseDTO(questionRepository.save(foundQuestion)); + }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); + + } + + public ResponseEntity delete(Long questionId) { + return questionRepository.findById(questionId) + .map(question -> { + questionRepository.delete(question); + return ResponseEntity.ok().build(); + }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); + } + + private QuestionResponseDTO toQuestionResponseDTO(Question question) { + QuestionResponseDTO questionResponse = new QuestionResponseDTO(); + questionResponse.setId(question.getId()); + questionResponse.setBody(question.getTitle() + "\n" + question.getDescription()); + + + return questionResponse; + } + + private Question toQuestion(QuestionRequestDTO questionRequestDTO) { + Question question = new Question(); + question.setTitle(questionRequestDTO.getTitle()); + question.setDescription(questionRequestDTO.getDescription()); + return question; + } +} diff --git a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java index 15229c4..bd86ada 100644 --- a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java +++ b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java @@ -79,8 +79,7 @@ void testCreateCorrectQuestion() throws Exception { "}")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.jsonPath("$.title", Matchers.equalTo("Question 1"))) - .andExpect(MockMvcResultMatchers.jsonPath("$.description", Matchers.equalTo("Description 1"))); + .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Question 1\nDescription 1"))); } @Test @@ -88,7 +87,7 @@ void testCreateQuestionWithoutTitle() throws Exception { mockMvc.perform(MockMvcRequestBuilders.post("/questions") .contentType(MediaType.APPLICATION_JSON) .content("{\n" + - " \"description\": \"Description\"\n" + + " \"body\": \"\\nDescription\"\n" + "}")) .andExpect(status().is4xxClientError()); } @@ -127,9 +126,7 @@ void testCreateQuestionWithoutDescription() throws Exception { "}")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.jsonPath("$.title", Matchers.equalTo("Question 1"))) - .andExpect(MockMvcResultMatchers.jsonPath("$.description", Matchers.equalTo(null))); - } + .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Question 1\nnull")));} @Test void testUpdateQuestion() throws Exception { @@ -144,8 +141,7 @@ void testUpdateQuestion() throws Exception { "}")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.jsonPath("$.title", Matchers.equalTo("Edited Question 1"))) - .andExpect(MockMvcResultMatchers.jsonPath("$.description", Matchers.equalTo("Edited Description 1"))); + .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Edited Question 1\nEdited Description 1"))); } @Test From 140bfd938feb2a5f7ea731b7a6e713ea35a8a922 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Mon, 17 Jun 2024 12:29:14 +0300 Subject: [PATCH 14/26] Add constructor instead of injection --- .../com/example/postgresdemo/service/QuestionService.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/postgresdemo/service/QuestionService.java b/src/main/java/com/example/postgresdemo/service/QuestionService.java index bd70046..d63faf4 100644 --- a/src/main/java/com/example/postgresdemo/service/QuestionService.java +++ b/src/main/java/com/example/postgresdemo/service/QuestionService.java @@ -13,8 +13,11 @@ @Service public class QuestionService { - @Autowired - private QuestionRepository questionRepository; + private final QuestionRepository questionRepository; + + public QuestionService(QuestionRepository questionRepository) { + this.questionRepository = questionRepository; + } public Page findAll(Pageable pageable) { return questionRepository.findAll(pageable) From 991984956f1760f1949df19801d5cab976faa046 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Mon, 17 Jun 2024 12:29:35 +0300 Subject: [PATCH 15/26] Add String.join() --- .../java/com/example/postgresdemo/service/QuestionService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/postgresdemo/service/QuestionService.java b/src/main/java/com/example/postgresdemo/service/QuestionService.java index d63faf4..c3c60fd 100644 --- a/src/main/java/com/example/postgresdemo/service/QuestionService.java +++ b/src/main/java/com/example/postgresdemo/service/QuestionService.java @@ -51,7 +51,7 @@ public ResponseEntity delete(Long questionId) { private QuestionResponseDTO toQuestionResponseDTO(Question question) { QuestionResponseDTO questionResponse = new QuestionResponseDTO(); questionResponse.setId(question.getId()); - questionResponse.setBody(question.getTitle() + "\n" + question.getDescription()); + questionResponse.setBody(String.join("\n", question.getTitle(), question.getDescription())); return questionResponse; From 8c20fd6df3e67a3b716cf921d28a1db426cfe7d9 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Mon, 17 Jun 2024 12:31:11 +0300 Subject: [PATCH 16/26] Changes the signature of the delete function --- .../example/postgresdemo/controller/QuestionController.java | 6 +++--- .../com/example/postgresdemo/service/QuestionService.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/postgresdemo/controller/QuestionController.java b/src/main/java/com/example/postgresdemo/controller/QuestionController.java index d9d5902..815404a 100644 --- a/src/main/java/com/example/postgresdemo/controller/QuestionController.java +++ b/src/main/java/com/example/postgresdemo/controller/QuestionController.java @@ -29,12 +29,12 @@ public QuestionResponseDTO createQuestion(@Valid @RequestBody QuestionRequestDTO @PutMapping("/questions/{questionId}") public QuestionResponseDTO updateQuestion(@PathVariable Long questionId, - @Valid @RequestBody QuestionRequestDTO questionRequest) { + @Valid @RequestBody QuestionRequestDTO questionRequest) { return questionService.update(questionId, questionRequest); } @DeleteMapping("/questions/{questionId}") - public ResponseEntity deleteQuestion(@PathVariable Long questionId) { - return questionService.delete(questionId); + public void deleteQuestion(@PathVariable Long questionId) { + questionService.delete(questionId); } } diff --git a/src/main/java/com/example/postgresdemo/service/QuestionService.java b/src/main/java/com/example/postgresdemo/service/QuestionService.java index c3c60fd..c477b1d 100644 --- a/src/main/java/com/example/postgresdemo/service/QuestionService.java +++ b/src/main/java/com/example/postgresdemo/service/QuestionService.java @@ -40,11 +40,11 @@ public QuestionResponseDTO update(Long questionId, QuestionRequestDTO questionRe } - public ResponseEntity delete(Long questionId) { - return questionRepository.findById(questionId) + public void delete(Long questionId) { + questionRepository.findById(questionId) .map(question -> { questionRepository.delete(question); - return ResponseEntity.ok().build(); + return true; }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); } From 7c35883b61646dc34ccf374a597e6b2d1b52335d Mon Sep 17 00:00:00 2001 From: Zahar Date: Tue, 18 Jun 2024 08:03:11 +0000 Subject: [PATCH 17/26] Add all data --- .../controller/AnswerController.java | 132 +++---- .../controller/QuestionController.java | 80 ++-- .../example/postgresdemo/model/Answer.java | 106 +++--- .../postgresdemo/model/AuditModel.java | 84 ++-- .../example/postgresdemo/model/Question.java | 98 ++--- .../model/QuestionRequestDTO.java | 56 +-- .../model/QuestionResponseDTO.java | 44 +-- .../repository/QuestionRepository.java | 18 +- .../postgresdemo/service/QuestionService.java | 128 +++---- .../controller/QuestionControllerTest.java | 358 +++++++++--------- .../service/QuestionServiceTest.java | 124 ++++++ 11 files changed, 674 insertions(+), 554 deletions(-) create mode 100644 src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java diff --git a/src/main/java/com/example/postgresdemo/controller/AnswerController.java b/src/main/java/com/example/postgresdemo/controller/AnswerController.java index 7cfaa47..5af8f7b 100644 --- a/src/main/java/com/example/postgresdemo/controller/AnswerController.java +++ b/src/main/java/com/example/postgresdemo/controller/AnswerController.java @@ -1,66 +1,66 @@ -package com.example.postgresdemo.controller; - -import com.example.postgresdemo.exception.ResourceNotFoundException; -import com.example.postgresdemo.model.Answer; -import com.example.postgresdemo.repository.AnswerRepository; -import com.example.postgresdemo.repository.QuestionRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; -import java.util.List; - -@RestController -public class AnswerController { - - @Autowired - private AnswerRepository answerRepository; - - @Autowired - private QuestionRepository questionRepository; - - @GetMapping("/questions/{questionId}/answers") - public List getAnswersByQuestionId(@PathVariable Long questionId) { - return answerRepository.findByQuestionId(questionId); - } - - @PostMapping("/questions/{questionId}/answers") - public Answer addAnswer(@PathVariable Long questionId, - @Valid @RequestBody Answer answer) { - return questionRepository.findById(questionId) - .map(question -> { - answer.setQuestion(question); - return answerRepository.save(answer); - }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); - } - - @PutMapping("/questions/{questionId}/answers/{answerId}") - public Answer updateAnswer(@PathVariable Long questionId, - @PathVariable Long answerId, - @Valid @RequestBody Answer answerRequest) { - if(!questionRepository.existsById(questionId)) { - throw new ResourceNotFoundException("Question not found with id " + questionId); - } - - return answerRepository.findById(answerId) - .map(answer -> { - answer.setText(answerRequest.getText()); - return answerRepository.save(answer); - }).orElseThrow(() -> new ResourceNotFoundException("Answer not found with id " + answerId)); - } - - @DeleteMapping("/questions/{questionId}/answers/{answerId}") - public ResponseEntity deleteAnswer(@PathVariable Long questionId, - @PathVariable Long answerId) { - if(!questionRepository.existsById(questionId)) { - throw new ResourceNotFoundException("Question not found with id " + questionId); - } - - return answerRepository.findById(answerId) - .map(answer -> { - answerRepository.delete(answer); - return ResponseEntity.ok().build(); - }).orElseThrow(() -> new ResourceNotFoundException("Answer not found with id " + answerId)); - - } -} +package com.example.postgresdemo.controller; + +import com.example.postgresdemo.exception.ResourceNotFoundException; +import com.example.postgresdemo.model.Answer; +import com.example.postgresdemo.repository.AnswerRepository; +import com.example.postgresdemo.repository.QuestionRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import javax.validation.Valid; +import java.util.List; + +@RestController +public class AnswerController { + + @Autowired + private AnswerRepository answerRepository; + + @Autowired + private QuestionRepository questionRepository; + + @GetMapping("/questions/{questionId}/answers") + public List getAnswersByQuestionId(@PathVariable Long questionId) { + return answerRepository.findByQuestionId(questionId); + } + + @PostMapping("/questions/{questionId}/answers") + public Answer addAnswer(@PathVariable Long questionId, + @Valid @RequestBody Answer answer) { + return questionRepository.findById(questionId) + .map(question -> { + answer.setQuestion(question); + return answerRepository.save(answer); + }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); + } + + @PutMapping("/questions/{questionId}/answers/{answerId}") + public Answer updateAnswer(@PathVariable Long questionId, + @PathVariable Long answerId, + @Valid @RequestBody Answer answerRequest) { + if(!questionRepository.existsById(questionId)) { + throw new ResourceNotFoundException("Question not found with id " + questionId); + } + + return answerRepository.findById(answerId) + .map(answer -> { + answer.setText(answerRequest.getText()); + return answerRepository.save(answer); + }).orElseThrow(() -> new ResourceNotFoundException("Answer not found with id " + answerId)); + } + + @DeleteMapping("/questions/{questionId}/answers/{answerId}") + public ResponseEntity deleteAnswer(@PathVariable Long questionId, + @PathVariable Long answerId) { + if(!questionRepository.existsById(questionId)) { + throw new ResourceNotFoundException("Question not found with id " + questionId); + } + + return answerRepository.findById(answerId) + .map(answer -> { + answerRepository.delete(answer); + return ResponseEntity.ok().build(); + }).orElseThrow(() -> new ResourceNotFoundException("Answer not found with id " + answerId)); + + } +} diff --git a/src/main/java/com/example/postgresdemo/controller/QuestionController.java b/src/main/java/com/example/postgresdemo/controller/QuestionController.java index 815404a..7fd3d71 100644 --- a/src/main/java/com/example/postgresdemo/controller/QuestionController.java +++ b/src/main/java/com/example/postgresdemo/controller/QuestionController.java @@ -1,40 +1,40 @@ -package com.example.postgresdemo.controller; - -import com.example.postgresdemo.model.QuestionRequestDTO; -import com.example.postgresdemo.model.QuestionResponseDTO; -import com.example.postgresdemo.service.QuestionService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import javax.validation.Valid; - -@RestController - -public class QuestionController { - @Autowired - private QuestionService questionService; - - @GetMapping("/questions") - public Page getQuestions(Pageable pageable) { - return questionService.findAll(pageable); - } - - @PostMapping("/questions") - public QuestionResponseDTO createQuestion(@Valid @RequestBody QuestionRequestDTO question) { - return questionService.create(question); - } - - @PutMapping("/questions/{questionId}") - public QuestionResponseDTO updateQuestion(@PathVariable Long questionId, - @Valid @RequestBody QuestionRequestDTO questionRequest) { - return questionService.update(questionId, questionRequest); - } - - @DeleteMapping("/questions/{questionId}") - public void deleteQuestion(@PathVariable Long questionId) { - questionService.delete(questionId); - } -} +package com.example.postgresdemo.controller; + +import com.example.postgresdemo.model.QuestionRequestDTO; +import com.example.postgresdemo.model.QuestionResponseDTO; +import com.example.postgresdemo.service.QuestionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +@RestController + +public class QuestionController { + @Autowired + private QuestionService questionService; + + @GetMapping("/questions") + public Page getQuestions(Pageable pageable) { + return questionService.findAll(pageable); + } + + @PostMapping("/questions") + public QuestionResponseDTO createQuestion(@Valid @RequestBody QuestionRequestDTO question) { + return questionService.create(question); + } + + @PutMapping("/questions/{questionId}") + public QuestionResponseDTO updateQuestion(@PathVariable Long questionId, + @Valid @RequestBody QuestionRequestDTO questionRequest) { + return questionService.update(questionId, questionRequest); + } + + @DeleteMapping("/questions/{questionId}") + public void deleteQuestion(@PathVariable Long questionId) { + questionService.delete(questionId); + } +} diff --git a/src/main/java/com/example/postgresdemo/model/Answer.java b/src/main/java/com/example/postgresdemo/model/Answer.java index b5e48d0..94f8c8b 100644 --- a/src/main/java/com/example/postgresdemo/model/Answer.java +++ b/src/main/java/com/example/postgresdemo/model/Answer.java @@ -1,53 +1,53 @@ -package com.example.postgresdemo.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import org.hibernate.annotations.OnDelete; -import org.hibernate.annotations.OnDeleteAction; - -import javax.persistence.*; - -@Entity -@Table(name = "answers") -public class Answer extends AuditModel { - @Id - @GeneratedValue(generator = "answer_generator") - @SequenceGenerator( - name = "answer_generator", - sequenceName = "answer_sequence", - initialValue = 1000 - ) - private Long id; - - @Column(columnDefinition = "text") - private String text; - - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "question_id", nullable = false) - @OnDelete(action = OnDeleteAction.CASCADE) - @JsonIgnore - private Question question; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public Question getQuestion() { - return question; - } - - public void setQuestion(Question question) { - this.question = question; - } -} +package com.example.postgresdemo.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import javax.persistence.*; + +@Entity +@Table(name = "answers") +public class Answer extends AuditModel { + @Id + @GeneratedValue(generator = "answer_generator") + @SequenceGenerator( + name = "answer_generator", + sequenceName = "answer_sequence", + initialValue = 1000 + ) + private Long id; + + @Column(columnDefinition = "text") + private String text; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "question_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + private Question question; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public Question getQuestion() { + return question; + } + + public void setQuestion(Question question) { + this.question = question; + } +} diff --git a/src/main/java/com/example/postgresdemo/model/AuditModel.java b/src/main/java/com/example/postgresdemo/model/AuditModel.java index a61a3b8..06e17cb 100644 --- a/src/main/java/com/example/postgresdemo/model/AuditModel.java +++ b/src/main/java/com/example/postgresdemo/model/AuditModel.java @@ -1,43 +1,43 @@ -package com.example.postgresdemo.model; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import javax.persistence.*; -import java.io.Serializable; -import java.util.Date; - -@MappedSuperclass -@EntityListeners(AuditingEntityListener.class) -@JsonIgnoreProperties( - value = {"createdAt", "updatedAt"}, - allowGetters = true -) -public abstract class AuditModel implements Serializable { - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "created_at", nullable = false, updatable = false) - @CreatedDate - private Date createdAt; - - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "updated_at", nullable = false) - @LastModifiedDate - private Date updatedAt; - - public Date getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(Date createdAt) { - this.createdAt = createdAt; - } - - public Date getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(Date updatedAt) { - this.updatedAt = updatedAt; - } +package com.example.postgresdemo.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import javax.persistence.*; +import java.io.Serializable; +import java.util.Date; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@JsonIgnoreProperties( + value = {"createdAt", "updatedAt"}, + allowGetters = true +) +public abstract class AuditModel implements Serializable { + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_at", nullable = false, updatable = false) + @CreatedDate + private Date createdAt; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "updated_at", nullable = false) + @LastModifiedDate + private Date updatedAt; + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } } \ No newline at end of file diff --git a/src/main/java/com/example/postgresdemo/model/Question.java b/src/main/java/com/example/postgresdemo/model/Question.java index d16a459..2c27d26 100644 --- a/src/main/java/com/example/postgresdemo/model/Question.java +++ b/src/main/java/com/example/postgresdemo/model/Question.java @@ -1,49 +1,49 @@ -package com.example.postgresdemo.model; - -import javax.persistence.*; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.Size; - -@Entity -@Table(name = "questions") -public class Question extends AuditModel { - @Id - @GeneratedValue(generator = "question_generator") - @SequenceGenerator( - name = "question_generator", - sequenceName = "question_sequence", - initialValue = 1000 - ) - private Long id; - - @NotBlank - @Size(min = 3, max = 100) - private String title; - - @Column(columnDefinition = "text") - private String description; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } -} +package com.example.postgresdemo.model; + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Entity +@Table(name = "questions") +public class Question extends AuditModel { + @Id + @GeneratedValue(generator = "question_generator") + @SequenceGenerator( + name = "question_generator", + sequenceName = "question_sequence", + initialValue = 1000 + ) + private Long id; + + @NotBlank + @Size(min = 3, max = 100) + private String title; + + @Column(columnDefinition = "text") + private String description; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java b/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java index 7c3bc75..117b8bf 100644 --- a/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java +++ b/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java @@ -1,28 +1,28 @@ -package com.example.postgresdemo.model; - -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.Size; - -public class QuestionRequestDTO { - @NotBlank - @Size(min = 3, max = 100) - private String title; - - private String description; - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } -} +package com.example.postgresdemo.model; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +public class QuestionRequestDTO { + @NotBlank + @Size(min = 3, max = 100) + private String title; + + private String description; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/src/main/java/com/example/postgresdemo/model/QuestionResponseDTO.java b/src/main/java/com/example/postgresdemo/model/QuestionResponseDTO.java index e2adf5f..924ecbe 100644 --- a/src/main/java/com/example/postgresdemo/model/QuestionResponseDTO.java +++ b/src/main/java/com/example/postgresdemo/model/QuestionResponseDTO.java @@ -1,22 +1,22 @@ -package com.example.postgresdemo.model; - -public class QuestionResponseDTO { - private Long id; - private String body; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getBody() { - return body; - } - - public void setBody(String body) { - this.body = body; - } -} +package com.example.postgresdemo.model; + +public class QuestionResponseDTO { + private Long id; + private String body; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } +} diff --git a/src/main/java/com/example/postgresdemo/repository/QuestionRepository.java b/src/main/java/com/example/postgresdemo/repository/QuestionRepository.java index 290373d..c5d4d41 100644 --- a/src/main/java/com/example/postgresdemo/repository/QuestionRepository.java +++ b/src/main/java/com/example/postgresdemo/repository/QuestionRepository.java @@ -1,9 +1,9 @@ -package com.example.postgresdemo.repository; - -import com.example.postgresdemo.model.Question; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface QuestionRepository extends JpaRepository { -} +package com.example.postgresdemo.repository; + +import com.example.postgresdemo.model.Question; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface QuestionRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/postgresdemo/service/QuestionService.java b/src/main/java/com/example/postgresdemo/service/QuestionService.java index c477b1d..6d794b7 100644 --- a/src/main/java/com/example/postgresdemo/service/QuestionService.java +++ b/src/main/java/com/example/postgresdemo/service/QuestionService.java @@ -1,66 +1,62 @@ -package com.example.postgresdemo.service; - -import com.example.postgresdemo.exception.ResourceNotFoundException; -import com.example.postgresdemo.model.Question; -import com.example.postgresdemo.model.QuestionRequestDTO; -import com.example.postgresdemo.model.QuestionResponseDTO; -import com.example.postgresdemo.repository.QuestionRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; - -@Service -public class QuestionService { - private final QuestionRepository questionRepository; - - public QuestionService(QuestionRepository questionRepository) { - this.questionRepository = questionRepository; - } - - public Page findAll(Pageable pageable) { - return questionRepository.findAll(pageable) - .map(this::toQuestionResponseDTO); - } - - public QuestionResponseDTO create(QuestionRequestDTO questionRequest) { - Question question = toQuestion(questionRequest); - return toQuestionResponseDTO(questionRepository.save(question)); - } - - public QuestionResponseDTO update(Long questionId, QuestionRequestDTO questionRequest) { - Question question = toQuestion(questionRequest); - return questionRepository.findById(questionId) - .map(foundQuestion -> { - foundQuestion.setTitle(question.getTitle()); - foundQuestion.setDescription(question.getDescription()); - return toQuestionResponseDTO(questionRepository.save(foundQuestion)); - }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); - - } - - public void delete(Long questionId) { - questionRepository.findById(questionId) - .map(question -> { - questionRepository.delete(question); - return true; - }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); - } - - private QuestionResponseDTO toQuestionResponseDTO(Question question) { - QuestionResponseDTO questionResponse = new QuestionResponseDTO(); - questionResponse.setId(question.getId()); - questionResponse.setBody(String.join("\n", question.getTitle(), question.getDescription())); - - - return questionResponse; - } - - private Question toQuestion(QuestionRequestDTO questionRequestDTO) { - Question question = new Question(); - question.setTitle(questionRequestDTO.getTitle()); - question.setDescription(questionRequestDTO.getDescription()); - return question; - } -} +package com.example.postgresdemo.service; + +import com.example.postgresdemo.exception.ResourceNotFoundException; +import com.example.postgresdemo.model.Question; +import com.example.postgresdemo.model.QuestionRequestDTO; +import com.example.postgresdemo.model.QuestionResponseDTO; +import com.example.postgresdemo.repository.QuestionRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +public class QuestionService { + private final QuestionRepository questionRepository; + + public QuestionService(QuestionRepository questionRepository) { + this.questionRepository = questionRepository; + } + + public Page findAll(Pageable pageable) { + return questionRepository.findAll(pageable) + .map(this::toQuestionResponseDTO); + } + + public QuestionResponseDTO create(QuestionRequestDTO questionRequest) { + Question question = toQuestion(questionRequest); + return toQuestionResponseDTO(questionRepository.save(question)); + } + + public QuestionResponseDTO update(Long questionId, QuestionRequestDTO questionRequest) { + Question question = toQuestion(questionRequest); + return questionRepository.findById(questionId) + .map(foundQuestion -> { + foundQuestion.setTitle(question.getTitle()); + foundQuestion.setDescription(question.getDescription()); + return toQuestionResponseDTO(questionRepository.save(foundQuestion)); + }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); + + } + + public void delete(Long questionId) { + Question question = questionRepository.findById(questionId) + .orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); + questionRepository.delete(question); + } + + private QuestionResponseDTO toQuestionResponseDTO(Question question) { + QuestionResponseDTO questionResponse = new QuestionResponseDTO(); + questionResponse.setId(question.getId()); + questionResponse.setBody(String.join("\n", question.getTitle(), question.getDescription())); + + + return questionResponse; + } + + private Question toQuestion(QuestionRequestDTO questionRequestDTO) { + Question question = new Question(); + question.setTitle(questionRequestDTO.getTitle()); + question.setDescription(questionRequestDTO.getDescription()); + return question; + } +} diff --git a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java index bd86ada..31d8ddd 100644 --- a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java +++ b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java @@ -1,179 +1,179 @@ -package com.example.postgresdemo.controller; - -import com.example.postgresdemo.model.Question; -import com.example.postgresdemo.repository.QuestionRepository; - -import org.hamcrest.Matchers; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.result.MockMvcResultMatchers; - - -import java.nio.CharBuffer; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@SpringBootTest - -@AutoConfigureMockMvc -public class QuestionControllerTest { - @Autowired - private QuestionRepository questionRepository; - - @Autowired - private MockMvc mockMvc; - - @AfterEach - void deleteQuestions() { - questionRepository.deleteAll(); - } - - @Test - void testGetQuestionsWithAmountLessThanPageSize() throws Exception { - int assertionNumber = 10; - int pageSize = 20; - - fillQuestions(assertionNumber); - - mockMvc.perform(get("/questions") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.jsonPath("$.content.length()", Matchers.equalTo(assertionNumber))) - .andExpect(MockMvcResultMatchers.jsonPath("$.totalElements", Matchers.equalTo(assertionNumber))) - .andExpect(MockMvcResultMatchers.jsonPath("$.totalPages", Matchers.equalTo(1))); - } - - @Test - void testGetQuestionsWithAmountMoreThanPageSize() throws Exception { - int assertionNumber = 30; - int pageSize = 20; - int totalPages = (int) Math.ceil(assertionNumber / (double) pageSize); - - fillQuestions(assertionNumber); - - mockMvc.perform(get("/questions") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.jsonPath("$.content.length()", Matchers.equalTo(pageSize))) - .andExpect(MockMvcResultMatchers.jsonPath("$.totalElements", Matchers.equalTo(assertionNumber))) - .andExpect(MockMvcResultMatchers.jsonPath("$.totalPages", Matchers.equalTo(totalPages))); - } - - @Test - void testCreateCorrectQuestion() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.post("/questions") - .contentType(MediaType.APPLICATION_JSON) - .content("{\n" + - " \"title\": \"Question 1\",\n" + - " \"description\": \"Description 1\"\n" + - "}")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Question 1\nDescription 1"))); - } - - @Test - void testCreateQuestionWithoutTitle() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.post("/questions") - .contentType(MediaType.APPLICATION_JSON) - .content("{\n" + - " \"body\": \"\\nDescription\"\n" + - "}")) - .andExpect(status().is4xxClientError()); - } - - @Test - void testCreateQuestionWithTitleLesThenThreeChars() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.post("/questions") - .contentType(MediaType.APPLICATION_JSON) - .content("{\n" + - " \"title\": \"Te\",\n" + - " \"description\": \"Description\"\n" + - "}")) - .andExpect(status().is4xxClientError()); - } - - @Test - void testCreateQuestionWithTitleMoreThenHundredChars() throws Exception { - int numberOfChars = 101; - String title = CharBuffer.allocate(numberOfChars).toString().replace('\0', 'T'); - - mockMvc.perform(MockMvcRequestBuilders.post("/questions") - .contentType(MediaType.APPLICATION_JSON) - .content("{\n" + - " \"title\": \"" + title + "\",\n" + - " \"description\": \"Description\"\n" + - "}")) - .andExpect(status().is4xxClientError()); - } - - @Test - void testCreateQuestionWithoutDescription() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.post("/questions") - .contentType(MediaType.APPLICATION_JSON) - .content("{\n" + - " \"title\": \"Question 1\"\n" + - "}")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Question 1\nnull")));} - - @Test - void testUpdateQuestion() throws Exception { - fillQuestions(1); - long questionId = questionRepository.findAll().get(0).getId(); - - mockMvc.perform(MockMvcRequestBuilders.put("/questions/" + questionId) - .contentType(MediaType.APPLICATION_JSON) - .content("{\n" + - " \"title\": \"Edited Question 1\",\n" + - " \"description\": \"Edited Description 1\"\n" + - "}")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Edited Question 1\nEdited Description 1"))); - } - - @Test - void testUpdateQuestionWithNonExistingId() throws Exception { - fillQuestions(1); - long questionId = questionRepository.findAll().get(0).getId(); - - mockMvc.perform(MockMvcRequestBuilders.put("/questions/" + (questionId + 1)) - .contentType(MediaType.APPLICATION_JSON) - .content("{\n" + - " \"title\": \"Edited Question 1\",\n" + - " \"description\": \"Edited Description 1\"\n" + - "}")) - .andExpect(status().is4xxClientError()); - } - - @Test - void testDeleteQuestion() throws Exception { - fillQuestions(1); - long questionId = questionRepository.findAll().get(0).getId(); - - mockMvc.perform(MockMvcRequestBuilders.delete("/questions/" + questionId) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - } - - private void fillQuestions(Integer number) { - for (int i = 0; i < number; i++) { - Question question = new Question(); - question.setTitle("Question " + i); - question.setDescription("Description " + i); - questionRepository.save(question); - } - } -} +package com.example.postgresdemo.controller; + +import com.example.postgresdemo.model.Question; +import com.example.postgresdemo.repository.QuestionRepository; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + + +import java.nio.CharBuffer; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest + +@AutoConfigureMockMvc +public class QuestionControllerTest { + @Autowired + private QuestionRepository questionRepository; + + @Autowired + private MockMvc mockMvc; + + @AfterEach + void deleteQuestions() { + questionRepository.deleteAll(); + } + + @Test + void testGetQuestionsWithAmountLessThanPageSize() throws Exception { + int assertionNumber = 10; + int pageSize = 20; + + fillQuestions(assertionNumber); + + mockMvc.perform(get("/questions") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.content.length()", Matchers.equalTo(assertionNumber))) + .andExpect(MockMvcResultMatchers.jsonPath("$.totalElements", Matchers.equalTo(assertionNumber))) + .andExpect(MockMvcResultMatchers.jsonPath("$.totalPages", Matchers.equalTo(1))); + } + + @Test + void testGetQuestionsWithAmountMoreThanPageSize() throws Exception { + int assertionNumber = 30; + int pageSize = 20; + int totalPages = (int) Math.ceil(assertionNumber / (double) pageSize); + + fillQuestions(assertionNumber); + + mockMvc.perform(get("/questions") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.content.length()", Matchers.equalTo(pageSize))) + .andExpect(MockMvcResultMatchers.jsonPath("$.totalElements", Matchers.equalTo(assertionNumber))) + .andExpect(MockMvcResultMatchers.jsonPath("$.totalPages", Matchers.equalTo(totalPages))); + } + + @Test + void testCreateCorrectQuestion() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Question 1\",\n" + + " \"description\": \"Description 1\"\n" + + "}")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Question 1\nDescription 1"))); + } + + @Test + void testCreateQuestionWithoutTitle() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"body\": \"\\nDescription\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + } + + @Test + void testCreateQuestionWithTitleLesThenThreeChars() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Te\",\n" + + " \"description\": \"Description\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + } + + @Test + void testCreateQuestionWithTitleMoreThenHundredChars() throws Exception { + int numberOfChars = 101; + String title = CharBuffer.allocate(numberOfChars).toString().replace('\0', 'T'); + + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"" + title + "\",\n" + + " \"description\": \"Description\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + } + + @Test + void testCreateQuestionWithoutDescription() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Question 1\"\n" + + "}")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Question 1\nnull")));} + + @Test + void testUpdateQuestion() throws Exception { + fillQuestions(1); + long questionId = questionRepository.findAll().get(0).getId(); + + mockMvc.perform(MockMvcRequestBuilders.put("/questions/" + questionId) + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Edited Question 1\",\n" + + " \"description\": \"Edited Description 1\"\n" + + "}")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Edited Question 1\nEdited Description 1"))); + } + + @Test + void testUpdateQuestionWithNonExistingId() throws Exception { + fillQuestions(1); + long questionId = questionRepository.findAll().get(0).getId(); + + mockMvc.perform(MockMvcRequestBuilders.put("/questions/" + (questionId + 1)) + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Edited Question 1\",\n" + + " \"description\": \"Edited Description 1\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + } + + @Test + void testDeleteQuestion() throws Exception { + fillQuestions(1); + long questionId = questionRepository.findAll().get(0).getId(); + + mockMvc.perform(MockMvcRequestBuilders.delete("/questions/" + questionId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + private void fillQuestions(Integer number) { + for (int i = 0; i < number; i++) { + Question question = new Question(); + question.setTitle("Question " + i); + question.setDescription("Description " + i); + questionRepository.save(question); + } + } +} diff --git a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java new file mode 100644 index 0000000..a94840a --- /dev/null +++ b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java @@ -0,0 +1,124 @@ +package com.example.postgresdemo.service; + +import com.example.postgresdemo.exception.ResourceNotFoundException; +import com.example.postgresdemo.model.Question; +import com.example.postgresdemo.model.QuestionRequestDTO; +import com.example.postgresdemo.model.QuestionResponseDTO; +import com.example.postgresdemo.repository.QuestionRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.springframework.data.domain.Page; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.Arrays; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; + +@ExtendWith(MockitoExtension.class) +class QuestionServiceTest { + + @Mock + QuestionRepository questionRepository; + + @InjectMocks + QuestionService questionService; + + @Test + void testFindAll() { + Question question1 = new Question(); + question1.setId(1L); + question1.setTitle("Title1" ); + question1.setDescription("Description1"); + Question question2 = new Question(); + question2.setId(2L); + question2.setTitle("Title2"); + question2.setDescription("Description2"); + + Page page = new PageImpl<>(Arrays.asList(question1, question2)); + Mockito.when(questionRepository.findAll(any(PageRequest.class))).thenReturn(page); + + Page result = questionService.findAll(PageRequest.of(0, 10)); + + Mockito.verify(questionRepository).findAll(any(Pageable.class)); + Assertions.assertEquals(2, result.getTotalElements()); + Assertions.assertEquals("Title1\nDescription1", result.getContent().get(0).getBody()); + Assertions.assertEquals("Title2\nDescription2", result.getContent().get(1).getBody()); + } + + @Test + void testCreate() { + QuestionRequestDTO request = new QuestionRequestDTO(); + request.setTitle("Title"); + request.setDescription("Description"); + Question question = new Question(); + question.setId(1L); + question.setTitle("Title"); + question.setDescription("Description"); + Mockito.when(questionRepository.save(any(Question.class))).thenReturn(question); + + QuestionResponseDTO result = questionService.create(request); + + Mockito.verify(questionRepository).save(any(Question.class)); + Assertions.assertEquals("Title\nDescription", result.getBody()); + Assertions.assertEquals(1L, result.getId()); + } + + @Test + void testUpdateExistingQuestion() { + Long questionId = 123L; + QuestionRequestDTO request = new QuestionRequestDTO(); + request.setTitle("Title"); + request.setDescription("Description"); + Question questionToUpdate = new Question(); + questionToUpdate.setId(questionId); + questionToUpdate.setTitle("Old Title"); + questionToUpdate.setDescription("Old Description"); + + Mockito.when(questionRepository.findById(questionId)).thenReturn(Optional.of(questionToUpdate)); + Mockito.when(questionRepository.save(any(Question.class))).thenReturn(questionToUpdate); + + QuestionResponseDTO result = questionService.update(questionId, request); + + Mockito.verify(questionRepository).findById(questionId); + Mockito.verify(questionRepository).save(questionToUpdate); + Assertions.assertEquals("Title\nDescription", result.getBody()); + Assertions.assertEquals(questionId, result.getId()); + } + + @Test + void deleteExistingQuestion() { + Long questionId = 123L; + Question questionToDelete = new Question(); + + Mockito.when(questionRepository.findById(questionId)).thenReturn(Optional.of(questionToDelete)); + + questionService.delete(questionId); + + Mockito.verify(questionRepository).findById(questionId); + Mockito.verify(questionRepository).delete(questionToDelete); + Mockito.verifyNoMoreInteractions(questionRepository); + } + + @Test + void deleteNonexistentQuestion() { + Long questionId = 123L; + + Mockito.when(questionRepository.findById(questionId)).thenReturn(Optional.empty()); + + ResourceNotFoundException resourceNotFoundException = Assertions.assertThrows(ResourceNotFoundException.class, () -> { + questionService.delete(questionId); + }); + Assertions.assertEquals("Question not found with id " + questionId, resourceNotFoundException.getMessage()); + Mockito.verify(questionRepository).findById(questionId); + + Mockito.verifyNoMoreInteractions(questionRepository); + } +} \ No newline at end of file From 218a21b7694e0ec381b5e8d84c29a30250dbe69f Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Thu, 20 Jun 2024 10:35:25 +0300 Subject: [PATCH 18/26] Add fixes to the tests --- .../service/QuestionServiceTest.java | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java index a94840a..6e8f352 100644 --- a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java +++ b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java @@ -8,9 +8,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; +import org.mockito.*; import org.springframework.data.domain.Page; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.PageImpl; @@ -35,21 +33,23 @@ class QuestionServiceTest { void testFindAll() { Question question1 = new Question(); question1.setId(1L); - question1.setTitle("Title1" ); + question1.setTitle("Title1"); question1.setDescription("Description1"); Question question2 = new Question(); question2.setId(2L); question2.setTitle("Title2"); question2.setDescription("Description2"); - + Pageable pageable = PageRequest.of(0, 10); Page page = new PageImpl<>(Arrays.asList(question1, question2)); - Mockito.when(questionRepository.findAll(any(PageRequest.class))).thenReturn(page); + Mockito.when(questionRepository.findAll(pageable)).thenReturn(page); - Page result = questionService.findAll(PageRequest.of(0, 10)); + Page result = questionService.findAll(pageable); - Mockito.verify(questionRepository).findAll(any(Pageable.class)); + Mockito.verify(questionRepository).findAll(pageable); Assertions.assertEquals(2, result.getTotalElements()); + Assertions.assertEquals(1L, result.getContent().get(0).getId()); Assertions.assertEquals("Title1\nDescription1", result.getContent().get(0).getBody()); + Assertions.assertEquals(2L, result.getContent().get(1).getId()); Assertions.assertEquals("Title2\nDescription2", result.getContent().get(1).getBody()); } @@ -93,6 +93,24 @@ void testUpdateExistingQuestion() { Assertions.assertEquals(questionId, result.getId()); } + @Test + void testUpdateNotExistingQuestion() { + Long questionId = 123L; + QuestionRequestDTO request = new QuestionRequestDTO(); + request.setTitle("Title"); + request.setDescription("Description"); + + Mockito.when(questionRepository.findById(questionId)).thenReturn(Optional.empty()); + + ResourceNotFoundException resourceNotFoundException = Assertions.assertThrows(ResourceNotFoundException.class, () -> { + questionService.update(questionId, request); + }); + + Assertions.assertEquals("Question not found with id " + questionId, resourceNotFoundException.getMessage()); + Mockito.verify(questionRepository).findById(questionId); + Mockito.verifyNoMoreInteractions(questionRepository); + } + @Test void deleteExistingQuestion() { Long questionId = 123L; From 62f51483bcf81301b50e2f2a4cb8b51ebf0d0927 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Thu, 20 Jun 2024 10:36:45 +0300 Subject: [PATCH 19/26] Add Captor to the tests but i am not sure that it's correct --- .../postgresdemo/service/QuestionServiceTest.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java index 6e8f352..dc16133 100644 --- a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java +++ b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java @@ -82,14 +82,19 @@ void testUpdateExistingQuestion() { questionToUpdate.setTitle("Old Title"); questionToUpdate.setDescription("Old Description"); + ArgumentCaptor questionCaptor = ArgumentCaptor.forClass(Question.class); Mockito.when(questionRepository.findById(questionId)).thenReturn(Optional.of(questionToUpdate)); - Mockito.when(questionRepository.save(any(Question.class))).thenReturn(questionToUpdate); + Mockito.when(questionRepository.save(questionCaptor.capture())).thenReturn(questionToUpdate); QuestionResponseDTO result = questionService.update(questionId, request); Mockito.verify(questionRepository).findById(questionId); - Mockito.verify(questionRepository).save(questionToUpdate); - Assertions.assertEquals("Title\nDescription", result.getBody()); + Mockito.verify(questionRepository).save(questionCaptor.capture()); + + Question capturedQuestion = questionCaptor.getValue(); + + Assertions.assertEquals(request.getTitle(), capturedQuestion.getTitle()); + Assertions.assertEquals(request.getDescription(), capturedQuestion.getDescription()); Assertions.assertEquals(questionId, result.getId()); } From 4ac170fc3979dd2438dd3529d7388973c4fb045e Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Thu, 20 Jun 2024 12:46:24 +0300 Subject: [PATCH 20/26] Rewrite testCreate with capturedQuestion --- .../postgresdemo/service/QuestionServiceTest.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java index dc16133..4d3ecdd 100644 --- a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java +++ b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java @@ -29,6 +29,9 @@ class QuestionServiceTest { @InjectMocks QuestionService questionService; + @Captor + ArgumentCaptor questionCaptor; + @Test void testFindAll() { Question question1 = new Question(); @@ -62,12 +65,16 @@ void testCreate() { question.setId(1L); question.setTitle("Title"); question.setDescription("Description"); - Mockito.when(questionRepository.save(any(Question.class))).thenReturn(question); + Mockito.when(questionRepository.save(questionCaptor.capture())).thenReturn(question); QuestionResponseDTO result = questionService.create(request); - Mockito.verify(questionRepository).save(any(Question.class)); - Assertions.assertEquals("Title\nDescription", result.getBody()); + Mockito.verify(questionRepository).save(questionCaptor.capture()); + + Question capturedQuestion = questionCaptor.getValue(); + + Assertions.assertEquals(request.getTitle(), capturedQuestion.getTitle()); + Assertions.assertEquals(request.getDescription(), capturedQuestion.getDescription()); Assertions.assertEquals(1L, result.getId()); } @@ -82,7 +89,6 @@ void testUpdateExistingQuestion() { questionToUpdate.setTitle("Old Title"); questionToUpdate.setDescription("Old Description"); - ArgumentCaptor questionCaptor = ArgumentCaptor.forClass(Question.class); Mockito.when(questionRepository.findById(questionId)).thenReturn(Optional.of(questionToUpdate)); Mockito.when(questionRepository.save(questionCaptor.capture())).thenReturn(questionToUpdate); From 4fda6bca6d1e4e1e2bee2cde3ba914c1d6e6e906 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Thu, 20 Jun 2024 13:37:17 +0300 Subject: [PATCH 21/26] Add ParameterizedTest fpr createTest --- .../service/QuestionServiceTest.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java index 4d3ecdd..42d3bfa 100644 --- a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java +++ b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java @@ -8,6 +8,9 @@ import org.junit.jupiter.api.Assertions; 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.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.*; import org.springframework.data.domain.Page; import org.mockito.junit.jupiter.MockitoExtension; @@ -56,15 +59,20 @@ void testFindAll() { Assertions.assertEquals("Title2\nDescription2", result.getContent().get(1).getBody()); } - @Test - void testCreate() { + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", "Some Description"}) + void testCreateWithDescriptionVariations(String description) { QuestionRequestDTO request = new QuestionRequestDTO(); request.setTitle("Title"); - request.setDescription("Description"); + request.setDescription(description); + Question question = new Question(); question.setId(1L); question.setTitle("Title"); - question.setDescription("Description"); + question.setDescription(description); + + ArgumentCaptor questionCaptor = ArgumentCaptor.forClass(Question.class); Mockito.when(questionRepository.save(questionCaptor.capture())).thenReturn(question); QuestionResponseDTO result = questionService.create(request); From f173b88e9cc0ef94d7d2df6e9e2cf07ebff57f32 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Thu, 20 Jun 2024 14:09:51 +0300 Subject: [PATCH 22/26] Add MethodSource and checking of body --- .../service/QuestionServiceTest.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java index 42d3bfa..703ee8c 100644 --- a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java +++ b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java @@ -9,8 +9,8 @@ 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.NullSource; -import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.*; import org.springframework.data.domain.Page; import org.mockito.junit.jupiter.MockitoExtension; @@ -20,8 +20,7 @@ import java.util.Arrays; import java.util.Optional; - -import static org.mockito.ArgumentMatchers.any; +import java.util.stream.Stream; @ExtendWith(MockitoExtension.class) class QuestionServiceTest { @@ -60,9 +59,8 @@ void testFindAll() { } @ParameterizedTest - @NullSource - @ValueSource(strings = {"", "Some Description"}) - void testCreateWithDescriptionVariations(String description) { + @MethodSource("provideDescriptions") + void testCreateWithDescriptionVariations(String description, String expectedBody) { QuestionRequestDTO request = new QuestionRequestDTO(); request.setTitle("Title"); request.setDescription(description); @@ -84,6 +82,8 @@ void testCreateWithDescriptionVariations(String description) { Assertions.assertEquals(request.getTitle(), capturedQuestion.getTitle()); Assertions.assertEquals(request.getDescription(), capturedQuestion.getDescription()); Assertions.assertEquals(1L, result.getId()); + + Assertions.assertEquals(expectedBody, result.getBody()); } @Test @@ -158,4 +158,12 @@ void deleteNonexistentQuestion() { Mockito.verifyNoMoreInteractions(questionRepository); } + + private static Stream provideDescriptions() { + return Stream.of( + Arguments.of(null, "Title\nnull"), + Arguments.of("", "Title\n"), + Arguments.of("Some description", "Title\nSome description") + ); + } } \ No newline at end of file From cf1cb5c01662c3b903a3d87bf509912134573411 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Fri, 21 Jun 2024 10:35:30 +0300 Subject: [PATCH 23/26] Update entity of user --- .../com/example/postgresdemo/model/User.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/main/java/com/example/postgresdemo/model/User.java diff --git a/src/main/java/com/example/postgresdemo/model/User.java b/src/main/java/com/example/postgresdemo/model/User.java new file mode 100644 index 0000000..edb13e5 --- /dev/null +++ b/src/main/java/com/example/postgresdemo/model/User.java @@ -0,0 +1,60 @@ +package com.example.postgresdemo.model; + +import javax.persistence.*; +import javax.validation.constraints.Size; + +@Entity +@Table(name = "users") +public class User { + @Id + @GeneratedValue + private Long id; + @Size(min = 1, max = 50) + @Column(columnDefinition = "firstname") + private String firstname; + @Size(min = 1, max = 50) + @Column(columnDefinition = "lastname") + private String lastname; + @Column(columnDefinition = "password") + private String password; + + public User() {} + + public User(Long id, String firstname, String lastname) { + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} From 048c7e6d349869d289cfd10ce2073612ae7a4265 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Fri, 21 Jun 2024 10:36:23 +0300 Subject: [PATCH 24/26] Add user field to question and refactor service and controller --- .../controller/QuestionController.java | 1 - .../example/postgresdemo/model/Question.java | 18 +++++++++++++++++ .../model/QuestionRequestDTO.java | 12 +++++++++++ .../postgresdemo/service/QuestionService.java | 20 ++++++++++++++----- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/postgresdemo/controller/QuestionController.java b/src/main/java/com/example/postgresdemo/controller/QuestionController.java index 7fd3d71..90b0884 100644 --- a/src/main/java/com/example/postgresdemo/controller/QuestionController.java +++ b/src/main/java/com/example/postgresdemo/controller/QuestionController.java @@ -12,7 +12,6 @@ import javax.validation.Valid; @RestController - public class QuestionController { @Autowired private QuestionService questionService; diff --git a/src/main/java/com/example/postgresdemo/model/Question.java b/src/main/java/com/example/postgresdemo/model/Question.java index 2c27d26..85193a8 100644 --- a/src/main/java/com/example/postgresdemo/model/Question.java +++ b/src/main/java/com/example/postgresdemo/model/Question.java @@ -1,5 +1,9 @@ package com.example.postgresdemo.model; +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + import javax.persistence.*; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; @@ -23,6 +27,12 @@ public class Question extends AuditModel { @Column(columnDefinition = "text") private String description; + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + private User user; + public Long getId() { return id; } @@ -46,4 +56,12 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } } diff --git a/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java b/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java index 117b8bf..80a213a 100644 --- a/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java +++ b/src/main/java/com/example/postgresdemo/model/QuestionRequestDTO.java @@ -1,6 +1,7 @@ package com.example.postgresdemo.model; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; public class QuestionRequestDTO { @@ -10,6 +11,9 @@ public class QuestionRequestDTO { private String description; + @NotNull + private Long authorId; + public String getTitle() { return title; } @@ -25,4 +29,12 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } + + public Long getAuthorId() { + return authorId; + } + + public void setAuthorId(Long authorId) { + this.authorId = authorId; + } } diff --git a/src/main/java/com/example/postgresdemo/service/QuestionService.java b/src/main/java/com/example/postgresdemo/service/QuestionService.java index 6d794b7..2352cdc 100644 --- a/src/main/java/com/example/postgresdemo/service/QuestionService.java +++ b/src/main/java/com/example/postgresdemo/service/QuestionService.java @@ -4,7 +4,9 @@ import com.example.postgresdemo.model.Question; import com.example.postgresdemo.model.QuestionRequestDTO; import com.example.postgresdemo.model.QuestionResponseDTO; +import com.example.postgresdemo.model.User; import com.example.postgresdemo.repository.QuestionRepository; +import com.example.postgresdemo.repository.UserRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -12,9 +14,12 @@ @Service public class QuestionService { private final QuestionRepository questionRepository; + private final UserRepository userRepository; - public QuestionService(QuestionRepository questionRepository) { + + public QuestionService(QuestionRepository questionRepository, UserRepository userRepository) { this.questionRepository = questionRepository; + this.userRepository = userRepository; } public Page findAll(Pageable pageable) { @@ -23,19 +28,23 @@ public Page findAll(Pageable pageable) { } public QuestionResponseDTO create(QuestionRequestDTO questionRequest) { - Question question = toQuestion(questionRequest); + User user = userRepository.findById(questionRequest.getAuthorId()) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id " + questionRequest.getAuthorId())); + Question question = toQuestion(questionRequest, user); return toQuestionResponseDTO(questionRepository.save(question)); } public QuestionResponseDTO update(Long questionId, QuestionRequestDTO questionRequest) { - Question question = toQuestion(questionRequest); + User user = userRepository.findById(questionRequest.getAuthorId()) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id " + questionRequest.getAuthorId())); + Question question = toQuestion(questionRequest, user); return questionRepository.findById(questionId) .map(foundQuestion -> { foundQuestion.setTitle(question.getTitle()); foundQuestion.setDescription(question.getDescription()); + foundQuestion.setUser(user); return toQuestionResponseDTO(questionRepository.save(foundQuestion)); }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId)); - } public void delete(Long questionId) { @@ -53,10 +62,11 @@ private QuestionResponseDTO toQuestionResponseDTO(Question question) { return questionResponse; } - private Question toQuestion(QuestionRequestDTO questionRequestDTO) { + private Question toQuestion(QuestionRequestDTO questionRequestDTO, User user) { Question question = new Question(); question.setTitle(questionRequestDTO.getTitle()); question.setDescription(questionRequestDTO.getDescription()); + question.setUser(user); return question; } } From f88ff4a7e88e6db26ff5c50893203b11f408ddfd Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Fri, 21 Jun 2024 10:37:15 +0300 Subject: [PATCH 25/26] Add user's service and controller --- .../repository/UserRepository.java | 9 ++++ .../postgresdemo/service/UserService.java | 41 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/main/java/com/example/postgresdemo/repository/UserRepository.java create mode 100644 src/main/java/com/example/postgresdemo/service/UserService.java diff --git a/src/main/java/com/example/postgresdemo/repository/UserRepository.java b/src/main/java/com/example/postgresdemo/repository/UserRepository.java new file mode 100644 index 0000000..c04c909 --- /dev/null +++ b/src/main/java/com/example/postgresdemo/repository/UserRepository.java @@ -0,0 +1,9 @@ +package com.example.postgresdemo.repository; + +import com.example.postgresdemo.model.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/example/postgresdemo/service/UserService.java b/src/main/java/com/example/postgresdemo/service/UserService.java new file mode 100644 index 0000000..a5e8819 --- /dev/null +++ b/src/main/java/com/example/postgresdemo/service/UserService.java @@ -0,0 +1,41 @@ +package com.example.postgresdemo.service; + +import com.example.postgresdemo.exception.ResourceNotFoundException; +import com.example.postgresdemo.model.User; +import com.example.postgresdemo.repository.UserRepository; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserService { + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public List findAll() { + return userRepository.findAll(); + } + + public User create(User user) { + return userRepository.save(user); + } + + public User update(Long userId, User userDetails) { + return userRepository.findById(userId) + .map(user -> { + user.setFirstname(userDetails.getFirstname()); + user.setLastname(userDetails.getLastname()); + return userRepository.save(user); + }).orElseThrow(() -> new ResourceNotFoundException("User not found with id " + userId)); + } + + + public void delete(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id " + userId)); + userRepository.delete(user); + } +} From edb355f2e425071e17de6fdd10aa9f2f1efbe5e6 Mon Sep 17 00:00:00 2001 From: "zahar.kalosha" Date: Fri, 21 Jun 2024 14:50:30 +0300 Subject: [PATCH 26/26] Add user's in controllers. --- .../com/example/postgresdemo/model/User.java | 3 - .../controller/QuestionControllerTest.java | 62 +++++++++++++++---- .../service/QuestionServiceTest.java | 41 +++++++++++- 3 files changed, 87 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/example/postgresdemo/model/User.java b/src/main/java/com/example/postgresdemo/model/User.java index edb13e5..d2cb3b9 100644 --- a/src/main/java/com/example/postgresdemo/model/User.java +++ b/src/main/java/com/example/postgresdemo/model/User.java @@ -10,12 +10,9 @@ public class User { @GeneratedValue private Long id; @Size(min = 1, max = 50) - @Column(columnDefinition = "firstname") private String firstname; @Size(min = 1, max = 50) - @Column(columnDefinition = "lastname") private String lastname; - @Column(columnDefinition = "password") private String password; public User() {} diff --git a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java index 31d8ddd..61aeec9 100644 --- a/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java +++ b/src/test/java/com/example/postgresdemo/controller/QuestionControllerTest.java @@ -1,10 +1,13 @@ package com.example.postgresdemo.controller; import com.example.postgresdemo.model.Question; +import com.example.postgresdemo.model.User; import com.example.postgresdemo.repository.QuestionRepository; +import com.example.postgresdemo.repository.UserRepository; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -14,7 +17,6 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; - import java.nio.CharBuffer; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -22,18 +24,32 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest - @AutoConfigureMockMvc public class QuestionControllerTest { @Autowired private QuestionRepository questionRepository; + @Autowired + private UserRepository userRepository; + @Autowired private MockMvc mockMvc; + private Long userId; + + @BeforeEach + void setup() { + User user = new User(); + user.setFirstname("John"); + user.setLastname("Doe"); + userRepository.save(user); + userId = user.getId(); + } + @AfterEach - void deleteQuestions() { + void deleteAll() { questionRepository.deleteAll(); + userRepository.deleteAll(); } @Test @@ -75,7 +91,8 @@ void testCreateCorrectQuestion() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content("{\n" + " \"title\": \"Question 1\",\n" + - " \"description\": \"Description 1\"\n" + + " \"description\": \"Description 1\",\n" + + " \"authorId\": \"" + userId + "\"\n" + "}")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -87,24 +104,26 @@ void testCreateQuestionWithoutTitle() throws Exception { mockMvc.perform(MockMvcRequestBuilders.post("/questions") .contentType(MediaType.APPLICATION_JSON) .content("{\n" + - " \"body\": \"\\nDescription\"\n" + + " \"description\": \"Description\",\n" + + " \"authorId\": \"" + userId + "\"\n" + "}")) .andExpect(status().is4xxClientError()); } @Test - void testCreateQuestionWithTitleLesThenThreeChars() throws Exception { + void testCreateQuestionWithTitleLessThanThreeChars() throws Exception { mockMvc.perform(MockMvcRequestBuilders.post("/questions") .contentType(MediaType.APPLICATION_JSON) .content("{\n" + " \"title\": \"Te\",\n" + - " \"description\": \"Description\"\n" + + " \"description\": \"Description\",\n" + + " \"authorId\": \"" + userId + "\"\n" + "}")) .andExpect(status().is4xxClientError()); } @Test - void testCreateQuestionWithTitleMoreThenHundredChars() throws Exception { + void testCreateQuestionWithTitleMoreThanHundredChars() throws Exception { int numberOfChars = 101; String title = CharBuffer.allocate(numberOfChars).toString().replace('\0', 'T'); @@ -112,7 +131,8 @@ void testCreateQuestionWithTitleMoreThenHundredChars() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content("{\n" + " \"title\": \"" + title + "\",\n" + - " \"description\": \"Description\"\n" + + " \"description\": \"Description\",\n" + + " \"authorId\": \"" + userId + "\"\n" + "}")) .andExpect(status().is4xxClientError()); } @@ -122,11 +142,24 @@ void testCreateQuestionWithoutDescription() throws Exception { mockMvc.perform(MockMvcRequestBuilders.post("/questions") .contentType(MediaType.APPLICATION_JSON) .content("{\n" + - " \"title\": \"Question 1\"\n" + + " \"title\": \"Question 1\",\n" + + " \"authorId\": \"" + userId + "\"\n" + "}")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Question 1\nnull")));} + .andExpect(MockMvcResultMatchers.jsonPath("$.body", Matchers.equalTo("Question 1\nnull"))); + } + + @Test + void testCreateQuestionWithoutAuthor() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/questions") + .contentType(MediaType.APPLICATION_JSON) + .content("{\n" + + " \"title\": \"Question 1\",\n" + + " \"description\": \"Description 1\"\n" + + "}")) + .andExpect(status().is4xxClientError()); + } @Test void testUpdateQuestion() throws Exception { @@ -137,7 +170,8 @@ void testUpdateQuestion() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content("{\n" + " \"title\": \"Edited Question 1\",\n" + - " \"description\": \"Edited Description 1\"\n" + + " \"description\": \"Edited Description 1\",\n" + + " \"authorId\": \"" + userId + "\"\n" + "}")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -153,7 +187,8 @@ void testUpdateQuestionWithNonExistingId() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content("{\n" + " \"title\": \"Edited Question 1\",\n" + - " \"description\": \"Edited Description 1\"\n" + + " \"description\": \"Edited Description 1\",\n" + + " \"authorId\": \"" + userId + "\"\n" + "}")) .andExpect(status().is4xxClientError()); } @@ -173,6 +208,7 @@ private void fillQuestions(Integer number) { Question question = new Question(); question.setTitle("Question " + i); question.setDescription("Description " + i); + question.setUser(userRepository.findById(userId).orElseThrow()); questionRepository.save(question); } } diff --git a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java index 703ee8c..00c0b77 100644 --- a/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java +++ b/src/test/java/com/example/postgresdemo/service/QuestionServiceTest.java @@ -4,7 +4,9 @@ import com.example.postgresdemo.model.Question; import com.example.postgresdemo.model.QuestionRequestDTO; import com.example.postgresdemo.model.QuestionResponseDTO; +import com.example.postgresdemo.model.User; import com.example.postgresdemo.repository.QuestionRepository; +import com.example.postgresdemo.repository.UserRepository; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,6 +30,9 @@ class QuestionServiceTest { @Mock QuestionRepository questionRepository; + @Mock + UserRepository userRepository; + @InjectMocks QuestionService questionService; @@ -61,47 +66,67 @@ void testFindAll() { @ParameterizedTest @MethodSource("provideDescriptions") void testCreateWithDescriptionVariations(String description, String expectedBody) { + Long authorId = 1L; QuestionRequestDTO request = new QuestionRequestDTO(); request.setTitle("Title"); request.setDescription(description); + request.setAuthorId(authorId); + + User user = new User(); + user.setId(authorId); + user.setFirstname("John"); + user.setLastname("Doe"); Question question = new Question(); question.setId(1L); question.setTitle("Title"); question.setDescription(description); + question.setUser(user); - ArgumentCaptor questionCaptor = ArgumentCaptor.forClass(Question.class); + Mockito.when(userRepository.findById(authorId)).thenReturn(Optional.of(user)); Mockito.when(questionRepository.save(questionCaptor.capture())).thenReturn(question); QuestionResponseDTO result = questionService.create(request); + Mockito.verify(userRepository).findById(authorId); Mockito.verify(questionRepository).save(questionCaptor.capture()); Question capturedQuestion = questionCaptor.getValue(); Assertions.assertEquals(request.getTitle(), capturedQuestion.getTitle()); Assertions.assertEquals(request.getDescription(), capturedQuestion.getDescription()); + Assertions.assertEquals(user, capturedQuestion.getUser()); Assertions.assertEquals(1L, result.getId()); - Assertions.assertEquals(expectedBody, result.getBody()); } @Test void testUpdateExistingQuestion() { Long questionId = 123L; + Long authorId = 1L; QuestionRequestDTO request = new QuestionRequestDTO(); request.setTitle("Title"); request.setDescription("Description"); + request.setAuthorId(authorId); + + User user = new User(); + user.setId(authorId); + user.setFirstname("John"); + user.setLastname("Doe"); + Question questionToUpdate = new Question(); questionToUpdate.setId(questionId); questionToUpdate.setTitle("Old Title"); questionToUpdate.setDescription("Old Description"); + questionToUpdate.setUser(user); + Mockito.when(userRepository.findById(authorId)).thenReturn(Optional.of(user)); Mockito.when(questionRepository.findById(questionId)).thenReturn(Optional.of(questionToUpdate)); Mockito.when(questionRepository.save(questionCaptor.capture())).thenReturn(questionToUpdate); QuestionResponseDTO result = questionService.update(questionId, request); + Mockito.verify(userRepository).findById(authorId); Mockito.verify(questionRepository).findById(questionId); Mockito.verify(questionRepository).save(questionCaptor.capture()); @@ -109,16 +134,25 @@ void testUpdateExistingQuestion() { Assertions.assertEquals(request.getTitle(), capturedQuestion.getTitle()); Assertions.assertEquals(request.getDescription(), capturedQuestion.getDescription()); + Assertions.assertEquals(user, capturedQuestion.getUser()); Assertions.assertEquals(questionId, result.getId()); } @Test void testUpdateNotExistingQuestion() { Long questionId = 123L; + Long authorId = 1L; QuestionRequestDTO request = new QuestionRequestDTO(); request.setTitle("Title"); request.setDescription("Description"); + request.setAuthorId(authorId); + + User user = new User(); + user.setId(authorId); + user.setFirstname("John"); + user.setLastname("Doe"); + Mockito.when(userRepository.findById(authorId)).thenReturn(Optional.of(user)); Mockito.when(questionRepository.findById(questionId)).thenReturn(Optional.empty()); ResourceNotFoundException resourceNotFoundException = Assertions.assertThrows(ResourceNotFoundException.class, () -> { @@ -126,6 +160,7 @@ void testUpdateNotExistingQuestion() { }); Assertions.assertEquals("Question not found with id " + questionId, resourceNotFoundException.getMessage()); + Mockito.verify(userRepository).findById(authorId); Mockito.verify(questionRepository).findById(questionId); Mockito.verifyNoMoreInteractions(questionRepository); } @@ -166,4 +201,4 @@ private static Stream provideDescriptions() { Arguments.of("Some description", "Title\nSome description") ); } -} \ No newline at end of file +}