fix(attendance): preserve frozen attendance boundaries
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
# Test Evidence: Frozen leave dates and concurrent punch outcomes
|
||||
|
||||
- **Test type:** Integration
|
||||
- **Requirement IDs:** `ATT-005`, `ATT-006`, `ATT-007`, `ATT-008`, `ATT-010`, `LEV-003`, `LEV-011`
|
||||
- **Scenario IDs:** `AC-ATT-003`, `AC-ATT-004`, `AC-LEV-001`, `AC-LEV-005`
|
||||
- **Test class/method:** `com.lab.labtimesheet.feature.attendance.service.AttendancePersistenceIntegrationTest#approvedLeaveBlocksOnlyItsFrozenAllocatedDates`, `com.lab.labtimesheet.feature.attendance.service.AttendanceConcurrencyIntegrationTest#concurrentDuplicatePunchesReturnStableDomainOutcomes`
|
||||
- **Implementation commit:** `pending`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
Approved leave blocks check-in only on exact immutable `leave_request_days`, not
|
||||
every calendar date inside the request range. Concurrent duplicate punches return
|
||||
stable attendance rejection codes while preserving a single raw check-in and checkout.
|
||||
|
||||
## Test method
|
||||
|
||||
Spring Boot migrates PostgreSQL 18.4, creates an active Intern only through public
|
||||
Account and SMTP services, and persists an Attendance-owned approved leave request
|
||||
plus one frozen allocation through JPA. A separate non-transactional test releases
|
||||
two threads simultaneously against each transactional punch endpoint.
|
||||
|
||||
## Hand-derived expected result
|
||||
|
||||
For an approved 14–17 August range with only 17 August allocated, check-in on
|
||||
14 August succeeds and 17 August returns `APPROVED_LEAVE`. Two simultaneous
|
||||
check-ins produce one success and one `ALREADY_CHECKED_IN`; two simultaneous
|
||||
checkouts produce one success and one `ALREADY_CHECKED_OUT`.
|
||||
|
||||
## RED
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
./mvnw -Dtest=AttendancePersistenceIntegrationTest#approvedLeaveBlocksOnlyItsFrozenAllocatedDates test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
AttendanceException: APPROVED_LEAVE at AttendanceApplicationService.checkIn for
|
||||
the unallocated 2026-08-14 range date.
|
||||
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0
|
||||
BUILD FAILURE
|
||||
Process exited 1 because the query used the whole leave request range.
|
||||
```
|
||||
|
||||
The repository-exception unit regressions separately failed because raw
|
||||
`DataIntegrityViolationException` and `ObjectOptimisticLockingFailureException`
|
||||
escaped the application boundary.
|
||||
|
||||
## GREEN
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
./mvnw -Dtest=AttendanceConcurrencyIntegrationTest test
|
||||
./mvnw -Dtest=AttendancePersistenceIntegrationTest#approvedLeaveBlocksOnlyItsFrozenAllocatedDates test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
AttendanceConcurrencyIntegrationTest: Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||
PostgreSQL reported SQLSTATE 23505 on uq_attendance_records_intern_date; the caller
|
||||
received ALREADY_CHECKED_IN. The checkout race returned ALREADY_CHECKED_OUT.
|
||||
AttendancePersistenceIntegrationTest focused allocation test: Tests run: 1,
|
||||
Failures: 0, Errors: 0, Skipped: 0.
|
||||
BUILD SUCCESS
|
||||
Process exited 0.
|
||||
```
|
||||
|
||||
## Affected suite
|
||||
|
||||
**Command and result**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
./mvnw -Dtest='*Attendance*Test' test
|
||||
Tests run: 32, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Process exited 0.
|
||||
```
|
||||
|
||||
## External-test boundaries
|
||||
|
||||
The tests do not implement the later leave workflow or account terminal-state
|
||||
transitions. They prove the current read/query boundary, exact PostgreSQL 18.4
|
||||
allocation semantics, and duplicate-punch conflict translation.
|
||||
@@ -4,7 +4,7 @@
|
||||
- **Requirement IDs:** `ATT-002`, `ATT-005`, `ATT-007`, `ATT-008`, `ATT-010`, `CAL-001`, `CAL-006`, `CAL-007`, `CAL-009`, `AUTH-003`, `RPT-004`
|
||||
- **Scenario IDs:** `AC-ATT-003`, `AC-ATT-004`, `AC-CAL-003`, `AC-CAL-004`
|
||||
- **Test class/method:** `com.lab.labtimesheet.feature.attendance.service.AttendancePersistenceIntegrationTest`
|
||||
- **Implementation commit:** `pending (committed with this evidence)`
|
||||
- **Implementation commit:** `8b48e281f7e860af435ae35b16c4edeb139286dc`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
@@ -87,7 +87,7 @@ export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
./mvnw -Dtest='*Attendance*Test' test
|
||||
Tests run: 26, Failures: 0, Errors: 0, Skipped: 0
|
||||
Tests run: 32, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Process exited 0.
|
||||
```
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Test Evidence: Checkout eligibility and stable conflict outcomes
|
||||
|
||||
- **Test type:** Unit
|
||||
- **Requirement IDs:** `ATT-007`, `ATT-008`, `ATT-010`, `ATT-012`
|
||||
- **Scenario IDs:** `AC-ATT-003`, `AC-ATT-004`
|
||||
- **Test class/method:** `com.lab.labtimesheet.feature.attendance.service.AttendanceApplicationServiceTest#rejectsCheckoutWhenInternIsNoLongerEligibleForPersistedWorkDate`, `#translatesConcurrentCheckInUniqueConflictToStableDuplicateRejection`, `#translatesConcurrentCheckoutVersionConflictToStableDuplicateRejection`
|
||||
- **Implementation commit:** `pending`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
Checkout revalidates active internship eligibility for the attendance row's
|
||||
persisted work date. A terminal Intern cannot checkout after checking in.
|
||||
Database uniqueness and optimistic-lock races are translated to stable duplicate
|
||||
punch rejection codes instead of leaking persistence exceptions.
|
||||
|
||||
## Test method
|
||||
|
||||
Plain JUnit and Mockito drive the production transactional application service
|
||||
with a fixed Clock, attached policy, persisted row, AccountService eligibility,
|
||||
and repository exceptions. Account state remains behind its public service API.
|
||||
|
||||
## Hand-derived expected result
|
||||
|
||||
False date-aware eligibility returns `INACTIVE_INTERN` before raw checkout is
|
||||
saved. A check-in uniqueness race returns `ALREADY_CHECKED_IN`; a checkout
|
||||
version race returns `ALREADY_CHECKED_OUT`.
|
||||
|
||||
## RED
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
./mvnw -Dtest=AttendanceApplicationServiceTest#rejectsCheckoutWhenInternIsNoLongerEligibleForPersistedWorkDate test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
Expected AttendanceException(INACTIVE_INTERN) but was NullPointerException after
|
||||
the service continued to save checkout without calling AccountService eligibility.
|
||||
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
|
||||
BUILD FAILURE
|
||||
Process exited 1.
|
||||
```
|
||||
|
||||
The conflict regressions were also observed RED in the combined focused run:
|
||||
|
||||
```text
|
||||
DataIntegrityViolationException: concurrent unique conflict
|
||||
ObjectOptimisticLockingFailureException: optimistic locking failed
|
||||
Both escaped AttendanceApplicationService instead of stable AttendanceException values.
|
||||
```
|
||||
|
||||
## GREEN
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
./mvnw -Dtest=AttendanceApplicationServiceTest test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Process exited 0.
|
||||
```
|
||||
|
||||
## Affected suite
|
||||
|
||||
**Command and result**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
./mvnw -Dtest='*Attendance*Test' test
|
||||
Tests run: 32, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Process exited 0.
|
||||
```
|
||||
|
||||
## External-test boundaries
|
||||
|
||||
The account platform has no Iteration 1 terminal-state mutation API, so the
|
||||
completed/withdrawn state is represented through its public date-aware eligibility
|
||||
result. The companion PostgreSQL concurrency test proves the real unique conflict.
|
||||
@@ -4,7 +4,7 @@
|
||||
- **Requirement IDs:** `ATT-005`, `I1-UI-03`
|
||||
- **Scenario IDs:** `I1-ATT-03`, `I1-ATT-04`
|
||||
- **Test class/method:** `com.lab.labtimesheet.feature.attendance.service.AttendanceApplicationServiceTest`
|
||||
- **Implementation commit:** `pending (committed with this evidence)`
|
||||
- **Implementation commit:** `8b48e281f7e860af435ae35b16c4edeb139286dc`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
@@ -70,7 +70,7 @@ export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
./mvnw -Dtest='*Attendance*Test' test
|
||||
Tests run: 26, Failures: 0, Errors: 0, Skipped: 0
|
||||
Tests run: 32, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Process exited 0.
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- **Requirement IDs:** `ARC-005`, `OPS-020`
|
||||
- **Scenario IDs:** `I1-ATT-01` through `I1-ATT-05` structural gate
|
||||
- **Test class/method:** `com.lab.labtimesheet.architecture.AttendanceLayerStructureTest`
|
||||
- **Implementation commit:** `pending (committed with this evidence)`
|
||||
- **Implementation commit:** `8b48e281f7e860af435ae35b16c4edeb139286dc`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
@@ -90,7 +90,7 @@ export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
./mvnw -Dtest='*Attendance*Test' test
|
||||
Tests run: 26, Failures: 0, Errors: 0, Skipped: 0
|
||||
Tests run: 32, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Process exited 0.
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- **Requirement IDs:** `ATT-001`, `ATT-002`, `ATT-003`, `ATT-004`
|
||||
- **Scenario IDs:** `AC-ATT-001`
|
||||
- **Test class/method:** `com.lab.labtimesheet.feature.attendance.model.AttendancePolicyTest`
|
||||
- **Implementation commit:** `pending (committed with this evidence)`
|
||||
- **Implementation commit:** `71901d1670f633a1b594bdce3348efebe73fc175`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
@@ -66,7 +66,7 @@ Process exited 0.
|
||||
|
||||
```text
|
||||
./mvnw -Dtest='*Attendance*Test' test
|
||||
Tests run: 26, Failures: 0, Errors: 0, Skipped: 0
|
||||
Tests run: 32, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Process exited 0.
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- **Requirement IDs:** `GOV-011`, `GOV-012`, `ATT-005`, `ATT-007`, `ATT-008`, `ATT-009`, `ATT-010`, `ATT-011`, `ATT-012`, `ATT-016`
|
||||
- **Scenario IDs:** `AC-ATT-002`, `AC-ATT-003`, `AC-ATT-004`, `AC-ATT-005`
|
||||
- **Test class/method:** `com.lab.labtimesheet.feature.attendance.service.AttendanceServiceTest`
|
||||
- **Implementation commit:** `pending (committed with this evidence)`
|
||||
- **Implementation commit:** `71901d1670f633a1b594bdce3348efebe73fc175`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
@@ -71,7 +71,7 @@ Process exited 0.
|
||||
|
||||
```text
|
||||
./mvnw -Dtest='*Attendance*Test' test
|
||||
Tests run: 26, Failures: 0, Errors: 0, Skipped: 0
|
||||
Tests run: 32, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Process exited 0.
|
||||
```
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
# Test Evidence: Attendance and global-calendar web authorization
|
||||
|
||||
- **Test type:** Web
|
||||
- **Requirement IDs:** `AUTH-001`, `AUTH-002`, `AUTH-003`, `ATT-007`, `ATT-010`, `CAL-001`, `CAL-007`, `RPT-004`
|
||||
- **Requirement IDs:** `AUTH-001`, `AUTH-002`, `AUTH-003`, `ATT-007`, `ATT-010`, `ATT-016`, `CAL-001`, `CAL-007`, `RPT-004`, `UI-013`
|
||||
- **Scenario IDs:** `AC-ATT-003`, `AC-ATT-004`, `AC-CAL-004`
|
||||
- **Test class/method:** `com.lab.labtimesheet.feature.attendance.controller.AttendanceControllerTest`
|
||||
- **Implementation commit:** `pending (committed with this evidence)`
|
||||
- **Implementation commit:** `8b48e281f7e860af435ae35b16c4edeb139286dc`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
Authenticated Intern punch routes use the server-resolved user ID, own history
|
||||
renders attached policy details, Mentor inspection routes preserve the target
|
||||
scope, and calendar management rejects non-Admin access. Calendar updates carry
|
||||
the submitted optimistic version.
|
||||
the submitted optimistic version. History renders policy-local 24-hour times,
|
||||
`dd/MM/yyyy` dates, and every simultaneous violation; `On time` appears only
|
||||
when no violation applies.
|
||||
|
||||
## Test method
|
||||
|
||||
`@WebMvcTest` runs Spring Security filters, CSRF protection, MVC binding, route
|
||||
selection, controller authorization, Thymeleaf rendering, and service-call
|
||||
arguments while mocking only application-service and current-user boundaries.
|
||||
The presentation regression supplies a row that is both late and early and
|
||||
asserts the attached Asia/Ho_Chi_Minh timezone conversion.
|
||||
|
||||
## Hand-derived expected result
|
||||
|
||||
An Intern authenticated as user 42 can punch only ID 42. A Mentor can inspect
|
||||
target 42 but receives HTTP 403 for Admin calendar management. Attached policy
|
||||
grace renders as `30 min`. An event form with version 3 calls update with 3.
|
||||
`2026-08-14T02:00:00.001Z` renders as local `09:00`, and a 15:00 local checkout
|
||||
on that late row renders both `Late` and `Early departure`, never `On time`.
|
||||
|
||||
## RED
|
||||
|
||||
@@ -46,6 +52,20 @@ export PATH="$JAVA_HOME/bin:$PATH"
|
||||
Process exited 1 because the required authenticated web endpoints did not exist.
|
||||
```
|
||||
|
||||
The review presentation regression was separately observed RED:
|
||||
|
||||
```text
|
||||
./mvnw -Dtest=AttendanceApplicationServiceTest,AttendanceControllerTest test
|
||||
AttendanceControllerTest.historyRendersPolicyLocalDisplayValuesAndEveryViolation:
|
||||
Expected a string containing "14/08/2026" but rendered "2026-08-14";
|
||||
the same row rendered one nested-ternary result, "Early departure", and raw UTC instants.
|
||||
Tests run: 13, Failures: 3, Errors: 1, Skipped: 0
|
||||
BUILD FAILURE
|
||||
Process exited 1. The eligible behavioral failures were the missing local presentation
|
||||
values and simultaneous violation output; the checkout fixture error was corrected
|
||||
before its own focused RED and is not claimed as behavioral evidence.
|
||||
```
|
||||
|
||||
## GREEN
|
||||
|
||||
**Command**
|
||||
@@ -59,7 +79,7 @@ export PATH="$JAVA_HOME/bin:$PATH"
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
|
||||
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Process exited 0.
|
||||
```
|
||||
@@ -73,7 +93,7 @@ export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
./mvnw -Dtest='*Attendance*Test' test
|
||||
Tests run: 26, Failures: 0, Errors: 0, Skipped: 0
|
||||
Tests run: 32, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Process exited 0.
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user