Add project and task management with scheduling and notifications
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Introduced `Project` and `Task` entities, along with supporting services, repositories, and APIs. Added features for project-based and household-level task management, including creation, listing, updates, and validation. Implemented scheduled notifications for tasks due tomorrow. Updated Flyway migrations, configuration files, and tests to support these functionalities.
This commit is contained in:
@ -1,32 +1,36 @@
|
||||
package se.urmo.hemhub.integration;
|
||||
|
||||
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;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
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.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class HouseholdControllerIT {
|
||||
|
||||
@Autowired MockMvc mvc;
|
||||
@Autowired
|
||||
MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void create_household_and_list_for_user() throws Exception {
|
||||
var jwtUser = jwt().jwt(j -> {
|
||||
j.subject("sub-user-1");
|
||||
j.claim("email","u1@example.com");
|
||||
j.claim("preferred_username","u1");
|
||||
j.claim("realm_access", java.util.Map.of("roles", java.util.List.of("OWNER","MEMBER")));
|
||||
j.claim("email", "u1@example.com");
|
||||
j.claim("preferred_username", "u1");
|
||||
j.claim("realm_access", Map.of("roles", List.of("OWNER", "MEMBER")));
|
||||
});
|
||||
|
||||
// create
|
||||
@ -45,8 +49,16 @@ class HouseholdControllerIT {
|
||||
|
||||
@Test
|
||||
void add_member_requires_owner() throws Exception {
|
||||
var owner = jwt().jwt(j -> { j.subject("owner-sub"); j.claim("email","o@ex.com"); j.claim("preferred_username","owner"); });
|
||||
var other = jwt().jwt(j -> { j.subject("other-sub"); j.claim("email","x@ex.com"); j.claim("preferred_username","x"); });
|
||||
var owner = jwt().jwt(j -> {
|
||||
j.subject("owner-sub");
|
||||
j.claim("email", "o@ex.com");
|
||||
j.claim("preferred_username", "owner");
|
||||
});
|
||||
var other = jwt().jwt(j -> {
|
||||
j.subject("other-sub");
|
||||
j.claim("email", "x@ex.com");
|
||||
j.claim("preferred_username", "x");
|
||||
});
|
||||
|
||||
// create household as owner
|
||||
var res = mvc.perform(post("/api/v1/households").with(owner)
|
||||
@ -55,7 +67,7 @@ class HouseholdControllerIT {
|
||||
var id = com.jayway.jsonpath.JsonPath.read(res.getResponse().getContentAsString(), "$.id");
|
||||
|
||||
// cannot add as non-owner
|
||||
mvc.perform(post("/api/v1/households/"+id+"/members").with(other)
|
||||
mvc.perform(post("/api/v1/households/" + id + "/members").with(other)
|
||||
.contentType("application/json")
|
||||
.content("{\"userSub\":\"u2\",\"role\":\"MEMBER\"}"))
|
||||
.andExpect(status().isForbidden());
|
||||
|
||||
@ -1,24 +1,26 @@
|
||||
package se.urmo.hemhub.integration;
|
||||
|
||||
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.test.context.ActiveProfiles;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class MeControllerBranchesIT {
|
||||
|
||||
@Autowired MockMvc mvc;
|
||||
@Autowired
|
||||
MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void me_withoutRealmAccess_rolesEmpty() throws Exception {
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
package se.urmo.hemhub.integration;
|
||||
|
||||
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.test.web.servlet.MockMvc;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
package se.urmo.hemhub.integration;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import org.junit.jupiter.api.Test;
|
||||
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;
|
||||
|
||||
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 ProjectTaskControllerIT {
|
||||
|
||||
@Autowired MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void project_and_tasks_happy_path() throws Exception {
|
||||
var owner = jwt().jwt(j -> {
|
||||
j.subject("owner-sub");
|
||||
j.claim("email","o@ex.com");
|
||||
j.claim("preferred_username","owner");
|
||||
});
|
||||
|
||||
// Create household first (from Iteration 2 endpoint)
|
||||
var hRes = mvc.perform(post("/api/v1/households").with(owner)
|
||||
.contentType("application/json").content("{\"name\":\"H1\"}"))
|
||||
.andExpect(status().isOk()).andReturn();
|
||||
var householdId = JsonPath.read(hRes.getResponse().getContentAsString(), "$.id");
|
||||
|
||||
// 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");
|
||||
|
||||
// List projects (should contain one)
|
||||
mvc.perform(get("/api/v1/households/"+householdId+"/projects").with(owner))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].name").value("Sovrumsrenovering"));
|
||||
|
||||
// Create a task
|
||||
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
|
||||
mvc.perform(get("/api/v1/projects/"+projectId+"/tasks").with(owner))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].priority").value("LOW"));
|
||||
|
||||
// Update task status
|
||||
var list = mvc.perform(get("/api/v1/projects/"+projectId+"/tasks").with(owner))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
var taskId = com.jayway.jsonpath.JsonPath.read(list, "$[0].id");
|
||||
|
||||
mvc.perform(patch("/api/v1/tasks/"+taskId).with(owner)
|
||||
.contentType("application/json")
|
||||
.content("{\"status\":\"DONE\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("DONE"));
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,15 @@
|
||||
package se.urmo.hemhub.integration;
|
||||
|
||||
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.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
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.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class TaskDueControllerIT {
|
||||
|
||||
@Autowired
|
||||
MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void list_tasks_due_tomorrow_returns_results_for_member() throws Exception {
|
||||
var user = jwt().jwt(j -> {
|
||||
j.subject("sub-user");
|
||||
j.claim("email", "u@ex.com");
|
||||
j.claim("preferred_username", "u");
|
||||
});
|
||||
|
||||
// Create household
|
||||
var hh = mvc.perform(post("/api/v1/households").with(user)
|
||||
.contentType("application/json").content("{\"name\":\"H1\"}"))
|
||||
.andExpect(status().isOk()).andReturn();
|
||||
var householdId = JsonPath.read(hh.getResponse().getContentAsString(), "$.id");
|
||||
|
||||
// Create a household-level task due tomorrow
|
||||
var tomorrow = LocalDate.now().plusDays(1);
|
||||
mvc.perform(post("/api/v1/households/" + householdId + "/tasks").with(user)
|
||||
.contentType("application/json")
|
||||
.content("{\"title\":\"Påminn mig\",\"priority\":\"MEDIUM\",\"dueDate\":\"" + tomorrow + "\"}"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// Expect the new endpoint to include it
|
||||
mvc.perform(get("/api/v1/tasks/due/tomorrow").with(user))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].title").value("Påminn mig"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user