Add integration tests for validation, error handling, and task filtering
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:
Urban Modig
2025-10-11 16:15:19 +02:00
parent 302078fbec
commit 65c340265f
4 changed files with 230 additions and 11 deletions

View File

@ -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))));
}
}

View File

@ -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")

View File

@ -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());
}
}