feat: add task editing
This commit is contained in:
@ -0,0 +1,245 @@
|
||||
package se.rubble.hemhub.task;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
import se.rubble.hemhub.user.User;
|
||||
import se.rubble.hemhub.user.UserRepository;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
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.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
class TaskEditingApiTest {
|
||||
|
||||
private static final Instant CREATED_AT = Instant.parse("2026-07-27T09:00:00Z");
|
||||
|
||||
@Autowired
|
||||
private WebApplicationContext context;
|
||||
|
||||
@Autowired
|
||||
private TaskRepository taskRepository;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
taskRepository.deleteAll();
|
||||
userRepository.deleteAll();
|
||||
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(TaskStatus.class)
|
||||
void updatesDetailsInEveryStatusAndPreservesOtherFields(TaskStatus statusValue) throws Exception {
|
||||
User assignee = createUser();
|
||||
Task task = saveTask(statusValue, assignee, "Före", "Gammal", 3);
|
||||
|
||||
mockMvc.perform(put("/api/tasks/{taskId}/details", task.getId())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"title": " Efter ",
|
||||
"description": " Ny beskrivning ",
|
||||
"points": 7
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(task.getId().toString()))
|
||||
.andExpect(jsonPath("$.title").value("Efter"))
|
||||
.andExpect(jsonPath("$.description").value("Ny beskrivning"))
|
||||
.andExpect(jsonPath("$.points").value(7))
|
||||
.andExpect(jsonPath("$.status").value(statusValue.name()))
|
||||
.andExpect(jsonPath("$.assignee.id").value(assignee.getId().toString()))
|
||||
.andExpect(jsonPath("$.assignee.name").value(assignee.getName()))
|
||||
.andExpect(jsonPath("$.createdAt").value(CREATED_AT.toString()));
|
||||
|
||||
Task updated = taskRepository.findOneById(task.getId()).orElseThrow();
|
||||
assertEquals(statusValue, updated.getStatus());
|
||||
assertEquals(assignee.getId(), updated.getAssignee().getId());
|
||||
assertEquals(CREATED_AT, updated.getCreatedAt());
|
||||
assertTrue(userRepository.existsById(assignee.getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsBoundariesBlankDescriptionAndUnchangedValues() throws Exception {
|
||||
String title = "å".repeat(100);
|
||||
String description = "å".repeat(500);
|
||||
Task task = saveTask(TaskStatus.WAITING, null, title, description, 1);
|
||||
|
||||
updateDetails(task.getId(), title, description, 99)
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.title").value(title))
|
||||
.andExpect(jsonPath("$.description").value(description))
|
||||
.andExpect(jsonPath("$.points").value(99));
|
||||
|
||||
updateDetails(task.getId(), title, " ", 1)
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.description").doesNotExist())
|
||||
.andExpect(jsonPath("$.points").value(1));
|
||||
|
||||
updateDetails(task.getId(), title, null, 1)
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.description").doesNotExist());
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{1}")
|
||||
@MethodSource("invalidRequests")
|
||||
void rejectsInvalidOrIncompleteRequests(String request, String description) throws Exception {
|
||||
Task task = saveTask(TaskStatus.WAITING, null, "Före", null, 3);
|
||||
|
||||
mockMvc.perform(put("/api/tasks/{taskId}/details", task.getId())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(request))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("INVALID_TASK"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsNotFoundAndKeepsOtherTaskUnchanged() throws Exception {
|
||||
Task otherTask = saveTask(TaskStatus.COMPLETED, null, "Annan", "Oförändrad", 9);
|
||||
UUID unknownId = UUID.randomUUID();
|
||||
|
||||
updateDetails(unknownId, "Ny", null, 4)
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value("TASK_NOT_FOUND"));
|
||||
|
||||
mockMvc.perform(get("/api/tasks"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.length()").value(1))
|
||||
.andExpect(jsonPath("$[0].id").value(otherTask.getId().toString()))
|
||||
.andExpect(jsonPath("$[0].title").value("Annan"))
|
||||
.andExpect(jsonPath("$[0].description").value("Oförändrad"))
|
||||
.andExpect(jsonPath("$[0].points").value(9));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsExistingBadRequestForInvalidUuid() throws Exception {
|
||||
mockMvc.perform(put("/api/tasks/{taskId}/details", "inte-ett-uuid")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"title": "Ny", "description": null, "points": 4}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("INVALID_TASK_ASSIGNMENT"));
|
||||
}
|
||||
|
||||
private org.springframework.test.web.servlet.ResultActions updateDetails(
|
||||
UUID taskId,
|
||||
String title,
|
||||
String description,
|
||||
int points) throws Exception {
|
||||
return mockMvc.perform(put("/api/tasks/{taskId}/details", taskId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"title": %s,
|
||||
"description": %s,
|
||||
"points": %d
|
||||
}
|
||||
""".formatted(jsonString(title), jsonString(description), points)));
|
||||
}
|
||||
|
||||
private User createUser() throws Exception {
|
||||
String response = mockMvc.perform(post("/api/users")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"name": "Urban"}
|
||||
"""))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
String id = com.jayway.jsonpath.JsonPath.read(response, "$.id");
|
||||
return userRepository.findById(UUID.fromString(id)).orElseThrow();
|
||||
}
|
||||
|
||||
private Task saveTask(
|
||||
TaskStatus statusValue,
|
||||
User assignee,
|
||||
String title,
|
||||
String description,
|
||||
int points) {
|
||||
return taskRepository.save(new Task(
|
||||
UUID.randomUUID(),
|
||||
title,
|
||||
description,
|
||||
statusValue,
|
||||
points,
|
||||
assignee,
|
||||
CREATED_AT));
|
||||
}
|
||||
|
||||
private static Stream<Arguments> invalidRequests() {
|
||||
return Stream.of(
|
||||
Arguments.of("{}", "alla fält saknas"),
|
||||
Arguments.of("""
|
||||
{"description": null, "points": 1}
|
||||
""", "titel saknas"),
|
||||
Arguments.of("""
|
||||
{"title": null, "description": null, "points": 1}
|
||||
""", "titel är null"),
|
||||
Arguments.of("""
|
||||
{"title": " ", "description": null, "points": 1}
|
||||
""", "titel är blank"),
|
||||
Arguments.of("""
|
||||
{"title": "%s", "description": null, "points": 1}
|
||||
""".formatted("a".repeat(101)), "titel är för lång"),
|
||||
Arguments.of("""
|
||||
{"title": "Titel", "description": "%s", "points": 1}
|
||||
""".formatted("a".repeat(501)), "beskrivning är för lång"),
|
||||
Arguments.of("""
|
||||
{"title": "Titel", "points": 1}
|
||||
""", "beskrivning saknas"),
|
||||
Arguments.of("""
|
||||
{"title": "Titel", "description": null}
|
||||
""", "poäng saknas"),
|
||||
Arguments.of("""
|
||||
{"title": "Titel", "description": null, "points": null}
|
||||
""", "poäng är null"),
|
||||
Arguments.of("""
|
||||
{"title": "Titel", "description": null, "points": "7"}
|
||||
""", "poäng är text"),
|
||||
Arguments.of("""
|
||||
{"title": "Titel", "description": null, "points": 1.5}
|
||||
""", "poäng är decimal"),
|
||||
Arguments.of("""
|
||||
{"title": "Titel", "description": null, "points": 0}
|
||||
""", "poäng är noll"),
|
||||
Arguments.of("""
|
||||
{"title": "Titel", "description": null, "points": -1}
|
||||
""", "poäng är negativ"),
|
||||
Arguments.of("""
|
||||
{"title": "Titel", "description": null, "points": 100}
|
||||
""", "poäng är för hög"));
|
||||
}
|
||||
|
||||
private static String jsonString(String value) {
|
||||
if (value == null) {
|
||||
return "null";
|
||||
}
|
||||
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class TaskTest {
|
||||
@ -29,6 +30,25 @@ class TaskTest {
|
||||
Instant.parse("2026-07-26T12:00:00Z")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void changesOnlyEditableDetailsAndProtectsPointsInvariant() {
|
||||
Task task = taskWithPoints(3);
|
||||
UUID id = task.getId();
|
||||
TaskStatus status = task.getStatus();
|
||||
Instant createdAt = task.getCreatedAt();
|
||||
|
||||
task.changeDetails("Ny titel", "Ny beskrivning", 7);
|
||||
|
||||
assertEquals(id, task.getId());
|
||||
assertEquals("Ny titel", task.getTitle());
|
||||
assertEquals("Ny beskrivning", task.getDescription());
|
||||
assertEquals(7, task.getPoints());
|
||||
assertEquals(status, task.getStatus());
|
||||
assertEquals(createdAt, task.getCreatedAt());
|
||||
assertThrows(InvalidTaskException.class, () -> task.changeDetails("Titel", null, 0));
|
||||
assertThrows(InvalidTaskException.class, () -> task.changeDetails("Titel", null, 100));
|
||||
}
|
||||
|
||||
private Task taskWithPoints(int points) {
|
||||
return new Task(
|
||||
UUID.randomUUID(),
|
||||
|
||||
Reference in New Issue
Block a user