Add integration tests for validation, error handling, and task filtering
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Introduced `ValidationAndErrorHandlingIT` and `PagingAndFilteringIT` integration tests to verify validations, error responses, and task filtering/pagination behaviors. Updated IntelliJ HTTP client script for task-related operations. Enhanced `ProjectTaskControllerIT` assertions for better coverage.
This commit is contained in:
@ -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}}
|
||||
|
||||
@ -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))));
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user