feat: add task points
This commit is contained in:
@ -1,5 +1,14 @@
|
||||
package se.rubble.hemhub.task;
|
||||
|
||||
public record CreateTaskRequest(String title, String description) {
|
||||
}
|
||||
import tools.jackson.databind.JsonNode;
|
||||
|
||||
public record CreateTaskRequest(String title, String description, JsonNode points) {
|
||||
|
||||
Integer integerPoints() {
|
||||
if (points == null || !points.isIntegralNumber() || !points.canConvertToInt()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return points.intValue();
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,9 @@ class Task {
|
||||
@Column(nullable = false, length = 20)
|
||||
private TaskStatus status;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int points;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@ -38,11 +41,18 @@ class Task {
|
||||
String title,
|
||||
String description,
|
||||
TaskStatus status,
|
||||
int points,
|
||||
Instant createdAt) {
|
||||
if (points < 1 || points > 99) {
|
||||
throw new InvalidTaskException(
|
||||
"Poäng måste vara ett heltal mellan 1 och 99.");
|
||||
}
|
||||
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.status = status;
|
||||
this.points = points;
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
@ -62,8 +72,11 @@ class Task {
|
||||
return status;
|
||||
}
|
||||
|
||||
int getPoints() {
|
||||
return points;
|
||||
}
|
||||
|
||||
Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ public class TaskController {
|
||||
public TaskResponse create(@RequestBody(required = false) CreateTaskRequest request) {
|
||||
return taskService.create(
|
||||
request == null ? null : request.title(),
|
||||
request == null ? null : request.description());
|
||||
request == null ? null : request.description(),
|
||||
request == null ? null : request.integerPoints());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ public record TaskResponse(
|
||||
String title,
|
||||
String description,
|
||||
TaskStatus status,
|
||||
int points,
|
||||
Instant createdAt) {
|
||||
|
||||
static TaskResponse from(Task task) {
|
||||
@ -16,7 +17,7 @@ public record TaskResponse(
|
||||
task.getTitle(),
|
||||
task.getDescription(),
|
||||
task.getStatus(),
|
||||
task.getPoints(),
|
||||
task.getCreatedAt());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -33,7 +33,10 @@ class TaskService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
TaskResponse create(String requestedTitle, String requestedDescription) {
|
||||
TaskResponse create(
|
||||
String requestedTitle,
|
||||
String requestedDescription,
|
||||
Integer requestedPoints) {
|
||||
String title = requestedTitle == null ? "" : requestedTitle.trim();
|
||||
String description = normalizeDescription(requestedDescription);
|
||||
|
||||
@ -47,11 +50,17 @@ class TaskService {
|
||||
"Beskrivningen får innehålla högst 500 tecken.");
|
||||
}
|
||||
|
||||
if (requestedPoints == null) {
|
||||
throw new InvalidTaskException(
|
||||
"Poäng måste vara ett heltal mellan 1 och 99.");
|
||||
}
|
||||
|
||||
Task task = new Task(
|
||||
UUID.randomUUID(),
|
||||
title,
|
||||
description,
|
||||
TaskStatus.WAITING,
|
||||
requestedPoints,
|
||||
Instant.now(clock));
|
||||
|
||||
return TaskResponse.from(taskRepository.save(task));
|
||||
@ -70,4 +79,3 @@ class TaskService {
|
||||
return value.codePointCount(0, value.length());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
spring.datasource.url=jdbc:h2:file:./data/hemhub;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH
|
||||
spring.datasource.url=jdbc:h2:mem:hemhub;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH;DB_CLOSE_DELAY=-1
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
spring.jpa.hibernate.ddl-auto=validate
|
||||
spring.jpa.open-in-view=false
|
||||
spring.flyway.enabled=true
|
||||
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
ALTER TABLE task ADD COLUMN points INTEGER;
|
||||
|
||||
UPDATE task SET points = 1 WHERE points IS NULL;
|
||||
|
||||
ALTER TABLE task ALTER COLUMN points SET NOT NULL;
|
||||
|
||||
ALTER TABLE task
|
||||
ADD CONSTRAINT ck_task_points_range CHECK (points BETWEEN 1 AND 99);
|
||||
@ -9,6 +9,7 @@ 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.ResultActions;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
@ -41,7 +42,8 @@ class TaskApiTest {
|
||||
.content("""
|
||||
{
|
||||
"title": " Dammsuga ",
|
||||
"description": " Bottenvåningen "
|
||||
"description": " Bottenvåningen ",
|
||||
"points": 7
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isCreated())
|
||||
@ -49,7 +51,12 @@ class TaskApiTest {
|
||||
.andExpect(jsonPath("$.title").value("Dammsuga"))
|
||||
.andExpect(jsonPath("$.description").value("Bottenvåningen"))
|
||||
.andExpect(jsonPath("$.status").value("WAITING"))
|
||||
.andExpect(jsonPath("$.points").value(7))
|
||||
.andExpect(jsonPath("$.createdAt").isString());
|
||||
|
||||
mockMvc.perform(get("/api/tasks"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].points").value(7));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -57,7 +64,7 @@ class TaskApiTest {
|
||||
mockMvc.perform(post("/api/tasks")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"title": "Dammsuga", "description": " "}
|
||||
{"title": "Dammsuga", "description": " ", "points": 1}
|
||||
"""))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.description").value((Object) null));
|
||||
@ -66,18 +73,58 @@ class TaskApiTest {
|
||||
@Test
|
||||
void rejectsBlankAndTooLongTitles() throws Exception {
|
||||
assertInvalidTask("""
|
||||
{"title": " "}
|
||||
{"title": " ", "points": 1}
|
||||
""");
|
||||
assertInvalidTask("{\"title\": \"%s\"}".formatted("a".repeat(101)));
|
||||
assertInvalidTask(
|
||||
"{\"title\": \"%s\", \"points\": 1}".formatted("a".repeat(101)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsTooLongDescription() throws Exception {
|
||||
assertInvalidTask("""
|
||||
{"title": "Dammsuga", "description": "%s"}
|
||||
{"title": "Dammsuga", "description": "%s", "points": 1}
|
||||
""".formatted("a".repeat(501)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsPointBoundaries() throws Exception {
|
||||
createTaskWithPoints(1)
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.points").value(1));
|
||||
createTaskWithPoints(99)
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.points").value(99));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingNullAndOutOfRangePoints() throws Exception {
|
||||
assertInvalidTask("""
|
||||
{"title": "Saknas"}
|
||||
""");
|
||||
assertInvalidTask("""
|
||||
{"title": "Null", "points": null}
|
||||
""");
|
||||
assertInvalidTask("""
|
||||
{"title": "Noll", "points": 0}
|
||||
""");
|
||||
assertInvalidTask("""
|
||||
{"title": "Negativ", "points": -1}
|
||||
""");
|
||||
assertInvalidTask("""
|
||||
{"title": "För stor", "points": 100}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonIntegerPoints() throws Exception {
|
||||
assertInvalidTask("""
|
||||
{"title": "Decimal", "points": 1.5}
|
||||
""");
|
||||
assertInvalidTask("""
|
||||
{"title": "Text", "points": "sju"}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listsTasksOldestFirstWithIdAsTieBreaker() throws Exception {
|
||||
Instant older = Instant.parse("2026-07-24T10:00:00Z");
|
||||
@ -86,17 +133,25 @@ class TaskApiTest {
|
||||
UUID secondId = UUID.fromString("00000000-0000-0000-0000-000000000002");
|
||||
UUID newestId = UUID.fromString("00000000-0000-0000-0000-000000000003");
|
||||
|
||||
taskRepository.save(new Task(newestId, "Nyast", null, TaskStatus.WAITING, newer));
|
||||
taskRepository.save(new Task(secondId, "Andra", null, TaskStatus.IN_PROGRESS, older));
|
||||
taskRepository.save(new Task(firstId, "Första", null, TaskStatus.COMPLETED, older));
|
||||
taskRepository.save(new Task(newestId, "Nyast", null, TaskStatus.WAITING, 3, newer));
|
||||
taskRepository.save(new Task(secondId, "Andra", null, TaskStatus.IN_PROGRESS, 2, older));
|
||||
taskRepository.save(new Task(firstId, "Första", null, TaskStatus.COMPLETED, 1, older));
|
||||
|
||||
mockMvc.perform(get("/api/tasks"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].title").value("Första"))
|
||||
.andExpect(jsonPath("$[0].points").value(1))
|
||||
.andExpect(jsonPath("$[1].title").value("Andra"))
|
||||
.andExpect(jsonPath("$[2].title").value("Nyast"));
|
||||
}
|
||||
|
||||
private ResultActions createTaskWithPoints(int points) throws Exception {
|
||||
return mockMvc.perform(post("/api/tasks")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"title\": \"Uppgift %d\", \"points\": %d}"
|
||||
.formatted(points, points)));
|
||||
}
|
||||
|
||||
private void assertInvalidTask(String body) throws Exception {
|
||||
mockMvc.perform(post("/api/tasks")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
|
||||
27
backend/src/test/java/se/rubble/hemhub/task/TaskTest.java
Normal file
27
backend/src/test/java/se/rubble/hemhub/task/TaskTest.java
Normal file
@ -0,0 +1,27 @@
|
||||
package se.rubble.hemhub.task;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class TaskTest {
|
||||
|
||||
@Test
|
||||
void rejectsPointsOutsideAllowedRange() {
|
||||
assertThrows(InvalidTaskException.class, () -> taskWithPoints(0));
|
||||
assertThrows(InvalidTaskException.class, () -> taskWithPoints(100));
|
||||
}
|
||||
|
||||
private Task taskWithPoints(int points) {
|
||||
return new Task(
|
||||
UUID.randomUUID(),
|
||||
"Dammsuga",
|
||||
null,
|
||||
TaskStatus.WAITING,
|
||||
points,
|
||||
Instant.parse("2026-07-26T12:00:00Z"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user