merge(iteration-1): adopt task lombok conventions

This commit is contained in:
sechmachine
2026-08-15 11:51:36 +07:00
7 changed files with 114 additions and 211 deletions
@@ -0,0 +1,93 @@
# Test Evidence: Task Lombok boilerplate retrofit
- **Test type:** Unit
- **Requirement IDs:** `AUTH-011`, `TSK-001``TSK-012`, `DB-004`
- **Scenario IDs:** `AC-TSK-001``AC-TSK-006`, `AC-TSK-010`
- **Test class/method:** Source audit plus existing Task unit, web, and PostgreSQL integration suites
- **Implementation commit:** `7c1a26d`
## Protected behavior
The Task feature uses the configured Lombok processor for mechanical dependency-injection constructors, JPA no-argument constructors, and existing entity getters. Explicit Task constructors and mutation methods remain responsible for initial state, attribution, timestamps, and workflow invariants. The retrofit must not add entity equality, hash, string, or setter behavior and must not alter the existing public getter contract.
## Test method
The source audit counts the eligible handwritten constructors and getter methods before and after the retrofit. Existing Task tests then exercise Spring injection, MVC binding, JPA materialization, entity getters, authorization, status transitions, comments, and Project/attendance boundaries through the same public behavior used before the source-only change.
## Hand-derived expected result
Four Spring components have injection-only constructors, so all four may use `@RequiredArgsConstructor`. `Task` and `TaskComment` need protected JPA no-argument constructors and may use targeted Lombok generation. The 17 existing entity getters may be generated, but `Task.deletedByMembershipId`, `Task.updatedAt`, and `Task.version` must remain without newly exposed getters. Domain constructors and `Task.changeStatus` must remain explicit.
## RED
**Command**
```text
task_component_boilerplate=$(rg -n 'public (TaskController|TaskService|TaskQueryService|TaskDashboardService)\(' src/main/java/com/lab/labtimesheet/feature/task | wc -l | tr -d ' ')
task_entity_boilerplate=$(rg -n 'protected (Task|TaskComment)\(\)|public (Long|long|String|Instant|LocalDate|TaskStatus) get[A-Z][A-Za-z0-9]*\(\)' src/main/java/com/lab/labtimesheet/feature/task/model/entity | wc -l | tr -d ' ')
printf 'component_boilerplate=%s entity_boilerplate=%s\n' "$task_component_boilerplate" "$task_entity_boilerplate"
test "$task_component_boilerplate" -eq 0 -a "$task_entity_boilerplate" -eq 0
```
**Observed result**
```text
component_boilerplate=4 entity_boilerplate=19
Exit status 1. The Task package still contained all eligible handwritten boilerplate.
```
## GREEN
**Command**
```text
task_component_boilerplate=$(rg -n 'public (TaskController|TaskService|TaskQueryService|TaskDashboardService)\(' src/main/java/com/lab/labtimesheet/feature/task | wc -l | tr -d ' ')
task_entity_boilerplate=$(rg -n 'protected (Task|TaskComment)\(\)|public (Long|long|String|Instant|LocalDate|TaskStatus) get[A-Z][A-Za-z0-9]*\(\)' src/main/java/com/lab/labtimesheet/feature/task/model/entity | wc -l | tr -d ' ')
printf 'component_boilerplate=%s entity_boilerplate=%s\n' "$task_component_boilerplate" "$task_entity_boilerplate"
test "$task_component_boilerplate" -eq 0 -a "$task_entity_boilerplate" -eq 0
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
export PATH="$JAVA_HOME/bin:$PATH"
./mvnw -DskipTests compile
javap -classpath target/classes -p \
com.lab.labtimesheet.feature.task.model.entity.Task \
com.lab.labtimesheet.feature.task.model.entity.TaskComment \
com.lab.labtimesheet.feature.task.service.TaskService \
com.lab.labtimesheet.feature.task.service.TaskQueryService \
com.lab.labtimesheet.feature.task.service.TaskDashboardService \
com.lab.labtimesheet.feature.task.controller.TaskController
```
**Observed result**
```text
component_boilerplate=0 entity_boilerplate=0
Maven compile: BUILD SUCCESS; 127 production source files compiled with Java 25.
Bytecode inspection retained the four public component constructors, both protected JPA constructors, and all 17 existing entity getters. No getter exists for deletedByMembershipId, updatedAt, or version; domain constructors and Task.changeStatus remain explicit.
```
## Affected suite
**Command and result**
```text
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
export PATH="$JAVA_HOME/bin:$PATH"
./mvnw -Dtest='TaskDomainRulesTest,TaskPersistenceStructureTest,TaskQueryServiceTest,TaskDashboardServiceTest' test
# BUILD SUCCESS: 26 tests, 0 failures, 0 errors, 0 skipped.
./mvnw -Dtest='TaskControllerTest,ProjectTaskShellContractTest,ProjectTaskFormAccessibilityWebTest' test
# BUILD SUCCESS: 28 tests, 0 failures, 0 errors, 0 skipped.
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
./mvnw -Dtest='TaskCreationIntegrationTest,TaskMutationBoundaryTest' test
# BUILD SUCCESS against PostgreSQL 18.4: 16 tests, 0 failures, 0 errors, 0 skipped.
./mvnw -DskipTests -Ddoclint=all javadoc:javadoc
# BUILD SUCCESS. Existing warnings were outside feature.task; the Task package emitted no warning.
```
## External-test boundaries
This source audit does not prove runtime behavior by itself. The affected Task tests and compilation/Javadoc gates cover the behavior-preserving contract; no browser walkthrough or production database is required because templates, mappings, schema, and business logic are unchanged. Unprivileged sandbox attempts could not attach Mockito's Java agent or reach the host Docker socket; the same commands passed outside that sandbox, with the verified OrbStack socket supplied for Testcontainers.
@@ -12,6 +12,7 @@ import com.lab.labtimesheet.feature.task.service.TaskService;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
@@ -31,19 +32,11 @@ import org.springframework.web.bind.annotation.RequestParam;
* successful mutations use redirects to prevent duplicate submissions. * successful mutations use redirects to prevent duplicate submissions.
*/ */
@Controller @Controller
@RequiredArgsConstructor
public class TaskController { public class TaskController {
private final TaskService taskService; private final TaskService taskService;
/**
* Creates the MVC adapter for the Task application service.
*
* @param taskService authorized Task use cases
*/
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@GetMapping("/projects/{projectId}/tasks") @GetMapping("/projects/{projectId}/tasks")
String list(Authentication authentication, @PathVariable long projectId, Model model) { String list(Authentication authentication, @PathVariable long projectId, Model model) {
TaskListView taskList = taskService.list(authentication.getName(), projectId); TaskListView taskList = taskService.list(authentication.getName(), projectId);
@@ -12,6 +12,9 @@ import jakarta.persistence.Table;
import jakarta.persistence.Version; import jakarta.persistence.Version;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
/** /**
* Persisted Task aggregate row with one current same-Project assignee. * Persisted Task aggregate row with one current same-Project assignee.
@@ -22,6 +25,8 @@ import java.time.LocalDate;
*/ */
@Entity @Entity
@Table(name = "tasks") @Table(name = "tasks")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Task { public class Task {
@Id @Id
@@ -60,20 +65,20 @@ public class Task {
private Instant deletedAt; private Instant deletedAt;
@Column(name = "deleted_by_membership_id") @Column(name = "deleted_by_membership_id")
@Getter(AccessLevel.NONE)
private Long deletedByMembershipId; private Long deletedByMembershipId;
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false)
private Instant createdAt; private Instant createdAt;
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
@Getter(AccessLevel.NONE)
private Instant updatedAt; private Instant updatedAt;
@Version @Version
@Getter(AccessLevel.NONE)
private long version; private long version;
/** Constructor reserved for JPA materialization. */
protected Task() {}
/** /**
* Creates a TODO Task and records the creating membership as both creator and assigner. * Creates a TODO Task and records the creating membership as both creator and assigner.
* *
@@ -121,111 +126,4 @@ public class Task {
updatedAt = now; updatedAt = now;
} }
/**
* Returns the persistence identity.
*
* @return Task identifier, or {@code null} before insertion
*/
public Long getId() {
return id;
}
/**
* Returns the aggregate identity.
*
* @return owning Project identifier
*/
public long getProjectId() {
return projectId;
}
/**
* Returns the current assignment identity.
*
* @return current same-Project assignee membership identifier
*/
public long getAssigneeMembershipId() {
return assigneeMembershipId;
}
/**
* Returns the display title.
*
* @return normalized Task title
*/
public String getTitle() {
return title;
}
/**
* Returns the descriptive text.
*
* @return optional normalized description
*/
public String getDescription() {
return description;
}
/**
* Returns the workflow state.
*
* @return current fixed workflow status
*/
public TaskStatus getStatus() {
return status;
}
/**
* Returns the business deadline.
*
* @return optional validated due date
*/
public LocalDate getDueDate() {
return dueDate;
}
/**
* Returns current assignment timing.
*
* @return instant when the current assignment was established
*/
public Instant getAssignedAt() {
return assignedAt;
}
/**
* Returns original creator attribution.
*
* @return immutable creating membership identifier
*/
public long getCreatorMembershipId() {
return creatorMembershipId;
}
/**
* Returns current assignment attribution.
*
* @return membership identifier responsible for the current assignment
*/
public long getAssignerMembershipId() {
return assignerMembershipId;
}
/**
* Returns lifecycle visibility state.
*
* @return soft-deletion instant, or {@code null} while current
*/
public Instant getDeletedAt() {
return deletedAt;
}
/**
* Returns creation timing.
*
* @return immutable creation instant
*/
public Instant getCreatedAt() {
return createdAt;
}
} }
@@ -7,6 +7,9 @@ import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.Instant; import java.time.Instant;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
/** /**
* Persisted append-only Task comment. * Persisted append-only Task comment.
@@ -16,6 +19,8 @@ import java.time.Instant;
*/ */
@Entity @Entity
@Table(name = "task_comments") @Table(name = "task_comments")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TaskComment { public class TaskComment {
@Id @Id
@@ -34,9 +39,6 @@ public class TaskComment {
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false)
private Instant createdAt; private Instant createdAt;
/** Constructor reserved for JPA materialization. */
protected TaskComment() {}
/** /**
* Creates an immutable comment from server-authorized values. * Creates an immutable comment from server-authorized values.
* *
@@ -52,48 +54,4 @@ public class TaskComment {
this.createdAt = createdAt; this.createdAt = createdAt;
} }
/**
* Returns the persistence identity.
*
* @return comment identifier, or {@code null} before insertion
*/
public Long getId() {
return id;
}
/**
* Returns the owning record identity.
*
* @return owning Task identifier
*/
public long getTaskId() {
return taskId;
}
/**
* Returns historical authorship.
*
* @return immutable historical author user identifier
*/
public long getAuthorUserId() {
return authorUserId;
}
/**
* Returns comment content.
*
* @return normalized comment text
*/
public String getBody() {
return body;
}
/**
* Returns creation timing.
*
* @return immutable creation instant
*/
public Instant getCreatedAt() {
return createdAt;
}
} }
@@ -11,6 +11,7 @@ import com.lab.labtimesheet.feature.task.repository.TaskRepository;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -23,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional;
* priorities cover current assignments only where the actor still has an active membership. * priorities cover current assignments only where the actor still has an active membership.
*/ */
@Service @Service
@RequiredArgsConstructor
public class TaskDashboardService { public class TaskDashboardService {
private static final TaskDashboardView EMPTY_DASHBOARD = new TaskDashboardView(0, 0, List.of()); private static final TaskDashboardView EMPTY_DASHBOARD = new TaskDashboardView(0, 0, List.of());
@@ -30,17 +32,6 @@ public class TaskDashboardService {
private final TaskRepository tasks; private final TaskRepository tasks;
private final ProjectQueryService projects; private final ProjectQueryService projects;
/**
* Creates the dashboard query service.
*
* @param tasks Task persistence boundary
* @param projects authorized Project query boundary
*/
public TaskDashboardService(TaskRepository tasks, ProjectQueryService projects) {
this.tasks = tasks;
this.projects = projects;
}
/** /**
* Builds the role-scoped Task dashboard for one authenticated account. * Builds the role-scoped Task dashboard for one authenticated account.
* *
@@ -2,6 +2,7 @@ package com.lab.labtimesheet.feature.task.service;
import com.lab.labtimesheet.feature.task.repository.TaskRepository; import com.lab.labtimesheet.feature.task.repository.TaskRepository;
import java.util.Set; import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -9,19 +10,11 @@ import org.springframework.transaction.annotation.Transactional;
* Public Task query boundary used by other features without exposing Task entities or repositories. * Public Task query boundary used by other features without exposing Task entities or repositories.
*/ */
@Service @Service
@RequiredArgsConstructor
public class TaskQueryService { public class TaskQueryService {
private final TaskRepository tasks; private final TaskRepository tasks;
/**
* Creates the cross-feature Task query service.
*
* @param tasks Task persistence boundary
*/
public TaskQueryService(TaskRepository tasks) {
this.tasks = tasks;
}
/** /**
* Counts current Tasks assigned outside the supplied active membership set. * Counts current Tasks assigned outside the supplied active membership set.
* *
@@ -30,6 +30,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -42,6 +43,7 @@ import org.springframework.transaction.annotation.Transactional;
* to a non-disclosing Task 404, while authenticated business-rule failures use Task validation. * to a non-disclosing Task 404, while authenticated business-rule failures use Task validation.
*/ */
@Service @Service
@RequiredArgsConstructor
public class TaskService { public class TaskService {
private final TaskRepository tasks; private final TaskRepository tasks;
@@ -51,31 +53,6 @@ public class TaskService {
private final CalendarApplicationService calendar; private final CalendarApplicationService calendar;
private final Clock clock; private final Clock clock;
/**
* Creates the Task application service and its feature boundaries.
*
* @param tasks Task persistence boundary
* @param comments append-only comment persistence boundary
* @param projects authorized Project read boundary
* @param projectMutations Project-first locking mutation boundary
* @param calendar authoritative global day-off query boundary
* @param clock server time source for persisted instants
*/
public TaskService(
TaskRepository tasks,
TaskCommentRepository comments,
ProjectQueryService projects,
ProjectService projectMutations,
CalendarApplicationService calendar,
Clock clock) {
this.tasks = tasks;
this.comments = comments;
this.projects = projects;
this.projectMutations = projectMutations;
this.calendar = calendar;
this.clock = clock;
}
/** /**
* Creates a TODO Task in a PLANNED or ACTIVE Project. * Creates a TODO Task in a PLANNED or ACTIVE Project.
* *