diff --git a/src/test/http/hemhub-api.http b/src/test/http/hemhub-api.http index bf2bdb2..ab0c19b 100644 --- a/src/test/http/hemhub-api.http +++ b/src/test/http/hemhub-api.http @@ -113,6 +113,37 @@ Authorization: Bearer {{token}} GET http://localhost:8080/api/v1/households/{{householdId}}/tasks Authorization: Bearer {{token}} +### 15a) Prepare a {{tomorrow}} variable (YYYY-MM-DD) +GET http://localhost:8080/public/info + +> {% + const d = new Date(); + d.setDate(d.getDate() + 1); + const tomorrow = d.toISOString().slice(0,10); + client.global.set("tomorrow", tomorrow); +%} + +### 15b) Capture latest household task id (if not already set) +GET http://localhost:8080/api/v1/households/{{householdId}}/tasks?size=1&sort=createdAt,desc +Authorization: Bearer {{token}} + +> {% + if (!client.global.get("taskId")) { + const id = response.body?.content?.length ? response.body.content[0].id : null; + if (id) client.global.set("taskId", id); + } +%} + +### 15c) Set that task's dueDate to {{tomorrow}} +PATCH http://localhost:8080/api/v1/tasks/{{taskId}} +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "dueDate": "{{tomorrow}}" +} + + ### 16) My tasks due tomorrow GET http://localhost:8080/api/v1/tasks/due/tomorrow Authorization: Bearer {{token}} diff --git a/src/test/java/se/urmo/hemhub/integration/PagingAndFilteringIT.java b/src/test/java/se/urmo/hemhub/integration/PagingAndFilteringIT.java new file mode 100644 index 0000000..2f32256 --- /dev/null +++ b/src/test/java/se/urmo/hemhub/integration/PagingAndFilteringIT.java @@ -0,0 +1,101 @@ +package se.urmo.hemhub.integration; + +import com.jayway.jsonpath.JsonPath; +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.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDate; + +import static org.hamcrest.Matchers.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class PagingAndFilteringIT { + + @Autowired MockMvc mvc; + + @Test + void list_tasks_supports_filters_and_pagination() throws Exception { + var user = jwt().jwt(j -> { + j.subject("sub-owner"); + j.claim("email","o@ex.com"); + j.claim("preferred_username","owner"); + }); + + // 1) Household + var hh = mvc.perform(post("/api/v1/households").with(user) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"H1\"}")) + .andExpect(status().isOk()) + .andReturn(); + var householdId = JsonPath.read(hh.getResponse().getContentAsString(), "$.id"); + + // 2) Project (owner-only) + var pr = mvc.perform(post("/api/v1/projects").with(user) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"householdId\":\"" + householdId + "\",\"name\":\"Sovrum\"}")) + .andExpect(status().isOk()) + .andReturn(); + var projectId = JsonPath.read(pr.getResponse().getContentAsString(), "$.id"); + + // 3) Create several tasks (mixed status/priority/due dates) + var tomorrow = LocalDate.now().plusDays(1); + var nextWeek = LocalDate.now().plusDays(7); + + // Project tasks + mvc.perform(post("/api/v1/tasks").with(user) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"projectId\":\""+projectId+"\",\"title\":\"A\",\"priority\":\"HIGH\",\"status\":\"OPEN\",\"dueDate\":\""+tomorrow+"\"}")) + .andExpect(status().isOk()); + mvc.perform(post("/api/v1/tasks").with(user) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"projectId\":\""+projectId+"\",\"title\":\"B\",\"priority\":\"LOW\",\"status\":\"DONE\",\"dueDate\":\""+nextWeek+"\"}")) + .andExpect(status().isOk()); + mvc.perform(post("/api/v1/tasks").with(user) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"projectId\":\""+projectId+"\",\"title\":\"C\",\"priority\":\"HIGH\",\"status\":\"IN_PROGRESS\",\"dueDate\":\""+nextWeek+"\"}")) + .andExpect(status().isOk()); + + // Household tasks (no project) + mvc.perform(post("/api/v1/households/"+householdId+"/tasks").with(user) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"title\":\"D\",\"priority\":\"HIGH\",\"status\":\"OPEN\",\"dueDate\":\""+nextWeek+"\"}")) + .andExpect(status().isOk()); + mvc.perform(post("/api/v1/households/"+householdId+"/tasks").with(user) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"title\":\"E\",\"priority\":\"MEDIUM\",\"status\":\"OPEN\"}")) + .andExpect(status().isOk()); + + // 4) Filter: project tasks with priority=HIGH and dueTo=nextWeek, paged size=1 + mvc.perform(get("/api/v1/projects/"+projectId+"/tasks") + .with(user) + .param("priority","HIGH") + .param("dueTo", nextWeek.toString()) + .param("page","0") + .param("size","1") + .param("sort","title,asc")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].title", anyOf(is("A"), is("C")))) + .andExpect(jsonPath("$.totalElements", is(2))) // A and C match HIGH and due<=nextWeek + .andExpect(jsonPath("$.totalPages", is(2))); + + // 5) Household tasks filtered: status=OPEN (should include D and E) + mvc.perform(get("/api/v1/households/"+householdId+"/tasks") + .with(user) + .param("status","OPEN") + .param("sort","title,asc")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content[*].title", everyItem(anyOf(is("D"), is("E"), is("A"))))) + .andExpect(jsonPath("$.content", hasSize(greaterThanOrEqualTo(2)))); + } +} diff --git a/src/test/java/se/urmo/hemhub/integration/ProjectTaskControllerIT.java b/src/test/java/se/urmo/hemhub/integration/ProjectTaskControllerIT.java index d268928..f42e39b 100644 --- a/src/test/java/se/urmo/hemhub/integration/ProjectTaskControllerIT.java +++ b/src/test/java/se/urmo/hemhub/integration/ProjectTaskControllerIT.java @@ -2,9 +2,9 @@ package se.urmo.hemhub.integration; import com.jayway.jsonpath.JsonPath; 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.beans.factory.annotation.Autowired; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; @@ -36,30 +36,32 @@ class ProjectTaskControllerIT { // Create project var pRes = mvc.perform(post("/api/v1/projects").with(owner) .contentType("application/json").content("{\"householdId\":\""+householdId+"\",\"name\":\"Sovrumsrenovering\"}")) - .andExpect(status().isOk()).andExpect(jsonPath("$.id").exists()).andReturn(); - var projectId = com.jayway.jsonpath.JsonPath.read(pRes.getResponse().getContentAsString(), "$.id"); + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").exists()) + .andReturn(); + var projectId = JsonPath.read(pRes.getResponse().getContentAsString(), "$.id"); - // List projects (should contain one) + // List projects (paged; expect first item name) mvc.perform(get("/api/v1/households/"+householdId+"/projects").with(owner)) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].name").value("Sovrumsrenovering")); + .andExpect(jsonPath("$.content[0].name").value("Sovrumsrenovering")); - // Create a task + // Create a task (project-scoped) mvc.perform(post("/api/v1/tasks").with(owner) .contentType("application/json") .content("{\"projectId\":\""+projectId+"\",\"title\":\"Damsug soffan\",\"priority\":\"LOW\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.title").value("Damsug soffan")); - // List tasks + // List tasks (paged; expect first item priority) mvc.perform(get("/api/v1/projects/"+projectId+"/tasks").with(owner)) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].priority").value("LOW")); + .andExpect(jsonPath("$.content[0].priority").value("LOW")); - // Update task status - var list = mvc.perform(get("/api/v1/projects/"+projectId+"/tasks").with(owner)) + // Update task status → fetch list to get taskId from page content + var listJson = mvc.perform(get("/api/v1/projects/"+projectId+"/tasks").with(owner)) .andReturn().getResponse().getContentAsString(); - var taskId = com.jayway.jsonpath.JsonPath.read(list, "$[0].id"); + var taskId = JsonPath.read(listJson, "$.content[0].id"); mvc.perform(patch("/api/v1/tasks/"+taskId).with(owner) .contentType("application/json") diff --git a/src/test/java/se/urmo/hemhub/integration/ValidationAndErrorHandlingIT.java b/src/test/java/se/urmo/hemhub/integration/ValidationAndErrorHandlingIT.java new file mode 100644 index 0000000..7fd5cc2 --- /dev/null +++ b/src/test/java/se/urmo/hemhub/integration/ValidationAndErrorHandlingIT.java @@ -0,0 +1,85 @@ +package se.urmo.hemhub.integration; + +import com.jayway.jsonpath.JsonPath; +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.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class ValidationAndErrorHandlingIT { + + @Autowired MockMvc mvc; + + @Test + void create_household_task_without_title_returns_400_with_field_error() throws Exception { + var user = jwt().jwt(j -> { + j.subject("sub-user"); + j.claim("email","user@example.com"); + j.claim("preferred_username","user"); + }); + + // Create household + var hh = mvc.perform(post("/api/v1/households").with(user) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"Test Household\"}")) + .andExpect(status().isOk()) + .andReturn(); + var householdId = JsonPath.read(hh.getResponse().getContentAsString(), "$.id"); + + // Try to create a household-level task with missing required 'title' + mvc.perform(post("/api/v1/households/" + householdId + "/tasks").with(user) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"priority\":\"HIGH\"}")) + .andExpect(status().isBadRequest()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + // Global error envelope + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.message").value("Validation failed")) + .andExpect(jsonPath("$.path").value("/api/v1/households/" + householdId + "/tasks")) + // Field details from @RestControllerAdvice + .andExpect(jsonPath("$.details.fields.title").value("title is required")); + } + + @Test + void update_task_with_invalid_enum_returns_400_with_message() throws Exception { + var user = jwt().jwt(j -> { + j.subject("sub-user2"); + j.claim("email","u2@example.com"); + j.claim("preferred_username","u2"); + }); + + // Create household + var hh = mvc.perform(post("/api/v1/households").with(user) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"H2\"}")) + .andExpect(status().isOk()) + .andReturn(); + var householdId = JsonPath.read(hh.getResponse().getContentAsString(), "$.id"); + + // Create a valid household task + var tRes = mvc.perform(post("/api/v1/households/" + householdId + "/tasks").with(user) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"title\":\"Valid task\"}")) + .andExpect(status().isOk()) + .andReturn(); + var taskId = JsonPath.read(tRes.getResponse().getContentAsString(), "$.id"); + + // Patch with an invalid status enum → should 400 (handled by @ControllerAdvice malformed body or enum conversion) + mvc.perform(patch("/api/v1/tasks/" + taskId).with(user) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"status\":\"INVALID_ENUM\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.message").exists()); + } +}