feat: expose account identity and eligibility boundary
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
# Test Evidence: Cross-feature account boundary
|
||||||
|
|
||||||
|
- **Test type:** Integration
|
||||||
|
- **Requirement IDs:** `ACC-002, ACC-014, ACC-020–ACC-021, PRJ-017, ATT-007`
|
||||||
|
- **Scenario IDs:** `AC-ACC-002, AC-ATT-001`
|
||||||
|
- **Test class/method:** `com.lab.labtimesheet.feature.account.service.BootstrapIntegrationTest.exposesIdentityAndDateAwareInternEligibilityWithoutPersistenceTypes`
|
||||||
|
- **Implementation commit:** `this milestone commit`
|
||||||
|
|
||||||
|
## Protected behavior
|
||||||
|
|
||||||
|
Other features can resolve an account by normalized email or ID through an immutable identity DTO and can ask whether an Intern is active and within an inclusive internship interval for a supplied work date. They do not need access to account repositories or JPA entities.
|
||||||
|
|
||||||
|
## Test method
|
||||||
|
|
||||||
|
The PostgreSQL 18.4 integration test creates the initial Admin through the production bootstrap transaction, resolves the resulting identity through `AccountService`, and verifies ID/email equivalence, normalized lookup, role, status, and rejection by both current and date-aware Intern eligibility gates. Starting the context also parses the Spring Data derived interval query against the mapped `intern_profiles` entity.
|
||||||
|
|
||||||
|
## Hand-derived expected result
|
||||||
|
|
||||||
|
` ADMIN@EXAMPLE.COM ` resolves to the persisted `admin@example.com` identity. An active Admin is not an eligible Intern on `2026-08-14`. The date-aware gate requires an active Intern account, an `ACTIVE` internship, and `start_date <= workDate <= end_date`.
|
||||||
|
|
||||||
|
## RED
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
```text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="$JAVA_HOME/bin:/opt/homebrew/opt/node@24/bin:$PATH"
|
||||||
|
./mvnw -Dtest=BootstrapIntegrationTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
BootstrapIntegrationTest.java: method isEligibleIntern in class AccountService
|
||||||
|
cannot be applied to given types; required: long; found: long, java.time.LocalDate
|
||||||
|
Tests did not run; test compilation failed
|
||||||
|
BUILD FAILURE
|
||||||
|
```
|
||||||
|
|
||||||
|
## GREEN
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
```text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="$JAVA_HOME/bin:/opt/homebrew/opt/node@24/bin:$PATH"
|
||||||
|
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||||
|
./mvnw -Dtest=BootstrapIntegrationTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Affected suite
|
||||||
|
|
||||||
|
**Command and result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="$JAVA_HOME/bin:/opt/homebrew/opt/node@24/bin:$PATH"
|
||||||
|
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||||
|
./mvnw test
|
||||||
|
|
||||||
|
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
```
|
||||||
|
|
||||||
|
## External-test boundaries
|
||||||
|
|
||||||
|
The test proves identity lookup and rejection of a non-Intern plus successful repository-query initialization. The positive active-Intern and interval-edge cases remain part of I1-PLAT-06 activation/account lifecycle work; dependent features must still enforce their own authorization and transaction invariants.
|
||||||
@@ -53,7 +53,7 @@ export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
|||||||
**Observed result**
|
**Observed result**
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
|
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
|
||||||
BUILD SUCCESS
|
BUILD SUCCESS
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ BUILD SUCCESS
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
./mvnw test
|
./mvnw test
|
||||||
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
|
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
|
||||||
BUILD SUCCESS
|
BUILD SUCCESS
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export PATH="$JAVA_HOME/bin:/opt/homebrew/opt/node@24/bin:$PATH"
|
|||||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||||
./mvnw test
|
./mvnw test
|
||||||
|
|
||||||
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
|
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
|
||||||
BUILD SUCCESS
|
BUILD SUCCESS
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ BUILD SUCCESS
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
./mvnw test
|
./mvnw test
|
||||||
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
|
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
|
||||||
BUILD SUCCESS
|
BUILD SUCCESS
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export PATH="$JAVA_HOME/bin:/opt/homebrew/opt/node@24/bin:$PATH"
|
|||||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||||
./mvnw test
|
./mvnw test
|
||||||
|
|
||||||
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
|
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
|
||||||
BUILD SUCCESS
|
BUILD SUCCESS
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+5
@@ -1,9 +1,14 @@
|
|||||||
package com.lab.labtimesheet.feature.account.repository;
|
package com.lab.labtimesheet.feature.account.repository;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
import com.lab.labtimesheet.feature.account.model.InternshipStatus;
|
import com.lab.labtimesheet.feature.account.model.InternshipStatus;
|
||||||
import com.lab.labtimesheet.feature.account.model.entity.InternProfile;
|
import com.lab.labtimesheet.feature.account.model.entity.InternProfile;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
public interface InternProfileRepository extends JpaRepository<InternProfile, Long> {
|
public interface InternProfileRepository extends JpaRepository<InternProfile, Long> {
|
||||||
boolean existsByUserIdAndInternshipStatus(Long userId, InternshipStatus status);
|
boolean existsByUserIdAndInternshipStatus(Long userId, InternshipStatus status);
|
||||||
|
|
||||||
|
boolean existsByUserIdAndInternshipStatusAndInternshipStartDateLessThanEqualAndInternshipEndDateGreaterThanEqual(
|
||||||
|
Long userId, InternshipStatus status, LocalDate latestStartDate, LocalDate earliestEndDate);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.lab.labtimesheet.feature.account.service;
|
package com.lab.labtimesheet.feature.account.service;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
import com.lab.labtimesheet.feature.account.model.AccountStatus;
|
import com.lab.labtimesheet.feature.account.model.AccountStatus;
|
||||||
import com.lab.labtimesheet.feature.account.model.GlobalRole;
|
import com.lab.labtimesheet.feature.account.model.GlobalRole;
|
||||||
import com.lab.labtimesheet.feature.account.model.InternshipStatus;
|
import com.lab.labtimesheet.feature.account.model.InternshipStatus;
|
||||||
@@ -42,6 +44,20 @@ public class AccountService {
|
|||||||
.isPresent();
|
.isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public boolean isEligibleIntern(long userId, LocalDate workDate) {
|
||||||
|
if (workDate == null) {
|
||||||
|
throw new IllegalArgumentException("Work date is required");
|
||||||
|
}
|
||||||
|
return users.findById(userId)
|
||||||
|
.filter(user -> user.getGlobalRole() == GlobalRole.INTERN)
|
||||||
|
.filter(user -> user.getAccountStatus() == AccountStatus.ACTIVE)
|
||||||
|
.filter(user -> internProfiles
|
||||||
|
.existsByUserIdAndInternshipStatusAndInternshipStartDateLessThanEqualAndInternshipEndDateGreaterThanEqual(
|
||||||
|
user.getId(), InternshipStatus.ACTIVE, workDate, workDate))
|
||||||
|
.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public AccountIdentity requireEligibleIntern(long userId) {
|
public AccountIdentity requireEligibleIntern(long userId) {
|
||||||
if (!isEligibleIntern(userId)) {
|
if (!isEligibleIntern(userId)) {
|
||||||
|
|||||||
+15
-8
@@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
@@ -80,17 +81,23 @@ class BootstrapIntegrationTest {
|
|||||||
BootstrapService.BootstrapOutcome.CREATED, BootstrapService.BootstrapOutcome.ALREADY_INITIALIZED);
|
BootstrapService.BootstrapOutcome.CREATED, BootstrapService.BootstrapOutcome.ALREADY_INITIALIZED);
|
||||||
assertThat(users.count()).isEqualTo(1);
|
assertThat(users.count()).isEqualTo(1);
|
||||||
assertThat(users.countByGlobalRoleAndAccountStatus(GlobalRole.ADMIN, AccountStatus.ACTIVE)).isEqualTo(1);
|
assertThat(users.countByGlobalRoleAndAccountStatus(GlobalRole.ADMIN, AccountStatus.ACTIVE)).isEqualTo(1);
|
||||||
var createdUser = users.findAll().getFirst();
|
|
||||||
var identityByEmail = accountService.requireIdentityByEmail(" " + createdUser.getEmail().toUpperCase() + " ");
|
|
||||||
assertThat(identityByEmail.email()).isEqualTo(createdUser.getEmail());
|
|
||||||
assertThat(identityByEmail.displayName()).isEqualTo("First Admin");
|
|
||||||
assertThat(identityByEmail.role()).isEqualTo(GlobalRole.ADMIN);
|
|
||||||
assertThat(identityByEmail.status()).isEqualTo(AccountStatus.ACTIVE);
|
|
||||||
assertThat(accountService.requireIdentityById(identityByEmail.id())).isEqualTo(identityByEmail);
|
|
||||||
assertThat(accountService.isEligibleIntern(identityByEmail.id())).isFalse();
|
|
||||||
assertThat(bootstrapService.bootstrap(
|
assertThat(bootstrapService.bootstrap(
|
||||||
"another@example.com", "Another", "correct horse battery staple"))
|
"another@example.com", "Another", "correct horse battery staple"))
|
||||||
.isEqualTo(BootstrapService.BootstrapOutcome.ALREADY_INITIALIZED);
|
.isEqualTo(BootstrapService.BootstrapOutcome.ALREADY_INITIALIZED);
|
||||||
assertThat(systemStates.findById((short) 1).orElseThrow().isInitialized()).isTrue();
|
assertThat(systemStates.findById((short) 1).orElseThrow().isInitialized()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exposesIdentityAndDateAwareInternEligibilityWithoutPersistenceTypes() {
|
||||||
|
bootstrapService.bootstrap("admin@example.com", "First Admin", "correct horse battery staple");
|
||||||
|
|
||||||
|
var identityByEmail = accountService.requireIdentityByEmail(" ADMIN@EXAMPLE.COM ");
|
||||||
|
assertThat(identityByEmail.email()).isEqualTo("admin@example.com");
|
||||||
|
assertThat(identityByEmail.displayName()).isEqualTo("First Admin");
|
||||||
|
assertThat(identityByEmail.role()).isEqualTo(GlobalRole.ADMIN);
|
||||||
|
assertThat(identityByEmail.status()).isEqualTo(AccountStatus.ACTIVE);
|
||||||
|
assertThat(accountService.requireIdentityById(identityByEmail.id())).isEqualTo(identityByEmail);
|
||||||
|
assertThat(accountService.isEligibleIntern(identityByEmail.id())).isFalse();
|
||||||
|
assertThat(accountService.isEligibleIntern(identityByEmail.id(), LocalDate.of(2026, 8, 14))).isFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user