test(attendance): guard component method surfaces

This commit is contained in:
sechmachine
2026-08-15 11:49:53 +07:00
parent 9023062477
commit 5ddbd75f07
2 changed files with 127 additions and 13 deletions
@@ -6,7 +6,8 @@
- **Test class/method:** - **Test class/method:**
`com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#generatedConstructorsPreserveParameterListsAndVisibility`, `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#generatedConstructorsPreserveParameterListsAndVisibility`,
`com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#immutableModelsRemainRecordsWithTheirComponentContracts`, `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#immutableModelsRemainRecordsWithTheirComponentContracts`,
`com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#entitiesExposeOnlyIntentionalPublicAndProtectedDeclaredMethods` `com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#entitiesExposeOnlyIntentionalPublicAndProtectedDeclaredMethods`,
`com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#componentsExposeOnlyIntentionalPublicAndProtectedDeclaredMethods`
- **Implementation commit:** `82ad8202fd31f77db8c3932a902dba07cee70894` - **Implementation commit:** `82ad8202fd31f77db8c3932a902dba07cee70894`
## Protected behavior ## Protected behavior
@@ -21,7 +22,8 @@ Reflection inspects compiled `feature.attendance` classes rather than source spe
parameter order and modifier, every immutable model's record components, and the exact public/protected declared method parameter order and modifier, every immutable model's record components, and the exact public/protected declared method
surface of each Attendance entity. The entity surface prevents generated bean getters/setters or entity surface of each Attendance entity. The entity surface prevents generated bean getters/setters or entity
`equals`/`hashCode`/`toString` widening while explicitly retaining `AttendanceRecordEntity#setCheckOutAt` and the `equals`/`hashCode`/`toString` widening while explicitly retaining `AttendanceRecordEntity#setCheckOutAt` and the
`LeaveRequestDayId` identity methods. `LeaveRequestDayId` identity methods. Exact controller and service surfaces likewise prevent Lombok from exposing
collaborator getters/setters or generated `equals`/`hashCode`/`toString` methods.
## Hand-derived expected result ## Hand-derived expected result
@@ -29,7 +31,8 @@ Five injection-only components expose only their package-scoped dependency const
retain protected no-argument construction alongside their intentional domain constructors, and the stateless domain retain protected no-argument construction alongside their intentional domain constructors, and the stateless domain
service remains package-scoped. Seven immutable models remain records with the same component order and types. Entity service remains package-scoped. Seven immutable models remain records with the same component order and types. Entity
method surfaces contain only intentional domain conversion/access/mutation methods; only the composite key owns method surfaces contain only intentional domain conversion/access/mutation methods; only the composite key owns
`equals` and `hashCode`, and no Attendance entity declares `toString`. `equals` and `hashCode`, and no Attendance entity declares `toString`. Both controllers and all four Attendance
services expose only their existing route or application/domain operations, never their injected collaborators.
## RED ## RED
@@ -66,7 +69,7 @@ export PATH="$JAVA_HOME/bin:$PATH"
**Observed result** **Observed result**
```text ```text
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS BUILD SUCCESS
Process exited 0. Process exited 0.
``` ```
@@ -80,7 +83,7 @@ export JAVA_HOME=/opt/homebrew/opt/openjdk@25
export PATH="$JAVA_HOME/bin:$PATH" export PATH="$JAVA_HOME/bin:$PATH"
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
./mvnw -Dtest='*Attendance*Test' test ./mvnw -Dtest='*Attendance*Test' test
Tests run: 38, Failures: 0, Errors: 0, Skipped: 0 Tests run: 39, Failures: 0, Errors: 0, Skipped: 0
PostgreSQL 18.4 started and Flyway applied V1 for the persistence and concurrency contexts. PostgreSQL 18.4 started and Flyway applied V1 for the persistence and concurrency contexts.
BUILD SUCCESS BUILD SUCCESS
Process exited 0. Process exited 0.
@@ -98,14 +101,6 @@ Additional verification on the same source tree:
./mvnw -DskipTests compile ./mvnw -DskipTests compile
BUILD SUCCESS BUILD SUCCESS
./mvnw -Dtest=AttendanceLombokBoilerplateTest,AttendanceLayerStructureTest,AttendancePolicyTest,AttendanceServiceTest,AttendanceApplicationServiceTest,AttendanceControllerTest test
Tests run: 27, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
./mvnw -Dtest=AttendancePersistenceIntegrationTest,AttendanceConcurrencyIntegrationTest test
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
PostgreSQL 18.4; BUILD SUCCESS
./mvnw -q -DskipTests compile dependency:build-classpath -Dmdep.outputFile=target/attendance-javadoc-classpath.txt ./mvnw -q -DskipTests compile dependency:build-classpath -Dmdep.outputFile=target/attendance-javadoc-classpath.txt
javadoc -quiet -Xdoclint:all -d target/attendance-javadocs -classpath "target/classes:$(tr -d '\n' < target/attendance-javadoc-classpath.txt)" -sourcepath src/main/java -subpackages com.lab.labtimesheet.feature.attendance javadoc -quiet -Xdoclint:all -d target/attendance-javadocs -classpath "target/classes:$(tr -d '\n' < target/attendance-javadoc-classpath.txt)" -sourcepath src/main/java -subpackages com.lab.labtimesheet.feature.attendance
Process exited 0. The source frontend reported seven generated-constructor missing-comment warnings because it does not Process exited 0. The source frontend reported seven generated-constructor missing-comment warnings because it does not
@@ -11,6 +11,7 @@ import com.lab.labtimesheet.feature.attendance.model.AttendancePolicy;
import com.lab.labtimesheet.feature.attendance.model.AttendanceRecord; import com.lab.labtimesheet.feature.attendance.model.AttendanceRecord;
import com.lab.labtimesheet.feature.attendance.model.AttendanceRole; import com.lab.labtimesheet.feature.attendance.model.AttendanceRole;
import com.lab.labtimesheet.feature.attendance.model.AttendanceViolations; import com.lab.labtimesheet.feature.attendance.model.AttendanceViolations;
import com.lab.labtimesheet.feature.attendance.model.dto.AttendanceCurrentState;
import com.lab.labtimesheet.feature.attendance.model.dto.AttendanceHistoryItem; import com.lab.labtimesheet.feature.attendance.model.dto.AttendanceHistoryItem;
import com.lab.labtimesheet.feature.attendance.model.dto.GlobalCalendarEvent; import com.lab.labtimesheet.feature.attendance.model.dto.GlobalCalendarEvent;
import com.lab.labtimesheet.feature.attendance.model.entity.AttendancePolicyEntity; import com.lab.labtimesheet.feature.attendance.model.entity.AttendancePolicyEntity;
@@ -31,6 +32,7 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.security.Principal;
import java.time.Clock; import java.time.Clock;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
@@ -38,8 +40,11 @@ import java.time.LocalTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.ui.Model;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
class AttendanceLombokBoilerplateTest { class AttendanceLombokBoilerplateTest {
@@ -213,6 +218,120 @@ class AttendanceLombokBoilerplateTest {
assertMethodSurface(LeaveRequestEntity.class); assertMethodSurface(LeaveRequestEntity.class);
} }
@Test
void componentsExposeOnlyIntentionalPublicAndProtectedDeclaredMethods() {
assertMethodSurface(
AttendanceController.class,
method(
Modifier.PUBLIC,
"ownHistory",
String.class,
Principal.class,
LocalDate.class,
LocalDate.class,
Model.class),
method(
Modifier.PUBLIC,
"inspectHistory",
String.class,
Principal.class,
long.class,
LocalDate.class,
LocalDate.class,
Model.class),
method(
Modifier.PUBLIC,
"checkIn",
String.class,
Principal.class,
RedirectAttributes.class),
method(
Modifier.PUBLIC,
"checkOut",
String.class,
Principal.class,
RedirectAttributes.class));
assertMethodSurface(
CalendarController.class,
method(Modifier.PUBLIC, "calendar", String.class, Principal.class, Model.class),
method(
Modifier.PUBLIC,
"create",
String.class,
Principal.class,
LocalDate.class,
String.class,
boolean.class,
RedirectAttributes.class),
method(
Modifier.PUBLIC,
"update",
String.class,
Principal.class,
long.class,
long.class,
LocalDate.class,
String.class,
boolean.class,
RedirectAttributes.class));
assertMethodSurface(
AttendanceApplicationService.class,
method(Modifier.PUBLIC, "checkIn", AttendanceRecord.class, long.class),
method(Modifier.PUBLIC, "checkOut", AttendanceRecord.class, long.class),
method(Modifier.PUBLIC, "currentState", AttendanceCurrentState.class, long.class),
method(
Modifier.PUBLIC,
"history",
List.class,
AttendanceActor.class,
long.class,
LocalDate.class,
LocalDate.class),
method(Modifier.PUBLIC, "currentBusinessDate", LocalDate.class));
assertMethodSurface(
AttendanceCurrentUserService.class,
method(Modifier.PUBLIC, "actor", AttendanceActor.class, Principal.class));
assertMethodSurface(
AttendanceService.class,
method(
Modifier.PUBLIC,
"checkIn",
AttendanceRecord.class,
long.class,
Instant.class,
AttendancePolicy.class,
AttendanceDayContext.class,
Optional.class),
method(
Modifier.PUBLIC,
"checkOut",
AttendanceRecord.class,
Optional.class,
Instant.class));
assertMethodSurface(
CalendarApplicationService.class,
method(
Modifier.PUBLIC,
"createManual",
GlobalCalendarEvent.class,
AttendanceActor.class,
LocalDate.class,
String.class,
boolean.class),
method(
Modifier.PUBLIC,
"updateManual",
GlobalCalendarEvent.class,
AttendanceActor.class,
long.class,
long.class,
LocalDate.class,
String.class,
boolean.class),
method(Modifier.PUBLIC, "list", List.class, LocalDate.class, LocalDate.class),
method(Modifier.PUBLIC, "isGlobalDayOff", boolean.class, LocalDate.class));
}
private static void assertConstructors( private static void assertConstructors(
Class<?> type, ConstructorContract... expectedConstructors) { Class<?> type, ConstructorContract... expectedConstructors) {
List<ConstructorContract> actual = Arrays.stream(type.getDeclaredConstructors()) List<ConstructorContract> actual = Arrays.stream(type.getDeclaredConstructors())