Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
159e634ffc |
@@ -1,161 +0,0 @@
|
|||||||
# Test Evidence: Eligible Intern picker query
|
|
||||||
|
|
||||||
- **Test type:** Integration
|
|
||||||
- **Requirement IDs:** ACC-014, ACC-019–ACC-021, AUTH-001, PRJ-017, TST-001–TST-010
|
|
||||||
- **Scenario IDs:** AC-ACC-009, AC-ACC-010, AC-PRJ-010 (selection-eligibility support)
|
|
||||||
- **Test class/method:** com.lab.labtimesheet.feature.account.service.EligibleInternOptionIntegrationTest#listsOnlyActiveInternsWithActiveInclusiveInternshipsInPickerOrder; #rejectsMissingBusinessDate
|
|
||||||
- **Implementation commit:** e70159a81b6445825f6d5f912ecf3c4aa3c1aa85
|
|
||||||
|
|
||||||
## Protected behavior
|
|
||||||
|
|
||||||
Pending, locked, deactivated, non-Intern, not-started, completed, and date-expired records must not appear in the
|
|
||||||
Account-owned Intern picker. An option is selectable only when both account and internship are ACTIVE and the
|
|
||||||
explicit business date lies within the inclusive internship range. The returned numeric user ID is the internal
|
|
||||||
submission identity, and options sort by display name then student code.
|
|
||||||
The public query rejects a missing business date with the documented actionable message instead of issuing an
|
|
||||||
ambiguous null-bound database query.
|
|
||||||
|
|
||||||
## Test method
|
|
||||||
|
|
||||||
The PostgreSQL 18.4 integration test persists valid account/profile combinations through the account feature's JPA
|
|
||||||
entities and repositories. It uses SQL only as a test fixture for future lock, deactivation, and completion states
|
|
||||||
whose production transitions are outside this change. It calls the public Account service query and compares the
|
|
||||||
complete immutable DTO sequence, including both inclusive date boundaries and unique user IDs.
|
|
||||||
Its separate null-date regression calls the same public service method and asserts the exact
|
|
||||||
<code>IllegalArgumentException</code> message documented by that method.
|
|
||||||
|
|
||||||
## Hand-derived expected result
|
|
||||||
|
|
||||||
For business date 2026-08-14, profiles starting on that date and ending on that date remain eligible. The only
|
|
||||||
expected options are Alpha / STU-100, Alpha / STU-200, and Zeta / STU-300, in that order. Every other seeded
|
|
||||||
row fails at least one account role/state, internship state, or inclusive date condition.
|
|
||||||
For a missing business date, the service must immediately throw
|
|
||||||
<code>IllegalArgumentException("Business date is required")</code>.
|
|
||||||
|
|
||||||
## RED
|
|
||||||
|
|
||||||
**Command**
|
|
||||||
|
|
||||||
~~~text
|
|
||||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
|
||||||
export PATH="$JAVA_HOME/bin:$PATH"
|
|
||||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
|
||||||
./mvnw -Dtest=EligibleInternOptionIntegrationTest test
|
|
||||||
~~~
|
|
||||||
|
|
||||||
**Observed result**
|
|
||||||
|
|
||||||
~~~text
|
|
||||||
[ERROR] EligibleInternOptionIntegrationTest.java:[11,54] cannot find symbol
|
|
||||||
symbol: class EligibleInternOption
|
|
||||||
location: package com.lab.labtimesheet.feature.account.model.dto
|
|
||||||
BUILD FAILURE
|
|
||||||
~~~
|
|
||||||
|
|
||||||
## GREEN
|
|
||||||
|
|
||||||
**Command**
|
|
||||||
|
|
||||||
~~~text
|
|
||||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
|
||||||
export PATH="$JAVA_HOME/bin:$PATH"
|
|
||||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
|
||||||
./mvnw -Dtest=EligibleInternOptionIntegrationTest test
|
|
||||||
~~~
|
|
||||||
|
|
||||||
**Observed result**
|
|
||||||
|
|
||||||
~~~text
|
|
||||||
PostgreSQL 18.4 Testcontainers started and Flyway applied V1 baseline.
|
|
||||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
|
||||||
BUILD SUCCESS
|
|
||||||
~~~
|
|
||||||
|
|
||||||
## Review follow-up: missing business date
|
|
||||||
|
|
||||||
The public guard was temporarily removed solely to prove the new regression fails for the intended reason, then
|
|
||||||
restored exactly before the GREEN checks. The follow-up commit contains only the regression test and evidence.
|
|
||||||
|
|
||||||
### RED
|
|
||||||
|
|
||||||
**Command**
|
|
||||||
|
|
||||||
~~~text
|
|
||||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
|
||||||
export PATH="$JAVA_HOME/bin:$PATH"
|
|
||||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
|
||||||
./mvnw '-Dtest=EligibleInternOptionIntegrationTest#rejectsMissingBusinessDate' test
|
|
||||||
~~~
|
|
||||||
|
|
||||||
**Observed result**
|
|
||||||
|
|
||||||
~~~text
|
|
||||||
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
|
|
||||||
java.lang.AssertionError: Expecting code to raise a throwable.
|
|
||||||
BUILD FAILURE
|
|
||||||
~~~
|
|
||||||
|
|
||||||
### GREEN
|
|
||||||
|
|
||||||
**Command**
|
|
||||||
|
|
||||||
~~~text
|
|
||||||
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
|
||||||
export PATH="$JAVA_HOME/bin:$PATH"
|
|
||||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
|
||||||
./mvnw '-Dtest=EligibleInternOptionIntegrationTest#rejectsMissingBusinessDate' test
|
|
||||||
~~~
|
|
||||||
|
|
||||||
**Observed result**
|
|
||||||
|
|
||||||
~~~text
|
|
||||||
PostgreSQL 18.4 Testcontainers started and Flyway applied V1 baseline.
|
|
||||||
Tests run: 1, 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:$PATH"
|
|
||||||
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
|
||||||
./mvnw -Dtest=EligibleInternOptionIntegrationTest,AccountActivationIntegrationTest,BootstrapIntegrationTest,AccountWebIntegrationTest,AuthenticationWebIntegrationTest,BootstrapOnboardingWebIntegrationTest test
|
|
||||||
|
|
||||||
Selected account reports: 13 tests, 0 failures, 0 errors, 0 skipped.
|
|
||||||
|
|
||||||
./mvnw -Dtest=AccountActivationIntegrationTest test
|
|
||||||
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
|
|
||||||
BUILD SUCCESS
|
|
||||||
|
|
||||||
./mvnw -Dtest=LayerStructureTest test
|
|
||||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
|
||||||
BUILD SUCCESS
|
|
||||||
|
|
||||||
./mvnw test
|
|
||||||
Tests run: 105, Failures: 0, Errors: 0, Skipped: 0
|
|
||||||
BUILD SUCCESS
|
|
||||||
~~~
|
|
||||||
|
|
||||||
### Review follow-up affected account-service checks
|
|
||||||
|
|
||||||
~~~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=EligibleInternOptionIntegrationTest,AccountActivationIntegrationTest,BootstrapIntegrationTest' test
|
|
||||||
|
|
||||||
EligibleInternOptionIntegrationTest: 2 tests, 0 failures, 0 errors, 0 skipped
|
|
||||||
BootstrapIntegrationTest: 4 tests, 0 failures, 0 errors, 0 skipped
|
|
||||||
AccountActivationIntegrationTest: 2 tests, 0 failures, 0 errors, 0 skipped
|
|
||||||
Selected account-service reports: 8 tests, 0 failures, 0 errors, 0 skipped.
|
|
||||||
BUILD SUCCESS
|
|
||||||
~~~
|
|
||||||
|
|
||||||
## External-test boundaries
|
|
||||||
|
|
||||||
This query does not authorize Project membership itself; the consuming Project transaction must still recheck
|
|
||||||
membership and ownership invariants. It does not test the later lifecycle mutation workflows that produce locked,
|
|
||||||
deactivated, or completed rows.
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Test Evidence: dark icon sprite presentation
|
||||||
|
|
||||||
|
- **Test type:** Web
|
||||||
|
- **Requirement IDs:** `UI-006`, `UI-009`, `UI-010`, `UI-018`
|
||||||
|
- **Scenario IDs:** `AC-UI-003`, `AC-UI-005`
|
||||||
|
- **Test class/method:** `com.lab.labtimesheet.ui.UiContractWebTest#generatedLucideSymbolsRetainCurrentColorStrokePresentation`
|
||||||
|
- **Implementation commit:** `pending`
|
||||||
|
|
||||||
|
## Protected behavior
|
||||||
|
|
||||||
|
Every local Lucide sprite symbol retains the source presentation attributes so icons referenced with `<use>` inherit `currentColor` rather than rendering with the SVG default black fill on dark surfaces.
|
||||||
|
|
||||||
|
## Test method
|
||||||
|
|
||||||
|
The focused web contract reads the generated classpath sprite, scans every emitted `<symbol>`, and checks the five presentation attributes on each symbol. It checks the deployable generated artifact rather than generator source text.
|
||||||
|
|
||||||
|
## Hand-derived expected result
|
||||||
|
|
||||||
|
Lucide 1.27.0 line icons use `fill="none"`, `stroke="currentColor"`, `stroke-width="2"`, `stroke-linecap="round"`, and `stroke-linejoin="round"` on their SVG root. Each selected generated symbol must preserve those values.
|
||||||
|
|
||||||
|
## RED
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
```text
|
||||||
|
env JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ./mvnw '-Dtest=UiContractWebTest#generatedLucideSymbolsRetainCurrentColorStrokePresentation' test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
UiContractWebTest.generatedLucideSymbolsRetainCurrentColorStrokePresentation
|
||||||
|
Missing fill on id="bell" viewBox="0 0 24 24" ==> expected: <true> but was: <false>
|
||||||
|
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
|
||||||
|
BUILD FAILURE
|
||||||
|
```
|
||||||
|
|
||||||
|
## GREEN
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
```text
|
||||||
|
env PATH=/opt/homebrew/opt/node@24/bin:$PATH npm ci
|
||||||
|
env PATH=/opt/homebrew/opt/node@24/bin:$PATH npm run build
|
||||||
|
env JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ./mvnw '-Dtest=UiContractWebTest#generatedLucideSymbolsRetainCurrentColorStrokePresentation' test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Node v24.19.0 and npm 11.17.0 installed the locked dependencies.
|
||||||
|
Tailwind CSS v4.3.3 rebuilt app.css and build-icons regenerated icons.svg.
|
||||||
|
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Affected suite
|
||||||
|
|
||||||
|
**Command and result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
env JAVA_HOME=/opt/homebrew/opt/openjdk@25 PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ./mvnw '-Dtest=UiContractWebTest' test
|
||||||
|
|
||||||
|
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
```
|
||||||
|
|
||||||
|
## External-test boundaries
|
||||||
|
|
||||||
|
The deterministic asset contract proves the generated sprite carries theme-aware Lucide presentation attributes. It does not replace the taskmaster-owned integrated browser/detector pass for rendered layout and interactive states.
|
||||||
@@ -10,10 +10,14 @@ const names = [
|
|||||||
const output = resolve('src/main/resources/static/assets/icons.svg');
|
const output = resolve('src/main/resources/static/assets/icons.svg');
|
||||||
const symbols = await Promise.all(names.map(async (name) => {
|
const symbols = await Promise.all(names.map(async (name) => {
|
||||||
const svg = await readFile(resolve(`node_modules/lucide-static/icons/${name}.svg`), 'utf8');
|
const svg = await readFile(resolve(`node_modules/lucide-static/icons/${name}.svg`), 'utf8');
|
||||||
const viewBox = svg.match(/viewBox="([^"]+)"/)?.[1] ?? '0 0 24 24';
|
const root = svg.match(/<svg\b([^>]*)>/)?.[1];
|
||||||
|
const viewBox = root?.match(/viewBox="([^"]+)"/)?.[1] ?? '0 0 24 24';
|
||||||
|
const presentation = ['fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin']
|
||||||
|
.map((attribute) => root?.match(new RegExp(`${attribute}="[^"]+"`))?.[0])
|
||||||
|
.join(' ');
|
||||||
const body = svg.match(/<svg[\s\S]*?>([\s\S]*?)<\/svg>/)?.[1];
|
const body = svg.match(/<svg[\s\S]*?>([\s\S]*?)<\/svg>/)?.[1];
|
||||||
if (!body) throw new Error(`Invalid Lucide SVG: ${name}`);
|
if (!body) throw new Error(`Invalid Lucide SVG: ${name}`);
|
||||||
return `<symbol id="${name}" viewBox="${viewBox}">${body.trim()}</symbol>`;
|
return `<symbol id="${name}" viewBox="${viewBox}" ${presentation}>${body.trim()}</symbol>`;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await mkdir(dirname(output), { recursive: true });
|
await mkdir(dirname(output), { recursive: true });
|
||||||
|
|||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
package com.lab.labtimesheet.feature.account.model.dto;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Immutable non-secret selection data for an eligible Intern.
|
|
||||||
* The numeric user ID is the internal form submission identity; displayed fields are not authorization identifiers.
|
|
||||||
*
|
|
||||||
* @param userId persistent account identifier submitted by a consuming form
|
|
||||||
* @param displayName user-facing Intern name
|
|
||||||
* @param studentCode university student code shown to distinguish Interns
|
|
||||||
* @param internshipStart inclusive internship eligibility start date
|
|
||||||
* @param internshipEnd inclusive internship eligibility end date
|
|
||||||
*/
|
|
||||||
public record EligibleInternOption(
|
|
||||||
long userId,
|
|
||||||
String displayName,
|
|
||||||
String studentCode,
|
|
||||||
LocalDate internshipStart,
|
|
||||||
LocalDate internshipEnd) {
|
|
||||||
}
|
|
||||||
-32
@@ -1,12 +1,8 @@
|
|||||||
package com.lab.labtimesheet.feature.account.repository;
|
package com.lab.labtimesheet.feature.account.repository;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.lab.labtimesheet.feature.account.model.AccountStatus;
|
|
||||||
import com.lab.labtimesheet.feature.account.model.GlobalRole;
|
|
||||||
import com.lab.labtimesheet.feature.account.model.InternshipStatus;
|
import com.lab.labtimesheet.feature.account.model.InternshipStatus;
|
||||||
import com.lab.labtimesheet.feature.account.model.dto.EligibleInternOption;
|
|
||||||
import com.lab.labtimesheet.feature.account.model.entity.InternProfile;
|
import com.lab.labtimesheet.feature.account.model.entity.InternProfile;
|
||||||
import jakarta.persistence.LockModeType;
|
import jakarta.persistence.LockModeType;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
@@ -23,34 +19,6 @@ public interface InternProfileRepository extends JpaRepository<InternProfile, Lo
|
|||||||
boolean existsByUserIdAndInternshipStatusAndInternshipStartDateLessThanEqualAndInternshipEndDateGreaterThanEqual(
|
boolean existsByUserIdAndInternshipStatusAndInternshipStartDateLessThanEqualAndInternshipEndDateGreaterThanEqual(
|
||||||
Long userId, InternshipStatus status, LocalDate latestStartDate, LocalDate earliestEndDate);
|
Long userId, InternshipStatus status, LocalDate latestStartDate, LocalDate earliestEndDate);
|
||||||
|
|
||||||
/**
|
|
||||||
* Projects account-owned non-secret selection data for Interns eligible on one inclusive business date.
|
|
||||||
* Results are ordered by display name, student code, then user ID for deterministic form rendering.
|
|
||||||
*
|
|
||||||
* @param globalRole required immutable Intern role
|
|
||||||
* @param accountStatus required active account state
|
|
||||||
* @param internshipStatus required active internship state
|
|
||||||
* @param businessDate date that must fall within the inclusive internship range
|
|
||||||
* @return eligible Intern selection projections without duplicate profile rows
|
|
||||||
*/
|
|
||||||
@Query("""
|
|
||||||
select new com.lab.labtimesheet.feature.account.model.dto.EligibleInternOption(
|
|
||||||
u.id, u.displayName, p.studentCode, p.internshipStartDate, p.internshipEndDate)
|
|
||||||
from InternProfile p
|
|
||||||
join AppUser u on u.id = p.userId
|
|
||||||
where u.globalRole = :globalRole
|
|
||||||
and u.accountStatus = :accountStatus
|
|
||||||
and p.internshipStatus = :internshipStatus
|
|
||||||
and p.internshipStartDate <= :businessDate
|
|
||||||
and p.internshipEndDate >= :businessDate
|
|
||||||
order by u.displayName asc, p.studentCode asc, u.id asc
|
|
||||||
""")
|
|
||||||
List<EligibleInternOption> findEligibleInternOptions(
|
|
||||||
@Param("globalRole") GlobalRole globalRole,
|
|
||||||
@Param("accountStatus") AccountStatus accountStatus,
|
|
||||||
@Param("internshipStatus") InternshipStatus internshipStatus,
|
|
||||||
@Param("businessDate") LocalDate businessDate);
|
|
||||||
|
|
||||||
/** Counts Intern profiles in a lifecycle state. */
|
/** Counts Intern profiles in a lifecycle state. */
|
||||||
long countByInternshipStatus(InternshipStatus status);
|
long countByInternshipStatus(InternshipStatus status);
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import java.time.Clock;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -18,7 +17,6 @@ import com.lab.labtimesheet.feature.account.model.dto.AccountCreation;
|
|||||||
import com.lab.labtimesheet.feature.account.model.dto.AccountIdentity;
|
import com.lab.labtimesheet.feature.account.model.dto.AccountIdentity;
|
||||||
import com.lab.labtimesheet.feature.account.model.dto.AccountSummary;
|
import com.lab.labtimesheet.feature.account.model.dto.AccountSummary;
|
||||||
import com.lab.labtimesheet.feature.account.model.dto.CreateAccountCommand;
|
import com.lab.labtimesheet.feature.account.model.dto.CreateAccountCommand;
|
||||||
import com.lab.labtimesheet.feature.account.model.dto.EligibleInternOption;
|
|
||||||
import com.lab.labtimesheet.feature.account.model.entity.AppUser;
|
import com.lab.labtimesheet.feature.account.model.entity.AppUser;
|
||||||
import com.lab.labtimesheet.feature.account.model.entity.InternProfile;
|
import com.lab.labtimesheet.feature.account.model.entity.InternProfile;
|
||||||
import com.lab.labtimesheet.feature.account.model.entity.UserActionToken;
|
import com.lab.labtimesheet.feature.account.model.entity.UserActionToken;
|
||||||
@@ -240,24 +238,6 @@ public class AccountService {
|
|||||||
.isPresent();
|
.isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists non-secret Intern selection options eligible on an explicit business date.
|
|
||||||
* The result requires active account and internship states plus inclusive internship dates, but it does not
|
|
||||||
* authorize a consuming Project operation; that operation must recheck its own ownership and membership rules.
|
|
||||||
*
|
|
||||||
* @param businessDate server-derived business date to evaluate inclusively
|
|
||||||
* @return deterministic options ordered by display name, student code, then account ID
|
|
||||||
* @throws IllegalArgumentException when {@code businessDate} is {@code null}
|
|
||||||
*/
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public List<EligibleInternOption> eligibleInternOptions(LocalDate businessDate) {
|
|
||||||
if (businessDate == null) {
|
|
||||||
throw new IllegalArgumentException("Business date is required");
|
|
||||||
}
|
|
||||||
return internProfiles.findEligibleInternOptions(
|
|
||||||
GlobalRole.INTERN, AccountStatus.ACTIVE, InternshipStatus.ACTIVE, businessDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the cross-feature identity of a currently eligible Intern.
|
* Resolves the cross-feature identity of a currently eligible Intern.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none"><symbol id="bell" viewBox="0 0 24 24"><path d="M10.268 21a2 2 0 0 0 3.464 0" />
|
<svg xmlns="http://www.w3.org/2000/svg" style="display:none"><symbol id="bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" />
|
||||||
<path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol><symbol id="calendar-days" viewBox="0 0 24 24"><path d="M8 2v4" />
|
<path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol><symbol id="calendar-days" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4" />
|
||||||
<path d="M16 2v4" />
|
<path d="M16 2v4" />
|
||||||
<rect width="18" height="18" x="3" y="4" rx="2" />
|
<rect width="18" height="18" x="3" y="4" rx="2" />
|
||||||
<path d="M3 10h18" />
|
<path d="M3 10h18" />
|
||||||
@@ -8,27 +8,27 @@
|
|||||||
<path d="M16 14h.01" />
|
<path d="M16 14h.01" />
|
||||||
<path d="M8 18h.01" />
|
<path d="M8 18h.01" />
|
||||||
<path d="M12 18h.01" />
|
<path d="M12 18h.01" />
|
||||||
<path d="M16 18h.01" /></symbol><symbol id="check-circle-2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" />
|
<path d="M16 18h.01" /></symbol><symbol id="check-circle-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" />
|
||||||
<path d="m9 12 2 2 4-4" /></symbol><symbol id="chevron-left" viewBox="0 0 24 24"><path d="m15 18-6-6 6-6" /></symbol><symbol id="chevron-right" viewBox="0 0 24 24"><path d="m9 18 6-6-6-6" /></symbol><symbol id="circle-user-round" viewBox="0 0 24 24"><path d="M17.925 20.056a6 6 0 0 0-11.851.001" />
|
<path d="m9 12 2 2 4-4" /></symbol><symbol id="chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6" /></symbol><symbol id="chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></symbol><symbol id="circle-user-round" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.925 20.056a6 6 0 0 0-11.851.001" />
|
||||||
<circle cx="12" cy="11" r="4" />
|
<circle cx="12" cy="11" r="4" />
|
||||||
<circle cx="12" cy="12" r="10" /></symbol><symbol id="clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" /></symbol><symbol id="clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" />
|
||||||
<path d="M12 6v6l4 2" /></symbol><symbol id="folder-kanban" viewBox="0 0 24 24"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z" />
|
<path d="M12 6v6l4 2" /></symbol><symbol id="folder-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z" />
|
||||||
<path d="M8 10v4" />
|
<path d="M8 10v4" />
|
||||||
<path d="M12 10v2" />
|
<path d="M12 10v2" />
|
||||||
<path d="M16 10v6" /></symbol><symbol id="folder-open" viewBox="0 0 24 24"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2" /></symbol><symbol id="inbox" viewBox="0 0 24 24"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
|
<path d="M16 10v6" /></symbol><symbol id="folder-open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2" /></symbol><symbol id="inbox" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
|
||||||
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /></symbol><symbol id="layout-dashboard" viewBox="0 0 24 24"><rect width="7" height="9" x="3" y="3" rx="1" />
|
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /></symbol><symbol id="layout-dashboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1" />
|
||||||
<rect width="7" height="5" x="14" y="3" rx="1" />
|
<rect width="7" height="5" x="14" y="3" rx="1" />
|
||||||
<rect width="7" height="9" x="14" y="12" rx="1" />
|
<rect width="7" height="9" x="14" y="12" rx="1" />
|
||||||
<rect width="7" height="5" x="3" y="16" rx="1" /></symbol><symbol id="list-check" viewBox="0 0 24 24"><path d="M16 5H3" />
|
<rect width="7" height="5" x="3" y="16" rx="1" /></symbol><symbol id="list-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 5H3" />
|
||||||
<path d="M16 12H3" />
|
<path d="M16 12H3" />
|
||||||
<path d="M11 19H3" />
|
<path d="M11 19H3" />
|
||||||
<path d="m15 18 2 2 4-4" /></symbol><symbol id="log-out" viewBox="0 0 24 24"><path d="m16 17 5-5-5-5" />
|
<path d="m15 18 2 2 4-4" /></symbol><symbol id="log-out" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 17 5-5-5-5" />
|
||||||
<path d="M21 12H9" />
|
<path d="M21 12H9" />
|
||||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /></symbol><symbol id="monitor" viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="3" rx="2" />
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /></symbol><symbol id="monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2" />
|
||||||
<line x1="8" x2="16" y1="21" y2="21" />
|
<line x1="8" x2="16" y1="21" y2="21" />
|
||||||
<line x1="12" x2="12" y1="17" y2="21" /></symbol><symbol id="moon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" /></symbol><symbol id="panel-left" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2" />
|
<line x1="12" x2="12" y1="17" y2="21" /></symbol><symbol id="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" /></symbol><symbol id="panel-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
<path d="M9 3v18" /></symbol><symbol id="settings" viewBox="0 0 24 24"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
|
<path d="M9 3v18" /></symbol><symbol id="settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
|
||||||
<circle cx="12" cy="12" r="3" /></symbol><symbol id="sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4" />
|
<circle cx="12" cy="12" r="3" /></symbol><symbol id="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4" />
|
||||||
<path d="M12 2v2" />
|
<path d="M12 2v2" />
|
||||||
<path d="M12 20v2" />
|
<path d="M12 20v2" />
|
||||||
<path d="m4.93 4.93 1.41 1.41" />
|
<path d="m4.93 4.93 1.41 1.41" />
|
||||||
@@ -36,10 +36,10 @@
|
|||||||
<path d="M2 12h2" />
|
<path d="M2 12h2" />
|
||||||
<path d="M20 12h2" />
|
<path d="M20 12h2" />
|
||||||
<path d="m6.34 17.66-1.41 1.41" />
|
<path d="m6.34 17.66-1.41 1.41" />
|
||||||
<path d="m19.07 4.93-1.41 1.41" /></symbol><symbol id="triangle-alert" viewBox="0 0 24 24"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
<path d="m19.07 4.93-1.41 1.41" /></symbol><symbol id="triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
||||||
<path d="M12 9v4" />
|
<path d="M12 9v4" />
|
||||||
<path d="M12 17h.01" /></symbol><symbol id="users" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
<path d="M12 17h.01" /></symbol><symbol id="users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||||
<path d="M16 3.128a4 4 0 0 1 0 7.744" />
|
<path d="M16 3.128a4 4 0 0 1 0 7.744" />
|
||||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
|
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
|
||||||
<circle cx="9" cy="7" r="4" /></symbol><symbol id="x" viewBox="0 0 24 24"><path d="M18 6 6 18" />
|
<circle cx="9" cy="7" r="4" /></symbol><symbol id="x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" />
|
||||||
<path d="m6 6 12 12" /></symbol></svg>
|
<path d="m6 6 12 12" /></symbol></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 6.1 KiB |
-165
@@ -1,165 +0,0 @@
|
|||||||
package com.lab.labtimesheet.feature.account.service;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
||||||
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.lab.labtimesheet.config.TestcontainersConfiguration;
|
|
||||||
import com.lab.labtimesheet.feature.account.model.GlobalRole;
|
|
||||||
import com.lab.labtimesheet.feature.account.model.dto.EligibleInternOption;
|
|
||||||
import com.lab.labtimesheet.feature.account.model.entity.AppUser;
|
|
||||||
import com.lab.labtimesheet.feature.account.model.entity.InternProfile;
|
|
||||||
import com.lab.labtimesheet.feature.account.repository.AppUserRepository;
|
|
||||||
import com.lab.labtimesheet.feature.account.repository.InternProfileRepository;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.context.annotation.Import;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.test.annotation.DirtiesContext;
|
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
|
||||||
|
|
||||||
@Import(TestcontainersConfiguration.class)
|
|
||||||
@SpringBootTest
|
|
||||||
@ActiveProfiles("test")
|
|
||||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
|
||||||
class EligibleInternOptionIntegrationTest {
|
|
||||||
private static final Instant NOW = Instant.parse("2026-08-14T00:00:00Z");
|
|
||||||
private static final LocalDate BUSINESS_DATE = LocalDate.of(2026, 8, 14);
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AccountService accounts;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AppUserRepository users;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private InternProfileRepository internProfiles;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private JdbcTemplate jdbc;
|
|
||||||
|
|
||||||
private AppUser admin;
|
|
||||||
private int userSequence;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
admin = users.saveAndFlush(AppUser.bootstrapAdmin(
|
|
||||||
"picker-admin@example.com", "Picker Admin", "encoded-password", NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void listsOnlyActiveInternsWithActiveInclusiveInternshipsInPickerOrder() {
|
|
||||||
long lowerBoundary = activeIntern("Alpha", "STU-100", BUSINESS_DATE, BUSINESS_DATE.plusDays(10));
|
|
||||||
long upperBoundary = activeIntern("Alpha", "STU-200", BUSINESS_DATE.minusDays(10), BUSINESS_DATE);
|
|
||||||
long laterName = activeIntern("Zeta", "STU-300", BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1));
|
|
||||||
|
|
||||||
pendingInternWithActiveProfile("Ignored Pending", "STU-400");
|
|
||||||
long locked = activeIntern("Ignored Locked", "STU-500", BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1));
|
|
||||||
lock(locked);
|
|
||||||
long deactivated = activeIntern(
|
|
||||||
"Ignored Deactivated", "STU-600", BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1));
|
|
||||||
deactivate(deactivated);
|
|
||||||
activeMentorWithActiveProfile("Ignored Mentor", "STU-700");
|
|
||||||
activeInternWithNotStartedProfile("Ignored Not Started", "STU-800");
|
|
||||||
activeIntern("Ignored Ended", "STU-900", BUSINESS_DATE.minusDays(10), BUSINESS_DATE.minusDays(1));
|
|
||||||
long completed = activeIntern(
|
|
||||||
"Ignored Completed", "STU-1000", BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1));
|
|
||||||
completeInternship(completed);
|
|
||||||
|
|
||||||
List<EligibleInternOption> options = accounts.eligibleInternOptions(BUSINESS_DATE);
|
|
||||||
|
|
||||||
assertThat(options).containsExactly(
|
|
||||||
new EligibleInternOption(
|
|
||||||
lowerBoundary, "Alpha", "STU-100", BUSINESS_DATE, BUSINESS_DATE.plusDays(10)),
|
|
||||||
new EligibleInternOption(
|
|
||||||
upperBoundary, "Alpha", "STU-200", BUSINESS_DATE.minusDays(10), BUSINESS_DATE),
|
|
||||||
new EligibleInternOption(
|
|
||||||
laterName, "Zeta", "STU-300", BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1)));
|
|
||||||
assertThat(options).extracting(EligibleInternOption::userId).doesNotHaveDuplicates();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void rejectsMissingBusinessDate() {
|
|
||||||
assertThatThrownBy(() -> accounts.eligibleInternOptions(null))
|
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
|
||||||
.hasMessage("Business date is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
private long activeIntern(String displayName, String studentCode, LocalDate startDate, LocalDate endDate) {
|
|
||||||
long userId = activeUser(GlobalRole.INTERN, displayName);
|
|
||||||
activeProfile(userId, studentCode, startDate, endDate);
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void pendingInternWithActiveProfile(String displayName, String studentCode) {
|
|
||||||
long userId = pendingUser(GlobalRole.INTERN, displayName);
|
|
||||||
activeProfile(userId, studentCode, BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void activeMentorWithActiveProfile(String displayName, String studentCode) {
|
|
||||||
long userId = activeUser(GlobalRole.MENTOR, displayName);
|
|
||||||
activeProfile(userId, studentCode, BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void activeInternWithNotStartedProfile(String displayName, String studentCode) {
|
|
||||||
long userId = activeUser(GlobalRole.INTERN, displayName);
|
|
||||||
internProfiles.saveAndFlush(InternProfile.notStarted(
|
|
||||||
userId, studentCode, BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1), NOW));
|
|
||||||
}
|
|
||||||
|
|
||||||
private long activeUser(GlobalRole role, String displayName) {
|
|
||||||
AppUser user = AppUser.pending(nextEmail(), displayName, role, admin, NOW);
|
|
||||||
user.activate("encoded-password", NOW);
|
|
||||||
return users.saveAndFlush(user).getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
private long pendingUser(GlobalRole role, String displayName) {
|
|
||||||
return users.saveAndFlush(AppUser.pending(nextEmail(), displayName, role, admin, NOW)).getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void activeProfile(long userId, String studentCode, LocalDate startDate, LocalDate endDate) {
|
|
||||||
InternProfile profile = InternProfile.notStarted(userId, studentCode, startDate, endDate, NOW);
|
|
||||||
profile.activate(NOW);
|
|
||||||
internProfiles.saveAndFlush(profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void lock(long userId) {
|
|
||||||
assertThat(jdbc.update(
|
|
||||||
"""
|
|
||||||
update app_users
|
|
||||||
set account_status = 'LOCKED', locked_at = ?, updated_at = ?
|
|
||||||
where id = ?
|
|
||||||
""",
|
|
||||||
Timestamp.from(NOW), Timestamp.from(NOW), userId)).isOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deactivate(long userId) {
|
|
||||||
assertThat(jdbc.update(
|
|
||||||
"""
|
|
||||||
update app_users
|
|
||||||
set account_status = 'DEACTIVATED', deactivated_at = ?, updated_at = ?
|
|
||||||
where id = ?
|
|
||||||
""",
|
|
||||||
Timestamp.from(NOW), Timestamp.from(NOW), userId)).isOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void completeInternship(long userId) {
|
|
||||||
assertThat(jdbc.update(
|
|
||||||
"""
|
|
||||||
update intern_profiles
|
|
||||||
set internship_status = 'COMPLETED', completed_at = ?, updated_at = ?
|
|
||||||
where user_id = ?
|
|
||||||
""",
|
|
||||||
Timestamp.from(NOW), Timestamp.from(NOW), userId)).isOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String nextEmail() {
|
|
||||||
return "picker-" + ++userSequence + "@example.com";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -97,6 +97,26 @@ class UiContractWebTest {
|
|||||||
assertTrue(themeBootstrap.contains("matchMedia('(prefers-color-scheme: dark)')"));
|
assertTrue(themeBootstrap.contains("matchMedia('(prefers-color-scheme: dark)')"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void generatedLucideSymbolsRetainCurrentColorStrokePresentation() throws Exception {
|
||||||
|
String icons = new ClassPathResource("static/assets/icons.svg")
|
||||||
|
.getContentAsString(StandardCharsets.UTF_8);
|
||||||
|
Matcher symbols = Pattern.compile("<symbol\\b([^>]*)>").matcher(icons);
|
||||||
|
int symbolCount = 0;
|
||||||
|
|
||||||
|
while (symbols.find()) {
|
||||||
|
String attributes = symbols.group(1);
|
||||||
|
assertTrue(attributes.contains("fill=\"none\""), () -> "Missing fill on " + attributes);
|
||||||
|
assertTrue(attributes.contains("stroke=\"currentColor\""), () -> "Missing stroke on " + attributes);
|
||||||
|
assertTrue(attributes.contains("stroke-width=\"2\""), () -> "Missing stroke width on " + attributes);
|
||||||
|
assertTrue(attributes.contains("stroke-linecap=\"round\""), () -> "Missing stroke linecap on " + attributes);
|
||||||
|
assertTrue(attributes.contains("stroke-linejoin=\"round\""), () -> "Missing stroke linejoin on " + attributes);
|
||||||
|
symbolCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(symbolCount > 0, "The generated sprite must contain symbols");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockUser(username = "admin@example.test", roles = "ADMIN")
|
@WithMockUser(username = "admin@example.test", roles = "ADMIN")
|
||||||
void collapsedSidebarExposesStateAndKeyboardVisibleControlNames() throws Exception {
|
void collapsedSidebarExposesStateAndKeyboardVisibleControlNames() throws Exception {
|
||||||
|
|||||||
Reference in New Issue
Block a user