Compare commits
21
Commits
b9b150ff8c
...
b764707716
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b764707716 | ||
|
|
6725f6a97d | ||
|
|
daa97aef71 | ||
|
|
73b57360a6 | ||
|
|
f93ced67b2 | ||
|
|
c949973282 | ||
|
|
5ddbd75f07 | ||
|
|
587127100d | ||
|
|
9023062477 | ||
|
|
469d7e274b | ||
|
|
41448903aa | ||
|
|
8500cb6eb7 | ||
|
|
4b1e5721e4 | ||
|
|
e5639c1461 | ||
|
|
baa2752466 | ||
|
|
382fa40101 | ||
|
|
7c1a26d77f | ||
|
|
abc5208d67 | ||
|
|
82ad8202fd | ||
|
|
1eb995500c | ||
|
|
e2b206c27e |
@@ -0,0 +1,108 @@
|
||||
# Test Evidence: Attendance targeted Lombok boilerplate retrofit
|
||||
|
||||
- **Test type:** Unit compiled-contract audit
|
||||
- **Requirement IDs:** `ATT-001`–`ATT-012`, `CAL-001`, `CAL-006`–`CAL-009`
|
||||
- **Scenario IDs:** `AC-ATT-001`–`AC-ATT-005`, `AC-CAL-003`, `AC-CAL-004`
|
||||
- **Test class/method:**
|
||||
`com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#generatedConstructorsPreserveParameterListsAndVisibility`,
|
||||
`com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#immutableModelsRemainRecordsWithTheirComponentContracts`,
|
||||
`com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#entitiesExposeOnlyIntentionalPublicAndProtectedDeclaredMethods`,
|
||||
`com.lab.labtimesheet.feature.attendance.AttendanceLombokBoilerplateTest#componentsExposeOnlyIntentionalPublicAndProtectedDeclaredMethods`
|
||||
- **Implementation commit:** `82ad8202fd31f77db8c3932a902dba07cee70894`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
Attendance uses the installed Lombok processor only for mechanical constructors while preserving the compiled API:
|
||||
package-level Spring injection, protected JPA construction, immutable record components, domain constructors and
|
||||
mutations, raw punch and attached-policy history rules, composite-key identity, and existing public method names.
|
||||
|
||||
## Test method
|
||||
|
||||
Reflection inspects compiled `feature.attendance` classes rather than source spelling. It verifies every constructor's
|
||||
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
|
||||
`equals`/`hashCode`/`toString` widening while explicitly retaining `AttendanceRecordEntity#setCheckOutAt` and the
|
||||
`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
|
||||
|
||||
Five injection-only components expose only their package-scoped dependency constructors. Six JPA/embeddable types
|
||||
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
|
||||
method surfaces contain only intentional domain conversion/access/mutation methods; only the composite key owns
|
||||
`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
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
./mvnw -Dtest=AttendanceLombokBoilerplateTest test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
Tests run: 3, Failures: 2, Errors: 0, Skipped: 0
|
||||
The initial source audit first failed on AttendanceController because its mechanical dependency constructor remained,
|
||||
and on AttendancePolicyEntity because its mechanical protected JPA constructor remained. The immutable-record and
|
||||
business-method retention guard passed. After this RED established the retrofit gap, the permanent regression was
|
||||
replaced with compiled reflection/API checks so formatting or annotation spelling cannot affect the result.
|
||||
BUILD FAILURE
|
||||
Process exited 1.
|
||||
```
|
||||
|
||||
## GREEN
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
./mvnw -Dtest=AttendanceLombokBoilerplateTest test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
Tests run: 4, 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: 39, Failures: 0, Errors: 0, Skipped: 0
|
||||
PostgreSQL 18.4 started and Flyway applied V1 for the persistence and concurrency contexts.
|
||||
BUILD SUCCESS
|
||||
Process exited 0.
|
||||
```
|
||||
|
||||
## External-test boundaries
|
||||
|
||||
The reflection audit does not replace Spring/JPA bootstrapping, MVC property access, PostgreSQL persistence, or
|
||||
Javadoc/doclint. Those checks remain affected verification. No application behavior or public API is intentionally
|
||||
changed by this retrofit.
|
||||
|
||||
Additional verification on the same source tree:
|
||||
|
||||
```text
|
||||
./mvnw -DskipTests compile
|
||||
BUILD SUCCESS
|
||||
|
||||
./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
|
||||
Process exited 0. The source frontend reported seven generated-constructor missing-comment warnings because it does not
|
||||
expand Lombok constructors; repository policy exempts generated trivial constructors from duplicate Javadoc.
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
# Test Evidence: Platform Lombok boilerplate retrofit
|
||||
|
||||
- **Test type:** Unit
|
||||
- **Requirement IDs:** `Engineering policy — targeted Lombok retrofit`
|
||||
- **Scenario IDs:** `Source-audit RED/GREEN`
|
||||
- **Test class/method:** `N/A — reproducible source audit; behavior is covered by the affected suites below`
|
||||
- **Implementation commit:** `41448903aa924dc5852db8d7bb4d9319cb9f91a7`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
Platform-owned Spring collaborators, request forms, and JPA entities must not retain eligible handwritten
|
||||
dependency-assignment constructors, trivial accessors, or empty persistence constructors. The retrofit must preserve
|
||||
constructor visibility, form normalization, entity encapsulation, defensive copies of credential/token bytes, and all
|
||||
account, authentication, SMTP, and cross-feature behavior.
|
||||
|
||||
## Test method
|
||||
|
||||
The source audit searches only the 79 members classified as mechanical after reading every root/config,
|
||||
`feature.account`, and `feature.integration` production type. It deliberately excludes normalized email/display-name
|
||||
setters, defensive byte-array getters, domain constructors and factories, state transitions, the normalized
|
||||
`AccountService` public-origin constructor, `SecretCipher` key construction, and the two-constructor JavaMail test
|
||||
seam. No permanent annotation-presence test was added because annotations are an implementation detail; compilation
|
||||
and production-shaped tests protect the real contracts.
|
||||
|
||||
## Hand-derived expected result
|
||||
|
||||
Before the retrofit the audit must find 79 eligible handwritten members and exit 1. After targeted Lombok generation,
|
||||
the same audit must find none and exit 0, while the retained non-mechanical members remain explicit.
|
||||
|
||||
## RED
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
matches=$( { rg -n '^ (public )?(AccountController|BootstrapAccessFilter|BootstrapController|BootstrapService|DatabaseUserDetailsService|SmtpController|SmtpWarningAdvice|MailDeliveryService|SmtpConfigurationService)\(' src/main/java/com/lab/labtimesheet/feature/account src/main/java/com/lab/labtimesheet/feature/integration; rg -n '^ public (String getMasterKey|void setMasterKey)\(' src/main/java/com/lab/labtimesheet/config/SecurityProperties.java; rg -n '^ public (String get(Token|Password|ConfirmPassword)|void set(Token|Password|ConfirmPassword))\(' src/main/java/com/lab/labtimesheet/feature/account/model/dto/ActivationForm.java; rg -n '^ public (String get(Email|DisplayName|Password)|void setPassword)\(' src/main/java/com/lab/labtimesheet/feature/account/model/dto/BootstrapForm.java; rg -n '^ public .+ (get(Email|DisplayName|Role|StudentCode|InternshipStart|InternshipEnd)|set(Role|StudentCode|InternshipStart|InternshipEnd))\(' src/main/java/com/lab/labtimesheet/feature/account/model/dto/CreateAccountForm.java; rg -n '^ public (Long getDraftId|void setDraftId)\(' src/main/java/com/lab/labtimesheet/feature/integration/model/dto/SmtpActionForm.java; rg -n '^ public .+ (get(Host|Port|SecurityMode|Username|Password|FromAddress|FromName)|set(Host|Port|SecurityMode|Username|Password|FromAddress|FromName))\(' src/main/java/com/lab/labtimesheet/feature/integration/model/dto/SmtpForm.java; rg -n '^ protected (AppUser|InternProfile|SystemState|UserActionToken|SmtpConfiguration)\(\)' src/main/java/com/lab/labtimesheet/feature/account/model/entity src/main/java/com/lab/labtimesheet/feature/integration/model/entity; rg -n '^ public .+ (get(Id|Email|DisplayName|PasswordHash|GlobalRole|AccountStatus|ActivatedAt|InternshipStatus|InternshipStartDate|InternshipEndDate|UserId|Purpose|ExpiresAt|UsedAt|InvalidatedAt|Status|Host|Port|SecurityMode|Username|SecretKeyVersion|FromAddress|FromName|TestedAt)|isInitialized)\(' src/main/java/com/lab/labtimesheet/feature/account/model/entity src/main/java/com/lab/labtimesheet/feature/integration/model/entity; } ); if [ -n "$matches" ]; then printf '%s\n' "$matches"; printf 'RED: eligible handwritten Lombok boilerplate remains (%s matches)\n' "$(printf '%s\n' "$matches" | wc -l | tr -d ' ')"; exit 1; fi; printf 'GREEN: no eligible handwritten Lombok boilerplate remains\n'
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
RED: eligible handwritten Lombok boilerplate remains (79 matches)
|
||||
Process exited with code 1 because the confirmed mechanical members were still handwritten.
|
||||
```
|
||||
|
||||
## GREEN
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
The exact RED source-audit command above was repeated without alteration.
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
GREEN: no eligible handwritten Lombok boilerplate remains
|
||||
Process exited with code 0.
|
||||
```
|
||||
|
||||
## Affected suite
|
||||
|
||||
**Command and result**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
./mvnw -DskipTests compile
|
||||
BUILD SUCCESS — 127 production source files compiled on Java 25.
|
||||
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
./mvnw -Dtest=LabtimesheetApplicationTests,LayerStructureTest,PlatformFoundationTest,TimeConfigurationTest,SecurityResponseIntegrationTest,AccountWebIntegrationTest,AuthenticationWebIntegrationTest,BootstrapOnboardingWebIntegrationTest,AccountActivationIntegrationTest,BootstrapIntegrationTest,SmtpOnboardingWebIntegrationTest,JavaMailSmtpProbeTest,SmtpIntegrationTest test
|
||||
BUILD SUCCESS — Tests run: 29, Failures: 0, Errors: 0, Skipped: 0; PostgreSQL 18.4.
|
||||
|
||||
./mvnw test
|
||||
BUILD SUCCESS — Tests run: 197, Failures: 0, Errors: 0, Skipped: 0; PostgreSQL 18.4.
|
||||
|
||||
./mvnw -DskipTests -Ddoclint=all javadoc:javadoc
|
||||
BUILD SUCCESS — doclint reported no errors; Maven emitted 66 non-fatal missing-comment warnings across the integrated
|
||||
tree, including generated default constructors/accessors.
|
||||
|
||||
git diff --check
|
||||
No output; exit 0.
|
||||
```
|
||||
|
||||
## External-test boundaries
|
||||
|
||||
This source audit does not prove Lombok internals or enforce a preferred annotation spelling. The compile and
|
||||
PostgreSQL-backed affected/full suites prove generated constructor/accessor compatibility with Spring binding,
|
||||
Security, JPA/Hibernate, Thymeleaf, and existing cross-feature consumers. No dependency, schema, migration, token,
|
||||
credential, template, container, CI, or runtime configuration was changed.
|
||||
@@ -0,0 +1,117 @@
|
||||
# Test Evidence: Targeted Project Lombok boilerplate
|
||||
|
||||
- **Test type:** Temporary source audit (removed after GREEN)
|
||||
- **Requirement IDs:** `ARC-002`, `ARC-005`, `ARC-006`, `TST-001`
|
||||
- **Scenario IDs:** `AC-TST-001`
|
||||
- **Test class/method:** N/A; the temporary source audit was removed after its RED/GREEN cycle
|
||||
- **Implementation commit:** `e5639c1`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
Project Spring components use targeted required-argument constructor generation only when their
|
||||
constructors assign required final dependencies. Project JPA entities use only protected no-arg
|
||||
constructor generation. Records remain records, and entity identity, lazy associations, explicit
|
||||
domain accessors, aggregate constructors, and mutation methods do not gain broad generated APIs.
|
||||
|
||||
## Temporary RED/GREEN method
|
||||
|
||||
The temporary source-contract test inspected only `feature.project` production sources. It required
|
||||
`@RequiredArgsConstructor` on the three injection-only components, removal of the stateless advice's
|
||||
handwritten no-arg constructor, and protected `@NoArgsConstructor` on the three JPA entities. It also
|
||||
rejected broad entity Lombok annotations, confirmed representative explicit domain APIs remained,
|
||||
and verified every Project immutable DTO/value type remained a Java record. It was deleted after
|
||||
preserving the historical RED/GREEN below because exact imports, annotation spelling, and source
|
||||
substrings are implementation details rather than a durable public contract.
|
||||
|
||||
## Hand-derived expected result
|
||||
|
||||
Seven handwritten constructors are mechanical and eligible for removal: three dependency-assignment
|
||||
constructors, one empty advice constructor, and three empty protected JPA constructors. The three
|
||||
entity domain constructors, all aggregate mutation methods, defensive-copy accessors, derived
|
||||
membership/leadership accessors, validation constructors, exception constructors, and all thirteen
|
||||
records must remain explicit or remain records because they carry behavior or preserve the existing
|
||||
API shape.
|
||||
|
||||
## RED
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||
./mvnw -Dtest=ProjectLombokBoilerplateTest test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
[INFO] Running com.lab.labtimesheet.feature.project.repository.ProjectLombokBoilerplateTest
|
||||
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
|
||||
ProjectLombokBoilerplateTest.eligibleConstructorsUseTargetedLombokWithoutChangingDomainApis
|
||||
expected ProjectController.java to contain import lombok.RequiredArgsConstructor; and
|
||||
@RequiredArgsConstructor, but neither was present and the handwritten assignment-only constructor
|
||||
remained.
|
||||
[INFO] BUILD FAILURE
|
||||
```
|
||||
|
||||
## GREEN
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||
./mvnw -Dtest=ProjectLombokBoilerplateTest test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
[INFO] Running com.lab.labtimesheet.feature.project.repository.ProjectLombokBoilerplateTest
|
||||
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO] BUILD SUCCESS
|
||||
```
|
||||
|
||||
## Affected suite
|
||||
|
||||
**Command and result**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||
./mvnw -DskipTests compile
|
||||
[INFO] BUILD SUCCESS
|
||||
|
||||
./mvnw -Dtest=ProjectEntityTest,ProjectPersistenceStructureTest,ProjectTaskMutationContextTest test
|
||||
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO] BUILD SUCCESS
|
||||
|
||||
./mvnw -Dtest=ProjectControllerTest test
|
||||
[INFO] Tests run: 16, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO] BUILD SUCCESS
|
||||
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
./mvnw -Dtest=ProjectServiceIntegrationTest test
|
||||
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO] BUILD SUCCESS
|
||||
|
||||
./mvnw -Dtest='Project*Test' test
|
||||
[INFO] Tests run: 44, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO] BUILD SUCCESS
|
||||
|
||||
./mvnw -DskipTests -Dmaven.javadoc.failOnWarnings=true -Ddoclint=all \
|
||||
-Dsubpackages=com.lab.labtimesheet.feature.project javadoc:javadoc
|
||||
[INFO] BUILD SUCCESS
|
||||
|
||||
git diff --check
|
||||
(no output; exit 0)
|
||||
```
|
||||
|
||||
## External-test boundaries
|
||||
|
||||
The removed source audit did not prove Lombok annotation processing, Spring constructor injection,
|
||||
Hibernate materialization, PostgreSQL mappings, Thymeleaf behavior, Project authorization, locking,
|
||||
or aggregate lifecycle rules. Those durable boundaries are covered by the compile, scoped
|
||||
Javadoc/doclint, Project unit/web, and PostgreSQL 18.4 integration gates above. The first sandboxed
|
||||
unit-suite attempt could not attach Mockito's Byte Buddy agent; the unchanged command passed after
|
||||
approved execution outside that sandbox. Browser E2E behavior remains outside this unit milestone.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Test Evidence: Reporting Lombok boilerplate boundary
|
||||
|
||||
- **Test type:** Unit
|
||||
- **Requirement IDs:** `ARC-005`, `OPS-019`, `OPS-021`
|
||||
- **Scenario IDs:** `N/A — user-directed behavior-preserving refactor with no product acceptance scenario`
|
||||
- **Test class/method:** Temporary executable source audit plus stable Reporting compile/behavior/architecture suites
|
||||
- **Implementation commit:** `e2b206c27e494fbcd0cc3ed99ff8c2c470285f9e`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
Reporting uses Lombok only for constructors that mechanically assign required Spring dependencies. Immutable dashboard DTOs remain records, the access-denied exception keeps its explicit superclass constructor, and the attendance-state presentation contract keeps its explicit fluent `label()` method.
|
||||
|
||||
## Test method
|
||||
|
||||
The narrow source audit reads only the four Reporting production sources. It requires `@RequiredArgsConstructor` and removal of the two injection-only constructors while positively checking the deliberately retained records, exception constructor, and presentation method. A temporary JUnit source test established RED and passed GREEN, then was removed because retaining exact source-string assertions would couple the suite to implementation details. Existing unit, MockMvc, and architecture tests compile and exercise the generated constructor API.
|
||||
|
||||
## Hand-derived expected result
|
||||
|
||||
`DashboardController` and `DashboardService` each contain final injected dependencies and constructors that only assign those fields, so both are eligible for `@RequiredArgsConstructor`. `DashboardAccessDeniedException(String)` must call its superclass and exposes a documented public error contract. The dashboard projections are already concise immutable records. `AttendanceState.label()` intentionally exposes a fluent presentation API rather than Lombok's default `getLabel()` shape.
|
||||
|
||||
## RED
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||
./mvnw -Dtest=ReportingLombokBoilerplateTest test
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
|
||||
ReportingLombokBoilerplateTest failed because DashboardController did not contain
|
||||
import lombok.RequiredArgsConstructor; and still had its handwritten injection constructor.
|
||||
BUILD FAILURE
|
||||
Total time: 4.638 s
|
||||
```
|
||||
|
||||
## GREEN
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||
./mvnw -Dtest=ReportingLombokBoilerplateTest test
|
||||
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Total time: 4.528 s
|
||||
|
||||
test "$(rg -l '@RequiredArgsConstructor' \
|
||||
src/main/java/com/lab/labtimesheet/feature/reporting/controller/DashboardController.java \
|
||||
src/main/java/com/lab/labtimesheet/feature/reporting/service/DashboardService.java | wc -l | tr -d ' ')" = "2"
|
||||
! rg -n 'public (DashboardController|DashboardService)\\(' \
|
||||
src/main/java/com/lab/labtimesheet/feature/reporting/controller/DashboardController.java \
|
||||
src/main/java/com/lab/labtimesheet/feature/reporting/service/DashboardService.java
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
The temporary source audit passed 1/1 after both conversions. The final shell source audit exited 0: both eligible classes carry `@RequiredArgsConstructor`, and neither handwritten injection-only constructor remains.
|
||||
```
|
||||
|
||||
## Affected suite
|
||||
|
||||
**Command and result**
|
||||
|
||||
```text
|
||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||
|
||||
./mvnw -Dtest=ReportingArchitectureTest,DashboardServiceTest,DashboardControllerWebTest test
|
||||
Tests run: 13, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Total time: 5.952 s
|
||||
|
||||
./mvnw -Dtest=ReportingArchitectureTest,AccountTemplateIntegrationTest,AdminDashboardWebTest,AttendanceTemplateIntegrationTest,DashboardControllerWebTest,DashboardTemplateWebTest,ProjectTaskFormAccessibilityWebTest,ProjectTaskShellContractTest,RoleDashboardWebIntegrationTest,SharedErrorTemplateWebTest,DashboardServiceTest test
|
||||
PostgreSQL 18.4 via Testcontainers
|
||||
Tests run: 42, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Total time: 20.781 s
|
||||
|
||||
./mvnw -Dtest=LayerStructureTest,ReportingArchitectureTest test
|
||||
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
|
||||
BUILD SUCCESS
|
||||
Total time: 2.198 s
|
||||
|
||||
./mvnw -DskipTests compile
|
||||
BUILD SUCCESS
|
||||
Total time: 0.655 s
|
||||
|
||||
./mvnw -DskipTests -Ddoclint=all javadoc:javadoc
|
||||
BUILD SUCCESS
|
||||
Total time: 1.208 s
|
||||
```
|
||||
|
||||
## External-test boundaries
|
||||
|
||||
The source audit proves annotation scope and deliberate retention but does not alone prove generated bytecode, Spring injection, dashboard behavior, or Thymeleaf property access. Compile, Reporting service tests, MockMvc dashboard tests, architecture tests, PostgreSQL role-dashboard integration, and Javadoc/doclint provide those gates. Reporting owns no JPA entity or mutable bean, so JPA accessor/mapping checks are outside this feature's retrofit scope. Java 25 reports Lombok's known `sun.misc.Unsafe` annotation-processor warning, and Mockito reports its dynamic-agent warning; neither changed or failed the executed gates.
|
||||
@@ -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.
|
||||
@@ -2,21 +2,17 @@ package com.lab.labtimesheet.config;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/** Security material used to encrypt integration credentials at rest. */
|
||||
@ConfigurationProperties("lab.security")
|
||||
@Getter
|
||||
@Setter
|
||||
public class SecurityProperties {
|
||||
private String masterKey;
|
||||
|
||||
public String getMasterKey() {
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
public void setMasterKey(String masterKey) {
|
||||
this.masterKey = masterKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes and validates the configured AES-256 master key.
|
||||
*
|
||||
|
||||
+3
-4
@@ -6,6 +6,8 @@ import com.lab.labtimesheet.feature.account.model.dto.ActivationForm;
|
||||
import com.lab.labtimesheet.feature.account.model.dto.CreateAccountForm;
|
||||
import com.lab.labtimesheet.feature.account.service.AccountService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.hibernate.exception.ConstraintViolationException;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -20,13 +22,10 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
* constraints are mapped to their owning form fields without exposing persistence diagnostics.
|
||||
*/
|
||||
@Controller
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
class AccountController {
|
||||
private final AccountService accounts;
|
||||
|
||||
AccountController(AccountService accounts) {
|
||||
this.accounts = accounts;
|
||||
}
|
||||
|
||||
@GetMapping("/admin/accounts/new")
|
||||
String newAccount(Model model) {
|
||||
if (!model.containsAttribute("accountForm")) {
|
||||
|
||||
+2
-9
@@ -7,24 +7,17 @@ import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* Hides all non-bootstrap application routes until durable first-Admin initialization completes.
|
||||
* Only bootstrap pages, health, public assets, and error rendering remain reachable beforehand.
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class BootstrapAccessFilter extends OncePerRequestFilter {
|
||||
private final BootstrapService bootstrap;
|
||||
|
||||
/**
|
||||
* Creates the pre-bootstrap access guard.
|
||||
*
|
||||
* @param bootstrap durable installation-state service
|
||||
*/
|
||||
public BootstrapAccessFilter(BootstrapService bootstrap) {
|
||||
this.bootstrap = bootstrap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTTP 404 for hidden routes before bootstrap so no authentication surface is exposed prematurely.
|
||||
*
|
||||
|
||||
+3
-4
@@ -3,6 +3,8 @@ package com.lab.labtimesheet.feature.account.controller;
|
||||
import com.lab.labtimesheet.feature.account.model.dto.BootstrapForm;
|
||||
import com.lab.labtimesheet.feature.account.service.BootstrapService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
@@ -16,13 +18,10 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
/** Renders and processes the one-time first-Admin installation form. */
|
||||
@Controller
|
||||
@RequestMapping("/bootstrap")
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
class BootstrapController {
|
||||
private final BootstrapService bootstrap;
|
||||
|
||||
BootstrapController(BootstrapService bootstrap) {
|
||||
this.bootstrap = bootstrap;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
String form(Model model) {
|
||||
requireOpen();
|
||||
|
||||
@@ -3,10 +3,14 @@ package com.lab.labtimesheet.feature.account.model.dto;
|
||||
import jakarta.validation.constraints.AssertTrue;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Validated activation submission. Password fields remain request-local and are never repopulated by the view.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class ActivationForm {
|
||||
@NotBlank(message = "This activation link is invalid or no longer usable")
|
||||
private String token;
|
||||
@@ -34,10 +38,4 @@ public class ActivationForm {
|
||||
confirmPassword = null;
|
||||
}
|
||||
|
||||
public String getToken() { return token; }
|
||||
public void setToken(String token) { this.token = token; }
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
public String getConfirmPassword() { return confirmPassword; }
|
||||
public void setConfirmPassword(String confirmPassword) { this.confirmPassword = confirmPassword; }
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ package com.lab.labtimesheet.feature.account.model.dto;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Validated browser input for creating the first administrator.
|
||||
* The password is deliberately never copied into redirected state or repopulated after validation failure.
|
||||
*/
|
||||
@Getter
|
||||
public class BootstrapForm {
|
||||
@NotBlank(message = "Email is required")
|
||||
@Email(message = "Enter a valid email address")
|
||||
@@ -20,29 +23,15 @@ public class BootstrapForm {
|
||||
|
||||
@NotBlank(message = "Password is required")
|
||||
@Size(min = 12, max = 128, message = "Password must contain 12 through 128 characters")
|
||||
@Setter
|
||||
private String password;
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email == null ? null : email.trim();
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName == null ? null : displayName.trim();
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-10
@@ -8,9 +8,12 @@ import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
/** Validated, non-secret Admin input for creating an immutable-role account. */
|
||||
@Getter
|
||||
public class CreateAccountForm {
|
||||
@NotBlank(message = "Email is required")
|
||||
@Email(message = "Enter a valid email address")
|
||||
@@ -22,15 +25,19 @@ public class CreateAccountForm {
|
||||
private String displayName;
|
||||
|
||||
@NotNull(message = "Role is required")
|
||||
@Setter
|
||||
private GlobalRole role;
|
||||
|
||||
@Size(max = 64, message = "Student code must contain at most 64 characters")
|
||||
@Setter
|
||||
private String studentCode;
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
@Setter
|
||||
private LocalDate internshipStart;
|
||||
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
@Setter
|
||||
private LocalDate internshipEnd;
|
||||
|
||||
/**
|
||||
@@ -67,16 +74,6 @@ public class CreateAccountForm {
|
||||
return hasText(value) ? value.trim() : null;
|
||||
}
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email == null ? null : email.trim(); }
|
||||
public String getDisplayName() { return displayName; }
|
||||
public void setDisplayName(String displayName) { this.displayName = displayName == null ? null : displayName.trim(); }
|
||||
public GlobalRole getRole() { return role; }
|
||||
public void setRole(GlobalRole role) { this.role = role; }
|
||||
public String getStudentCode() { return studentCode; }
|
||||
public void setStudentCode(String studentCode) { this.studentCode = studentCode; }
|
||||
public LocalDate getInternshipStart() { return internshipStart; }
|
||||
public void setInternshipStart(LocalDate internshipStart) { this.internshipStart = internshipStart; }
|
||||
public LocalDate getInternshipEnd() { return internshipEnd; }
|
||||
public void setInternshipEnd(LocalDate internshipEnd) { this.internshipEnd = internshipEnd; }
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Persistent global account with immutable role, authentication lifecycle, creator attribution, and optimistic
|
||||
@@ -23,29 +26,37 @@ import jakarta.persistence.Version;
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "app_users")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class AppUser {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Getter
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, length = 320)
|
||||
@Getter
|
||||
private String email;
|
||||
|
||||
@Column(name = "display_name", nullable = false, length = 120)
|
||||
@Getter
|
||||
private String displayName;
|
||||
|
||||
@Column(name = "password_hash", length = 255)
|
||||
@Getter
|
||||
private String passwordHash;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "global_role", nullable = false, length = 16, updatable = false)
|
||||
@Getter
|
||||
private GlobalRole globalRole;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "account_status", nullable = false, length = 32)
|
||||
@Getter
|
||||
private AccountStatus accountStatus;
|
||||
|
||||
@Column(name = "activated_at")
|
||||
@Getter
|
||||
private Instant activatedAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@@ -61,10 +72,6 @@ public class AppUser {
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
/** Required by JPA; domain instances are created through named factories. */
|
||||
protected AppUser() {
|
||||
}
|
||||
|
||||
private AppUser(String email, String displayName, String passwordHash, GlobalRole globalRole,
|
||||
AccountStatus accountStatus, Instant activatedAt, AppUser createdBy, Instant now) {
|
||||
this.email = email;
|
||||
@@ -124,31 +131,4 @@ public class AppUser {
|
||||
updatedAt = now;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getPasswordHash() {
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
public GlobalRole getGlobalRole() {
|
||||
return globalRole;
|
||||
}
|
||||
|
||||
public AccountStatus getAccountStatus() {
|
||||
return accountStatus;
|
||||
}
|
||||
|
||||
public Instant getActivatedAt() {
|
||||
return activatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Persistent internship lifecycle and inclusive eligibility dates for an Intern account.
|
||||
@@ -18,6 +21,7 @@ import jakarta.persistence.Version;
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "intern_profiles")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class InternProfile {
|
||||
@Id
|
||||
@Column(name = "user_id")
|
||||
@@ -33,13 +37,16 @@ public class InternProfile {
|
||||
private String phone;
|
||||
|
||||
@Column(name = "internship_start_date", nullable = false)
|
||||
@Getter
|
||||
private LocalDate internshipStartDate;
|
||||
|
||||
@Column(name = "internship_end_date", nullable = false)
|
||||
@Getter
|
||||
private LocalDate internshipEndDate;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "internship_status", nullable = false, length = 24)
|
||||
@Getter
|
||||
private InternshipStatus internshipStatus;
|
||||
|
||||
@Column(name = "activated_at")
|
||||
@@ -60,10 +67,6 @@ public class InternProfile {
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
/** Required by JPA; domain instances are created through {@link #notStarted}. */
|
||||
protected InternProfile() {
|
||||
}
|
||||
|
||||
private InternProfile(
|
||||
long userId, String studentCode, LocalDate internshipStartDate, LocalDate internshipEndDate, Instant now) {
|
||||
this.userId = userId;
|
||||
@@ -105,15 +108,4 @@ public class InternProfile {
|
||||
updatedAt = now;
|
||||
}
|
||||
|
||||
public InternshipStatus getInternshipStatus() {
|
||||
return internshipStatus;
|
||||
}
|
||||
|
||||
public LocalDate getInternshipStartDate() {
|
||||
return internshipStartDate;
|
||||
}
|
||||
|
||||
public LocalDate getInternshipEndDate() {
|
||||
return internshipEndDate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,21 @@ import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/** Durable singleton installation state used to serialize and remember first-Admin bootstrap. */
|
||||
@Entity
|
||||
@Table(name = "system_state")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class SystemState {
|
||||
@Id
|
||||
@Column(name = "singleton_id")
|
||||
private short singletonId;
|
||||
|
||||
@Column(nullable = false)
|
||||
@Getter
|
||||
private boolean initialized;
|
||||
|
||||
@Column(name = "initialized_at")
|
||||
@@ -38,14 +43,6 @@ public class SystemState {
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
/** Required by JPA; Flyway creates the singleton row. */
|
||||
protected SystemState() {
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the installation initialized and retains the first Admin attribution.
|
||||
*
|
||||
|
||||
+10
-27
@@ -12,6 +12,9 @@ import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Persistent one-time user-action token state. Only a defensive copy of the SHA-256 token hash is stored; raw
|
||||
@@ -19,28 +22,35 @@ import jakarta.persistence.Table;
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "user_action_tokens")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class UserActionToken {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Getter
|
||||
private Long id;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
@Getter
|
||||
private Long userId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 24)
|
||||
@Getter
|
||||
private TokenPurpose purpose;
|
||||
|
||||
@Column(name = "token_hash", nullable = false, columnDefinition = "bytea")
|
||||
private byte[] tokenHash;
|
||||
|
||||
@Column(name = "expires_at", nullable = false)
|
||||
@Getter
|
||||
private Instant expiresAt;
|
||||
|
||||
@Column(name = "used_at")
|
||||
@Getter
|
||||
private Instant usedAt;
|
||||
|
||||
@Column(name = "invalidated_at")
|
||||
@Getter
|
||||
private Instant invalidatedAt;
|
||||
|
||||
@Column(name = "issued_by_user_id")
|
||||
@@ -49,10 +59,6 @@ public class UserActionToken {
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
/** Required by JPA; domain instances are created through named factories. */
|
||||
protected UserActionToken() {
|
||||
}
|
||||
|
||||
private UserActionToken(long userId, byte[] tokenHash, Instant expiresAt, long issuedByUserId, Instant now) {
|
||||
this.userId = userId;
|
||||
this.purpose = TokenPurpose.ACTIVATION;
|
||||
@@ -115,18 +121,6 @@ public class UserActionToken {
|
||||
}
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public TokenPurpose getPurpose() {
|
||||
return purpose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a defensive copy of the persisted token hash.
|
||||
*
|
||||
@@ -136,15 +130,4 @@ public class UserActionToken {
|
||||
return Arrays.copyOf(tokenHash, tokenHash.length);
|
||||
}
|
||||
|
||||
public Instant getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public Instant getUsedAt() {
|
||||
return usedAt;
|
||||
}
|
||||
|
||||
public Instant getInvalidatedAt() {
|
||||
return invalidatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import com.lab.labtimesheet.feature.account.model.entity.AppUser;
|
||||
import com.lab.labtimesheet.feature.account.model.entity.SystemState;
|
||||
import com.lab.labtimesheet.feature.account.repository.AppUserRepository;
|
||||
import com.lab.labtimesheet.feature.account.repository.SystemStateRepository;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -16,20 +18,13 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
* Successful creation persists the first active Admin and initialization marker atomically.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
public class BootstrapService {
|
||||
private final SystemStateRepository systemStates;
|
||||
private final AppUserRepository users;
|
||||
private final PasswordEncoder passwords;
|
||||
private final Clock clock;
|
||||
|
||||
BootstrapService(SystemStateRepository systemStates, AppUserRepository users, PasswordEncoder passwords,
|
||||
Clock clock) {
|
||||
this.systemStates = systemStates;
|
||||
this.users = users;
|
||||
this.passwords = passwords;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the first active Admin exactly once.
|
||||
*
|
||||
|
||||
+3
-4
@@ -2,6 +2,8 @@ package com.lab.labtimesheet.feature.account.service;
|
||||
|
||||
import com.lab.labtimesheet.feature.account.model.AccountStatus;
|
||||
import com.lab.labtimesheet.feature.account.repository.AppUserRepository;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
@@ -11,13 +13,10 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/** Adapts persisted account credentials and lifecycle state to Spring Security authentication. */
|
||||
@Service
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
class DatabaseUserDetailsService implements UserDetailsService {
|
||||
private final AppUserRepository users;
|
||||
|
||||
DatabaseUserDetailsService(AppUserRepository users) {
|
||||
this.users = users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the normalized account and disables authentication unless its lifecycle state is active.
|
||||
*
|
||||
|
||||
+3
-6
@@ -7,6 +7,8 @@ import com.lab.labtimesheet.feature.attendance.service.AttendanceApplicationServ
|
||||
import com.lab.labtimesheet.feature.attendance.service.AttendanceCurrentUserService;
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -22,17 +24,12 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/attendance")
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
public class AttendanceController {
|
||||
|
||||
private final AttendanceApplicationService attendance;
|
||||
private final AttendanceCurrentUserService currentUsers;
|
||||
|
||||
AttendanceController(
|
||||
AttendanceApplicationService attendance, AttendanceCurrentUserService currentUsers) {
|
||||
this.attendance = attendance;
|
||||
this.currentUsers = currentUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the authenticated Intern's inclusive attendance history, defaulting to the current month.
|
||||
*
|
||||
|
||||
+3
-9
@@ -7,6 +7,8 @@ import com.lab.labtimesheet.feature.attendance.service.AttendanceCurrentUserServ
|
||||
import com.lab.labtimesheet.feature.attendance.service.CalendarApplicationService;
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -23,21 +25,13 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/attendance/calendar")
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
public class CalendarController {
|
||||
|
||||
private final CalendarApplicationService calendar;
|
||||
private final AttendanceApplicationService attendance;
|
||||
private final AttendanceCurrentUserService currentUsers;
|
||||
|
||||
CalendarController(
|
||||
CalendarApplicationService calendar,
|
||||
AttendanceApplicationService attendance,
|
||||
AttendanceCurrentUserService currentUsers) {
|
||||
this.calendar = calendar;
|
||||
this.attendance = attendance;
|
||||
this.currentUsers = currentUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the next year of locally stored calendar events for an authenticated Admin.
|
||||
*
|
||||
|
||||
+3
-5
@@ -19,12 +19,15 @@ import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* JPA mapping of an immutable-on-effective attendance policy version and its configured workdays.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "attendance_policy_versions")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class AttendancePolicyEntity {
|
||||
|
||||
@Id
|
||||
@@ -65,11 +68,6 @@ public class AttendancePolicyEntity {
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
/**
|
||||
* Required by JPA; application code resolves existing effective-dated versions instead of constructing them here.
|
||||
*/
|
||||
protected AttendancePolicyEntity() {}
|
||||
|
||||
/**
|
||||
* Converts the persisted version to the immutable policy used for historical boundary calculations.
|
||||
*
|
||||
|
||||
+3
-5
@@ -13,12 +13,15 @@ import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* JPA persistence model for one Intern/work-date punch row with its permanently attached policy version.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "attendance_records")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class AttendanceRecordEntity {
|
||||
|
||||
@Id
|
||||
@@ -44,11 +47,6 @@ public class AttendanceRecordEntity {
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
/**
|
||||
* Required by JPA.
|
||||
*/
|
||||
protected AttendanceRecordEntity() {}
|
||||
|
||||
/**
|
||||
* Creates a new persistence row from server-authoritative raw punch values.
|
||||
*
|
||||
|
||||
+3
-5
@@ -10,12 +10,15 @@ import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import java.time.LocalDate;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* JPA model for the locally authoritative global calendar decision.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "global_calendar_events")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class GlobalCalendarEventEntity {
|
||||
|
||||
@Id
|
||||
@@ -43,11 +46,6 @@ public class GlobalCalendarEventEntity {
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
/**
|
||||
* Required by JPA.
|
||||
*/
|
||||
protected GlobalCalendarEventEntity() {}
|
||||
|
||||
/**
|
||||
* Creates a custom calendar event attributed to the Admin actor.
|
||||
*
|
||||
|
||||
+3
-5
@@ -9,12 +9,15 @@ import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.MapsId;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDate;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* JPA mapping of an immutable leave-day allocation whose exact date, policy, and quota snapshot remain historical.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "leave_request_days")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class LeaveRequestDayEntity {
|
||||
|
||||
@EmbeddedId
|
||||
@@ -35,11 +38,6 @@ public class LeaveRequestDayEntity {
|
||||
@Column(name = "monthly_quota_snapshot", nullable = false)
|
||||
private int monthlyQuotaSnapshot;
|
||||
|
||||
/**
|
||||
* Required by JPA.
|
||||
*/
|
||||
protected LeaveRequestDayEntity() {}
|
||||
|
||||
LeaveRequestDayEntity(
|
||||
LeaveRequestEntity request,
|
||||
LocalDate leaveDate,
|
||||
|
||||
+3
-5
@@ -5,11 +5,14 @@ import jakarta.persistence.Embeddable;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Objects;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Composite identifier of one frozen quota-consuming date within a leave request.
|
||||
*/
|
||||
@Embeddable
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class LeaveRequestDayId implements Serializable {
|
||||
|
||||
/** Parent request identity used by the composite primary key. */
|
||||
@@ -20,11 +23,6 @@ public class LeaveRequestDayId implements Serializable {
|
||||
@Column(name = "leave_date", nullable = false)
|
||||
private LocalDate leaveDate;
|
||||
|
||||
/**
|
||||
* Required by JPA.
|
||||
*/
|
||||
protected LeaveRequestDayId() {}
|
||||
|
||||
/**
|
||||
* Creates the identity for an already-persisted request and its exact allocated date.
|
||||
*
|
||||
|
||||
+3
-5
@@ -9,12 +9,15 @@ import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Minimal Attendance-owned JPA mapping of leave request state used when evaluating frozen leave-day allocations.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "leave_requests")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class LeaveRequestEntity {
|
||||
|
||||
@Id
|
||||
@@ -51,11 +54,6 @@ public class LeaveRequestEntity {
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
/**
|
||||
* Required by JPA.
|
||||
*/
|
||||
protected LeaveRequestEntity() {}
|
||||
|
||||
LeaveRequestEntity(
|
||||
long internUserId,
|
||||
LocalDate startDate,
|
||||
|
||||
+4
-18
@@ -20,9 +20,11 @@ import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -31,6 +33,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
* Account eligibility is obtained only through {@link AccountService}; raw rows retain their attached policy.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
public class AttendanceApplicationService {
|
||||
|
||||
private final Clock clock;
|
||||
@@ -41,23 +44,6 @@ public class AttendanceApplicationService {
|
||||
private final CalendarApplicationService calendar;
|
||||
private final AttendanceService attendance;
|
||||
|
||||
AttendanceApplicationService(
|
||||
Clock clock,
|
||||
AttendancePolicyRepository policyEntities,
|
||||
AttendanceRecordRepository recordEntities,
|
||||
AttendanceQueryRepository queries,
|
||||
AccountService accounts,
|
||||
CalendarApplicationService calendar,
|
||||
AttendanceService attendance) {
|
||||
this.clock = clock;
|
||||
this.policyEntities = policyEntities;
|
||||
this.recordEntities = recordEntities;
|
||||
this.queries = queries;
|
||||
this.accounts = accounts;
|
||||
this.calendar = calendar;
|
||||
this.attendance = attendance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the sole server-time check-in for the effective policy-local date.
|
||||
* Eligibility, workday, calendar, and exact frozen leave allocation are evaluated in the transaction;
|
||||
|
||||
+3
-4
@@ -6,6 +6,8 @@ import com.lab.labtimesheet.feature.account.service.AccountService;
|
||||
import com.lab.labtimesheet.feature.attendance.model.AttendanceActor;
|
||||
import com.lab.labtimesheet.feature.attendance.model.AttendanceRole;
|
||||
import java.security.Principal;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -13,14 +15,11 @@ import org.springframework.stereotype.Service;
|
||||
* Converts Spring Security principals into active Attendance authorization contexts through AccountService DTOs.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
public class AttendanceCurrentUserService {
|
||||
|
||||
private final AccountService accounts;
|
||||
|
||||
AttendanceCurrentUserService(AccountService accounts) {
|
||||
this.accounts = accounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the authenticated email through the Account feature and rejects missing or inactive identities.
|
||||
*
|
||||
|
||||
+3
-2
@@ -8,16 +8,17 @@ import com.lab.labtimesheet.feature.attendance.model.AttendanceRecord;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Pure attendance punch rules over immutable policy, date context, and raw record values.
|
||||
*/
|
||||
@Service
|
||||
@NoArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
public final class AttendanceService {
|
||||
|
||||
AttendanceService() {}
|
||||
|
||||
/**
|
||||
* Creates the sole raw check-in for an eligible Intern/date using the supplied server instant.
|
||||
* Equality at the grace boundary is accepted; violation classification remains attached-policy based.
|
||||
|
||||
+3
-9
@@ -12,6 +12,8 @@ import com.lab.labtimesheet.feature.attendance.repository.GlobalCalendarEventRep
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -20,21 +22,13 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
* Transactional boundary for the locally authoritative global calendar and its cross-feature day-off decision.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
public class CalendarApplicationService {
|
||||
|
||||
private final Clock clock;
|
||||
private final AttendancePolicyRepository policies;
|
||||
private final GlobalCalendarEventRepository events;
|
||||
|
||||
CalendarApplicationService(
|
||||
Clock clock,
|
||||
AttendancePolicyRepository policies,
|
||||
GlobalCalendarEventRepository events) {
|
||||
this.clock = clock;
|
||||
this.policies = policies;
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Admin-authored custom event on a non-past policy-local date.
|
||||
*
|
||||
|
||||
+3
-5
@@ -9,6 +9,8 @@ import com.lab.labtimesheet.feature.integration.model.dto.SmtpForm;
|
||||
import com.lab.labtimesheet.feature.integration.service.SmtpConfigurationService;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.mail.MailException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
@@ -25,6 +27,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/admin/smtp")
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
class SmtpController {
|
||||
private static final String TEST_FAILURE_MESSAGE =
|
||||
"SMTP test failed. Verify the draft settings and server availability, then try again.";
|
||||
@@ -41,11 +44,6 @@ class SmtpController {
|
||||
private final SmtpConfigurationService smtp;
|
||||
private final AccountService accounts;
|
||||
|
||||
SmtpController(SmtpConfigurationService smtp, AccountService accounts) {
|
||||
this.smtp = smtp;
|
||||
this.accounts = accounts;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
String form(Model model) {
|
||||
return renderForm(model, null);
|
||||
|
||||
+3
-4
@@ -1,6 +1,8 @@
|
||||
package com.lab.labtimesheet.feature.integration.controller;
|
||||
|
||||
import com.lab.labtimesheet.feature.integration.service.SmtpConfigurationService;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
@@ -9,13 +11,10 @@ import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
* configuration is active.
|
||||
*/
|
||||
@ControllerAdvice
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
class SmtpWarningAdvice {
|
||||
private final SmtpConfigurationService smtp;
|
||||
|
||||
SmtpWarningAdvice(SmtpConfigurationService smtp) {
|
||||
this.smtp = smtp;
|
||||
}
|
||||
|
||||
@ModelAttribute("smtpRestricted")
|
||||
boolean smtpRestricted() {
|
||||
return !smtp.hasActiveConfiguration();
|
||||
|
||||
+4
-7
@@ -2,18 +2,15 @@ package com.lab.labtimesheet.feature.integration.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/** Validated identifier submitted by the SMTP test and activation forms. */
|
||||
@Getter
|
||||
@Setter
|
||||
public class SmtpActionForm {
|
||||
@NotNull(message = "SMTP draft is required")
|
||||
@Positive(message = "SMTP draft is invalid")
|
||||
private Long draftId;
|
||||
|
||||
public Long getDraftId() {
|
||||
return draftId;
|
||||
}
|
||||
|
||||
public void setDraftId(Long draftId) {
|
||||
this.draftId = draftId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,15 @@ import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Validated Admin input for an SMTP draft. The cleartext password exists only for the current request and is
|
||||
* cleared before the form is rendered again.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class SmtpForm {
|
||||
@NotBlank(message = "Host is required")
|
||||
@Size(max = 255, message = "Host must contain at most 255 characters")
|
||||
@@ -96,18 +100,4 @@ public class SmtpForm {
|
||||
return value == null || value.isEmpty() ? null : value;
|
||||
}
|
||||
|
||||
public String getHost() { return host; }
|
||||
public void setHost(String host) { this.host = host; }
|
||||
public int getPort() { return port; }
|
||||
public void setPort(int port) { this.port = port; }
|
||||
public SecurityMode getSecurityMode() { return securityMode; }
|
||||
public void setSecurityMode(SecurityMode securityMode) { this.securityMode = securityMode; }
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
public String getFromAddress() { return fromAddress; }
|
||||
public void setFromAddress(String fromAddress) { this.fromAddress = fromAddress; }
|
||||
public String getFromName() { return fromName; }
|
||||
public void setFromName(String fromName) { this.fromName = fromName; }
|
||||
}
|
||||
|
||||
+14
-43
@@ -15,6 +15,9 @@ import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Versioned SMTP configuration entity whose credentials remain AES-GCM encrypted at rest.
|
||||
@@ -22,26 +25,33 @@ import jakarta.persistence.Version;
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "smtp_configurations")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class SmtpConfiguration {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Getter
|
||||
private Long id;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
@Getter
|
||||
private SmtpStatus status;
|
||||
|
||||
@Column(nullable = false, length = 255)
|
||||
@Getter
|
||||
private String host;
|
||||
|
||||
@Column(nullable = false)
|
||||
@Getter
|
||||
private int port;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "security_mode", nullable = false, length = 16)
|
||||
@Getter
|
||||
private SecurityMode securityMode;
|
||||
|
||||
@Column(length = 320)
|
||||
@Getter
|
||||
private String username;
|
||||
|
||||
@Column(name = "password_ciphertext")
|
||||
@@ -51,15 +61,19 @@ public class SmtpConfiguration {
|
||||
private byte[] passwordNonce;
|
||||
|
||||
@Column(name = "secret_key_version")
|
||||
@Getter
|
||||
private Integer secretKeyVersion;
|
||||
|
||||
@Column(name = "from_address", nullable = false, length = 320)
|
||||
@Getter
|
||||
private String fromAddress;
|
||||
|
||||
@Column(name = "from_name", nullable = false, length = 120)
|
||||
@Getter
|
||||
private String fromName;
|
||||
|
||||
@Column(name = "tested_at")
|
||||
@Getter
|
||||
private Instant testedAt;
|
||||
|
||||
@Column(name = "tested_by_user_id")
|
||||
@@ -89,10 +103,6 @@ public class SmtpConfiguration {
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
/** Required by JPA; revisions are created through {@link #draft}. */
|
||||
protected SmtpConfiguration() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an editable SMTP revision with encrypted credential material.
|
||||
*
|
||||
@@ -191,30 +201,6 @@ public class SmtpConfiguration {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public SmtpStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public SecurityMode getSecurityMode() {
|
||||
return securityMode;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/** @return a defensive copy of encrypted password bytes, or {@code null} */
|
||||
public byte[] getPasswordCiphertext() {
|
||||
return passwordCiphertext == null ? null : passwordCiphertext.clone();
|
||||
@@ -225,19 +211,4 @@ public class SmtpConfiguration {
|
||||
return passwordNonce == null ? null : passwordNonce.clone();
|
||||
}
|
||||
|
||||
public Integer getSecretKeyVersion() {
|
||||
return secretKeyVersion;
|
||||
}
|
||||
|
||||
public String getFromAddress() {
|
||||
return fromAddress;
|
||||
}
|
||||
|
||||
public String getFromName() {
|
||||
return fromName;
|
||||
}
|
||||
|
||||
public Instant getTestedAt() {
|
||||
return testedAt;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-6
@@ -4,6 +4,8 @@ import com.lab.labtimesheet.feature.integration.model.dto.SmtpConnection;
|
||||
import com.lab.labtimesheet.feature.integration.model.entity.SmtpConfiguration;
|
||||
import com.lab.labtimesheet.feature.integration.model.SmtpStatus;
|
||||
import com.lab.labtimesheet.feature.integration.repository.SmtpConfigurationRepository;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -12,17 +14,12 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
* Stored credentials are decrypted only while constructing the immediate adapter call.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
public class MailDeliveryService {
|
||||
private final SmtpConfigurationRepository configurations;
|
||||
private final SecretCipher secrets;
|
||||
private final SmtpProbe probe;
|
||||
|
||||
MailDeliveryService(SmtpConfigurationRepository configurations, SecretCipher secrets, SmtpProbe probe) {
|
||||
this.configurations = configurations;
|
||||
this.secrets = secrets;
|
||||
this.probe = probe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether workflows may emit required email.
|
||||
*
|
||||
|
||||
+3
-12
@@ -11,6 +11,8 @@ import com.lab.labtimesheet.feature.integration.model.dto.SmtpDraft;
|
||||
import com.lab.labtimesheet.feature.integration.model.dto.SmtpSetupStatus;
|
||||
import com.lab.labtimesheet.feature.integration.model.entity.SmtpConfiguration;
|
||||
import com.lab.labtimesheet.feature.integration.repository.SmtpConfigurationRepository;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.Profiles;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -21,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
* A changed draft loses prior test status, and an active revision is retired when its tested successor activates.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
public class SmtpConfigurationService {
|
||||
private final SmtpConfigurationRepository configurations;
|
||||
private final AccountService accounts;
|
||||
@@ -30,18 +33,6 @@ public class SmtpConfigurationService {
|
||||
private final Clock clock;
|
||||
private final MailDeliveryService mailDelivery;
|
||||
|
||||
SmtpConfigurationService(SmtpConfigurationRepository configurations, AccountService accounts,
|
||||
SecretCipher secrets, SmtpProbe probe, Environment environment, Clock clock,
|
||||
MailDeliveryService mailDelivery) {
|
||||
this.configurations = configurations;
|
||||
this.accounts = accounts;
|
||||
this.secrets = secrets;
|
||||
this.probe = probe;
|
||||
this.environment = environment;
|
||||
this.clock = clock;
|
||||
this.mailDelivery = mailDelivery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or replaces the editable draft after validating Admin authority and environment transport rules.
|
||||
* Any supplied password is encrypted before persistence and prior test status is cleared.
|
||||
|
||||
+2
-11
@@ -8,6 +8,7 @@ import com.lab.labtimesheet.feature.project.service.ProjectQueryService;
|
||||
import com.lab.labtimesheet.feature.project.service.ProjectService;
|
||||
import jakarta.validation.Valid;
|
||||
import java.security.Principal;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
@@ -27,22 +28,12 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/projects")
|
||||
@RequiredArgsConstructor
|
||||
public class ProjectController {
|
||||
|
||||
private final ProjectQueryService pages;
|
||||
private final ProjectService projects;
|
||||
|
||||
/**
|
||||
* Creates the MVC adapter for Project queries and mutations.
|
||||
*
|
||||
* @param pages authorized Project read operations
|
||||
* @param projects transactional Project mutation operations
|
||||
*/
|
||||
public ProjectController(ProjectQueryService pages, ProjectService projects) {
|
||||
this.pages = pages;
|
||||
this.projects = projects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists only Projects visible to the authenticated actor and exposes Project creation only
|
||||
* to Mentors.
|
||||
|
||||
-4
@@ -17,10 +17,6 @@ import org.springframework.web.servlet.ModelAndView;
|
||||
@ControllerAdvice(assignableTypes = ProjectController.class)
|
||||
public class ProjectControllerAdvice {
|
||||
|
||||
/** Creates the stateless Project exception-to-view adapter. */
|
||||
public ProjectControllerAdvice() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides whether a requested Project or nested resource exists.
|
||||
*
|
||||
|
||||
@@ -22,6 +22,8 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* JPA aggregate root for Project lifecycle, membership intervals, and leadership intervals.
|
||||
@@ -32,6 +34,7 @@ import java.util.Set;
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "projects")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class ProjectEntity {
|
||||
|
||||
@Id
|
||||
@@ -75,10 +78,6 @@ public class ProjectEntity {
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
/** Constructor reserved for JPA materialization. */
|
||||
protected ProjectEntity() {
|
||||
}
|
||||
|
||||
private ProjectEntity(
|
||||
long mentorUserId,
|
||||
String name,
|
||||
|
||||
+3
-4
@@ -11,6 +11,8 @@ import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.Instant;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* JPA leadership interval attached to an active same-Project membership.
|
||||
@@ -20,6 +22,7 @@ import java.time.Instant;
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "project_leadership_terms")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class ProjectLeadershipTermEntity {
|
||||
|
||||
@Id
|
||||
@@ -46,10 +49,6 @@ public class ProjectLeadershipTermEntity {
|
||||
@Column(name = "ended_by_mentor_user_id")
|
||||
private Long endedByMentorUserId;
|
||||
|
||||
/** Constructor reserved for JPA materialization. */
|
||||
protected ProjectLeadershipTermEntity() {
|
||||
}
|
||||
|
||||
ProjectLeadershipTermEntity(
|
||||
ProjectEntity project,
|
||||
ProjectMembershipEntity membership,
|
||||
|
||||
+3
-4
@@ -11,6 +11,8 @@ import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import java.time.Instant;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* JPA membership interval linking one Intern to one Project.
|
||||
@@ -20,6 +22,7 @@ import java.time.Instant;
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "project_memberships")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class ProjectMembershipEntity {
|
||||
|
||||
@Id
|
||||
@@ -48,10 +51,6 @@ public class ProjectMembershipEntity {
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
/** Constructor reserved for JPA materialization. */
|
||||
protected ProjectMembershipEntity() {
|
||||
}
|
||||
|
||||
ProjectMembershipEntity(ProjectEntity project, long internUserId, Instant joinedAt, long addedByUserId) {
|
||||
this.project = project;
|
||||
this.internUserId = internUserId;
|
||||
|
||||
+2
-11
@@ -15,6 +15,7 @@ import com.lab.labtimesheet.feature.project.model.dto.ProjectTaskMemberView;
|
||||
import com.lab.labtimesheet.feature.project.model.entity.ProjectEntity;
|
||||
import com.lab.labtimesheet.feature.project.repository.ProjectRepository;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -26,22 +27,12 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
* members but expose no current Leader or active-member context.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProjectQueryService {
|
||||
|
||||
private final ProjectRepository projects;
|
||||
private final AccountService accounts;
|
||||
|
||||
/**
|
||||
* Creates the Project read service.
|
||||
*
|
||||
* @param projects Project aggregate repository
|
||||
* @param accounts public Account identity and internship-eligibility boundary
|
||||
*/
|
||||
public ProjectQueryService(ProjectRepository projects, AccountService accounts) {
|
||||
this.projects = projects;
|
||||
this.accounts = accounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an active authenticated account to its stable user identifier.
|
||||
*
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.lab.labtimesheet.feature.task.service.TaskQueryService;
|
||||
import java.time.Clock;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -22,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
* imports their repositories or entities.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProjectService {
|
||||
|
||||
private final ProjectRepository projects;
|
||||
@@ -30,28 +32,6 @@ public class ProjectService {
|
||||
private final TaskQueryService taskQueries;
|
||||
private final Clock clock;
|
||||
|
||||
/**
|
||||
* Creates the Project mutation service.
|
||||
*
|
||||
* @param projects Project aggregate repository
|
||||
* @param accounts public Account identity and eligibility boundary
|
||||
* @param queries DTO-only Project query boundary reused for locked Task context
|
||||
* @param taskQueries public Task activation-guard boundary
|
||||
* @param clock server clock supplying persisted mutation instants
|
||||
*/
|
||||
public ProjectService(
|
||||
ProjectRepository projects,
|
||||
AccountService accounts,
|
||||
ProjectQueryService queries,
|
||||
TaskQueryService taskQueries,
|
||||
Clock clock) {
|
||||
this.projects = projects;
|
||||
this.accounts = accounts;
|
||||
this.queries = queries;
|
||||
this.taskQueries = taskQueries;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically creates a planned Mentor-owned Project, eligible initial membership, and first
|
||||
* leadership term. {@code saveAndFlush} exposes database invariant violations before commit.
|
||||
|
||||
+2
-9
@@ -2,6 +2,7 @@ package com.lab.labtimesheet.feature.reporting.controller;
|
||||
|
||||
import com.lab.labtimesheet.feature.reporting.exception.DashboardAccessDeniedException;
|
||||
import com.lab.labtimesheet.feature.reporting.service.DashboardService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
@@ -14,19 +15,11 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
* reloads and revalidates the persisted account role and lifecycle before returning any data.
|
||||
*/
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardController {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
|
||||
/**
|
||||
* Creates the dashboard endpoint backed by the reporting composition service.
|
||||
*
|
||||
* @param dashboardService service that authorizes and assembles role-scoped dashboard data
|
||||
*/
|
||||
public DashboardController(DashboardService dashboardService) {
|
||||
this.dashboardService = dashboardService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the dashboard permitted by the caller's authenticated global role.
|
||||
*
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.lab.labtimesheet.feature.project.model.dto.ProjectDashboardSummary;
|
||||
import com.lab.labtimesheet.feature.project.service.ProjectQueryService;
|
||||
import com.lab.labtimesheet.feature.task.model.dto.TaskDashboardView;
|
||||
import com.lab.labtimesheet.feature.task.service.TaskDashboardService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -28,6 +29,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
*/
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardService {
|
||||
|
||||
private final AccountService accounts;
|
||||
@@ -35,25 +37,6 @@ public class DashboardService {
|
||||
private final TaskDashboardService tasks;
|
||||
private final AttendanceApplicationService attendance;
|
||||
|
||||
/**
|
||||
* Creates a reporting coordinator over the concrete feature query boundaries.
|
||||
*
|
||||
* @param accounts account identity and Admin summary boundary
|
||||
* @param projects role-scoped Project summary boundary
|
||||
* @param tasks role-scoped Task dashboard boundary
|
||||
* @param attendance attendance state boundary using the active policy business date
|
||||
*/
|
||||
public DashboardService(
|
||||
AccountService accounts,
|
||||
ProjectQueryService projects,
|
||||
TaskDashboardService tasks,
|
||||
AttendanceApplicationService attendance) {
|
||||
this.accounts = accounts;
|
||||
this.projects = projects;
|
||||
this.tasks = tasks;
|
||||
this.attendance = attendance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds system-wide Admin counts after confirming an active persisted Admin identity.
|
||||
*
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.lab.labtimesheet.feature.task.service.TaskService;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
@@ -31,19 +32,11 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
* successful mutations use redirects to prevent duplicate submissions.
|
||||
*/
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class TaskController {
|
||||
|
||||
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")
|
||||
String list(Authentication authentication, @PathVariable long projectId, Model model) {
|
||||
TaskListView taskList = taskService.list(authentication.getName(), projectId);
|
||||
|
||||
@@ -12,6 +12,9 @@ import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Version;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Persisted Task aggregate row with one current same-Project assignee.
|
||||
@@ -22,6 +25,8 @@ import java.time.LocalDate;
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "tasks")
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class Task {
|
||||
|
||||
@Id
|
||||
@@ -60,20 +65,20 @@ public class Task {
|
||||
private Instant deletedAt;
|
||||
|
||||
@Column(name = "deleted_by_membership_id")
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Long deletedByMembershipId;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Instant updatedAt;
|
||||
|
||||
@Version
|
||||
@Getter(AccessLevel.NONE)
|
||||
private long version;
|
||||
|
||||
/** Constructor reserved for JPA materialization. */
|
||||
protected Task() {}
|
||||
|
||||
/**
|
||||
* Creates a TODO Task and records the creating membership as both creator and assigner.
|
||||
*
|
||||
@@ -121,111 +126,4 @@ public class Task {
|
||||
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.Table;
|
||||
import java.time.Instant;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Persisted append-only Task comment.
|
||||
@@ -16,6 +19,8 @@ import java.time.Instant;
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "task_comments")
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class TaskComment {
|
||||
|
||||
@Id
|
||||
@@ -34,9 +39,6 @@ public class TaskComment {
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
/** Constructor reserved for JPA materialization. */
|
||||
protected TaskComment() {}
|
||||
|
||||
/**
|
||||
* Creates an immutable comment from server-authorized values.
|
||||
*
|
||||
@@ -52,48 +54,4 @@ public class TaskComment {
|
||||
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.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
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.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TaskDashboardService {
|
||||
|
||||
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 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.
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.lab.labtimesheet.feature.task.service;
|
||||
|
||||
import com.lab.labtimesheet.feature.task.repository.TaskRepository;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
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.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TaskQueryService {
|
||||
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -30,6 +30,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
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.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TaskService {
|
||||
|
||||
private final TaskRepository tasks;
|
||||
@@ -51,31 +53,6 @@ public class TaskService {
|
||||
private final CalendarApplicationService calendar;
|
||||
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.
|
||||
*
|
||||
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
package com.lab.labtimesheet.feature.attendance;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.lab.labtimesheet.feature.account.service.AccountService;
|
||||
import com.lab.labtimesheet.feature.attendance.controller.AttendanceController;
|
||||
import com.lab.labtimesheet.feature.attendance.controller.CalendarController;
|
||||
import com.lab.labtimesheet.feature.attendance.model.AttendanceActor;
|
||||
import com.lab.labtimesheet.feature.attendance.model.AttendanceDayContext;
|
||||
import com.lab.labtimesheet.feature.attendance.model.AttendancePolicy;
|
||||
import com.lab.labtimesheet.feature.attendance.model.AttendanceRecord;
|
||||
import com.lab.labtimesheet.feature.attendance.model.AttendanceRole;
|
||||
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.GlobalCalendarEvent;
|
||||
import com.lab.labtimesheet.feature.attendance.model.entity.AttendancePolicyEntity;
|
||||
import com.lab.labtimesheet.feature.attendance.model.entity.AttendanceRecordEntity;
|
||||
import com.lab.labtimesheet.feature.attendance.model.entity.GlobalCalendarEventEntity;
|
||||
import com.lab.labtimesheet.feature.attendance.model.entity.LeaveRequestDayEntity;
|
||||
import com.lab.labtimesheet.feature.attendance.model.entity.LeaveRequestDayId;
|
||||
import com.lab.labtimesheet.feature.attendance.model.entity.LeaveRequestEntity;
|
||||
import com.lab.labtimesheet.feature.attendance.repository.AttendancePolicyRepository;
|
||||
import com.lab.labtimesheet.feature.attendance.repository.AttendanceQueryRepository;
|
||||
import com.lab.labtimesheet.feature.attendance.repository.AttendanceRecordRepository;
|
||||
import com.lab.labtimesheet.feature.attendance.repository.GlobalCalendarEventRepository;
|
||||
import com.lab.labtimesheet.feature.attendance.service.AttendanceApplicationService;
|
||||
import com.lab.labtimesheet.feature.attendance.service.AttendanceCurrentUserService;
|
||||
import com.lab.labtimesheet.feature.attendance.service.AttendanceService;
|
||||
import com.lab.labtimesheet.feature.attendance.service.CalendarApplicationService;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.math.BigDecimal;
|
||||
import java.security.Principal;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
class AttendanceLombokBoilerplateTest {
|
||||
|
||||
private static final int PACKAGE_PRIVATE = 0;
|
||||
|
||||
@Test
|
||||
void generatedConstructorsPreserveParameterListsAndVisibility() {
|
||||
assertConstructors(
|
||||
AttendanceController.class,
|
||||
constructor(
|
||||
PACKAGE_PRIVATE,
|
||||
AttendanceApplicationService.class,
|
||||
AttendanceCurrentUserService.class));
|
||||
assertConstructors(
|
||||
CalendarController.class,
|
||||
constructor(
|
||||
PACKAGE_PRIVATE,
|
||||
CalendarApplicationService.class,
|
||||
AttendanceApplicationService.class,
|
||||
AttendanceCurrentUserService.class));
|
||||
assertConstructors(
|
||||
AttendanceApplicationService.class,
|
||||
constructor(
|
||||
PACKAGE_PRIVATE,
|
||||
Clock.class,
|
||||
AttendancePolicyRepository.class,
|
||||
AttendanceRecordRepository.class,
|
||||
AttendanceQueryRepository.class,
|
||||
AccountService.class,
|
||||
CalendarApplicationService.class,
|
||||
AttendanceService.class));
|
||||
assertConstructors(
|
||||
AttendanceCurrentUserService.class,
|
||||
constructor(PACKAGE_PRIVATE, AccountService.class));
|
||||
assertConstructors(
|
||||
CalendarApplicationService.class,
|
||||
constructor(
|
||||
PACKAGE_PRIVATE,
|
||||
Clock.class,
|
||||
AttendancePolicyRepository.class,
|
||||
GlobalCalendarEventRepository.class));
|
||||
assertConstructors(AttendanceService.class, constructor(PACKAGE_PRIVATE));
|
||||
|
||||
assertConstructors(AttendancePolicyEntity.class, constructor(Modifier.PROTECTED));
|
||||
assertConstructors(
|
||||
AttendanceRecordEntity.class,
|
||||
constructor(Modifier.PROTECTED),
|
||||
constructor(
|
||||
Modifier.PUBLIC,
|
||||
long.class,
|
||||
LocalDate.class,
|
||||
AttendancePolicyEntity.class,
|
||||
Instant.class,
|
||||
Instant.class));
|
||||
assertConstructors(
|
||||
GlobalCalendarEventEntity.class,
|
||||
constructor(Modifier.PROTECTED),
|
||||
constructor(
|
||||
Modifier.PUBLIC,
|
||||
LocalDate.class,
|
||||
String.class,
|
||||
boolean.class,
|
||||
long.class));
|
||||
assertConstructors(
|
||||
LeaveRequestDayEntity.class,
|
||||
constructor(Modifier.PROTECTED),
|
||||
constructor(
|
||||
PACKAGE_PRIVATE,
|
||||
LeaveRequestEntity.class,
|
||||
LocalDate.class,
|
||||
AttendancePolicyEntity.class,
|
||||
int.class));
|
||||
assertConstructors(
|
||||
LeaveRequestDayId.class,
|
||||
constructor(Modifier.PROTECTED),
|
||||
constructor(Modifier.PUBLIC, long.class, LocalDate.class));
|
||||
assertConstructors(
|
||||
LeaveRequestEntity.class,
|
||||
constructor(Modifier.PROTECTED),
|
||||
constructor(
|
||||
PACKAGE_PRIVATE,
|
||||
long.class,
|
||||
LocalDate.class,
|
||||
LocalDate.class,
|
||||
String.class,
|
||||
Instant.class,
|
||||
Instant.class,
|
||||
long.class,
|
||||
Instant.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void immutableModelsRemainRecordsWithTheirComponentContracts() {
|
||||
assertRecordComponents(
|
||||
AttendanceActor.class,
|
||||
component("userId", long.class),
|
||||
component("role", AttendanceRole.class));
|
||||
assertRecordComponents(
|
||||
AttendanceDayContext.class,
|
||||
component("activeIntern", boolean.class),
|
||||
component("globalDayOff", boolean.class),
|
||||
component("approvedLeave", boolean.class));
|
||||
assertRecordComponents(
|
||||
AttendancePolicy.class,
|
||||
component("id", long.class),
|
||||
component("effectiveFrom", LocalDate.class),
|
||||
component("zoneId", ZoneId.class),
|
||||
component("scheduledStart", LocalTime.class),
|
||||
component("scheduledEnd", LocalTime.class),
|
||||
component("checkInGraceMinutes", int.class),
|
||||
component("checkoutGraceMinutes", int.class),
|
||||
component("monthlyLeaveQuota", int.class),
|
||||
component("violationPenalty", BigDecimal.class),
|
||||
component("workdays", Set.class));
|
||||
assertRecordComponents(
|
||||
AttendanceRecord.class,
|
||||
component("internId", long.class),
|
||||
component("workDate", LocalDate.class),
|
||||
component("policy", AttendancePolicy.class),
|
||||
component("checkInAt", Instant.class),
|
||||
component("checkOutAt", Instant.class));
|
||||
assertRecordComponents(
|
||||
AttendanceViolations.class,
|
||||
component("late", boolean.class),
|
||||
component("earlyDeparture", boolean.class),
|
||||
component("missingCheckout", boolean.class));
|
||||
assertRecordComponents(
|
||||
AttendanceHistoryItem.class,
|
||||
component("workDate", LocalDate.class),
|
||||
component("checkInAt", Instant.class),
|
||||
component("checkOutAt", Instant.class),
|
||||
component("policy", AttendancePolicy.class),
|
||||
component("violations", AttendanceViolations.class));
|
||||
assertRecordComponents(
|
||||
GlobalCalendarEvent.class,
|
||||
component("id", long.class),
|
||||
component("date", LocalDate.class),
|
||||
component("name", String.class),
|
||||
component("dayOff", boolean.class),
|
||||
component("version", long.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void entitiesExposeOnlyIntentionalPublicAndProtectedDeclaredMethods() {
|
||||
assertMethodSurface(
|
||||
AttendancePolicyEntity.class,
|
||||
method(Modifier.PUBLIC, "toDomain", AttendancePolicy.class));
|
||||
assertMethodSurface(
|
||||
AttendanceRecordEntity.class,
|
||||
method(Modifier.PUBLIC, "toDomain", AttendanceRecord.class),
|
||||
method(Modifier.PUBLIC, "setCheckOutAt", void.class, Instant.class),
|
||||
method(Modifier.PUBLIC, "workDate", LocalDate.class));
|
||||
assertMethodSurface(
|
||||
GlobalCalendarEventEntity.class,
|
||||
method(
|
||||
Modifier.PUBLIC,
|
||||
"update",
|
||||
void.class,
|
||||
LocalDate.class,
|
||||
String.class,
|
||||
boolean.class,
|
||||
long.class),
|
||||
method(Modifier.PUBLIC, "toDomain", GlobalCalendarEvent.class),
|
||||
method(Modifier.PUBLIC, "calendarDate", LocalDate.class),
|
||||
method(Modifier.PUBLIC, "version", long.class));
|
||||
assertMethodSurface(LeaveRequestDayEntity.class);
|
||||
assertMethodSurface(
|
||||
LeaveRequestDayId.class,
|
||||
method(Modifier.PUBLIC, "equals", boolean.class, Object.class),
|
||||
method(Modifier.PUBLIC, "hashCode", int.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(
|
||||
Class<?> type, ConstructorContract... expectedConstructors) {
|
||||
List<ConstructorContract> actual = Arrays.stream(type.getDeclaredConstructors())
|
||||
.map(ConstructorContract::from)
|
||||
.toList();
|
||||
|
||||
assertThat(actual).as(type.getName()).containsExactlyInAnyOrder(expectedConstructors);
|
||||
}
|
||||
|
||||
private static void assertRecordComponents(
|
||||
Class<?> type, RecordComponentContract... expectedComponents) {
|
||||
assertThat(type.isRecord()).as(type.getName()).isTrue();
|
||||
assertThat(Arrays.stream(type.getRecordComponents())
|
||||
.map(component -> new RecordComponentContract(component.getName(), component.getType()))
|
||||
.toList())
|
||||
.as(type.getName())
|
||||
.containsExactly(expectedComponents);
|
||||
}
|
||||
|
||||
private static void assertMethodSurface(Class<?> type, MethodContract... expectedMethods) {
|
||||
List<MethodContract> actual = Arrays.stream(type.getDeclaredMethods())
|
||||
.filter(method -> Modifier.isPublic(method.getModifiers())
|
||||
|| Modifier.isProtected(method.getModifiers()))
|
||||
.map(MethodContract::from)
|
||||
.toList();
|
||||
|
||||
assertThat(actual).as(type.getName()).containsExactlyInAnyOrder(expectedMethods);
|
||||
}
|
||||
|
||||
private static ConstructorContract constructor(int modifiers, Class<?>... parameterTypes) {
|
||||
return new ConstructorContract(modifiers, List.of(parameterTypes));
|
||||
}
|
||||
|
||||
private static RecordComponentContract component(String name, Class<?> type) {
|
||||
return new RecordComponentContract(name, type);
|
||||
}
|
||||
|
||||
private static MethodContract method(
|
||||
int modifiers, String name, Class<?> returnType, Class<?>... parameterTypes) {
|
||||
return new MethodContract(modifiers, name, returnType, List.of(parameterTypes));
|
||||
}
|
||||
|
||||
private record ConstructorContract(int modifiers, List<Class<?>> parameterTypes) {
|
||||
|
||||
private static ConstructorContract from(Constructor<?> constructor) {
|
||||
return new ConstructorContract(
|
||||
constructor.getModifiers(), List.of(constructor.getParameterTypes()));
|
||||
}
|
||||
}
|
||||
|
||||
private record RecordComponentContract(String name, Class<?> type) {}
|
||||
|
||||
private record MethodContract(
|
||||
int modifiers, String name, Class<?> returnType, List<Class<?>> parameterTypes) {
|
||||
|
||||
private static MethodContract from(Method method) {
|
||||
return new MethodContract(
|
||||
method.getModifiers(),
|
||||
method.getName(),
|
||||
method.getReturnType(),
|
||||
List.of(method.getParameterTypes()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user