Public Access
Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c63b7e6aaa | ||
|
|
9df7389fab | ||
|
|
508e628d86 | ||
|
|
1d88c30d48 | ||
|
|
973b980e05 | ||
|
|
07ccb489ec | ||
|
|
05e87b4396 | ||
|
|
4ebca75be4 | ||
|
|
f3b6d67b5b | ||
|
|
da7c3ed069 | ||
|
|
3b6acb2b34 | ||
|
|
0f8c724556 | ||
|
|
0e830aca72 | ||
|
|
faed2d3411 | ||
|
|
3c3cda9409 | ||
|
|
89fdc7841b | ||
|
|
9114eb096b | ||
|
|
4859e74d45 | ||
|
|
34fa22c2b9 | ||
|
|
6f63179ec2 | ||
|
|
40135550f8 | ||
|
|
bcbf761aa5 | ||
|
|
2dc3cda51c | ||
|
|
2d4fb5bef7 | ||
|
|
c7ec8c2f7c | ||
|
|
2a5839b736 | ||
|
|
3fcba271a6 | ||
|
|
37b0fcdd21 | ||
|
|
f90b674ed2 | ||
|
|
20402eb7c4 | ||
|
|
14a4ccfb87 | ||
|
|
03d5a011ae | ||
|
|
6e194b9c9a | ||
|
|
954669d1a4 | ||
|
|
afbe950ba4 | ||
|
|
5dcdf7bd61 | ||
|
|
ad30de4190 | ||
|
|
c028e8d382 | ||
|
|
7ba38fbbce | ||
|
|
5dc3632687 | ||
|
|
afd71b82be | ||
|
|
3ee636253b | ||
|
|
a94b2bf9e7 | ||
|
|
161381ebe5 | ||
|
|
cc24c43ab8 | ||
|
|
1ce56590d1 | ||
|
|
132bb6bb64 | ||
|
|
eecda99d22 |
No files matched your search
@@ -1,2 +0,0 @@
|
||||
# .env file for local development
|
||||
DB_PASSWORD=my_super_secret_local_password
|
||||
@@ -31,3 +31,11 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Local secrets and generated files ###
|
||||
.env
|
||||
effective-pom
|
||||
|
||||
### Local dev config, never committed ###
|
||||
application-dev.yml
|
||||
application-dev.yaml
|
||||
-10713
File diff suppressed because it is too large.
Load diff
@@ -1,150 +1,110 @@
|
||||
# To Be Played (TBP) - Authentication Service
|
||||
# To Be Played – Auth Service
|
||||
|
||||
This is the Authentication and User Management Service for the "To Be Played" project. It acts as the central identity provider, handling user authentication, JWT generation, and profile management for the TBP frontend.
|
||||
Identity service of **To Be Played**, a Steam backlog tracker. It handles registration, login, user settings and the
|
||||
public profile, and it signs the JWTs that the Core service trusts.
|
||||
|
||||
## 🚀 Tech Stack
|
||||
To Be Played is split into three repositories:
|
||||
|
||||
* **Language:** Java 25
|
||||
* **Framework:** Spring Boot 4.1.x
|
||||
* **Security:** Spring Security & JSON Web Tokens (JWT)
|
||||
* **Database:** PostgreSQL 17
|
||||
* **Migrations:** Flyway
|
||||
* **Observability:** Spring Boot Actuator & Micrometer (Prometheus)
|
||||
* **Build Tool:** Maven Wrapper (`mvnw`)
|
||||
* **CI/CD:** Gitea Actions (Forgejo)
|
||||
| Repository | Role |
|
||||
|---|---|
|
||||
| **ToBePlayed-Auth** (this one) | Users, login, settings, signs JWTs |
|
||||
| ToBePlayed-Core | Steam library, backlog, wishlist, stats; verifies JWTs |
|
||||
| ToBePlayed-Frontend | React single-page app |
|
||||
|
||||
## 🏗️ Architecture (Clean Architecture)
|
||||
## Features
|
||||
|
||||
The project strictly follows a Domain-Driven / Clean Architecture (Ports & Adapters) approach to ensure high maintainability and testability. The codebase is divided into three main layers to prevent framework lock-in and keep business logic isolated.
|
||||
- Registration with an invitation code, login with email and password (BCrypt).
|
||||
- JWT signed with **EdDSA (Ed25519)**. Auth holds the private key, Core only the public key, so Core can verify
|
||||
tokens but never create them. Tokens carry a `kid` header for key rotation.
|
||||
- User settings: display name (unique, case-insensitive), Steam ID, language, dark mode.
|
||||
- Appearance settings synced across devices: accent color, corner style, wide layout, background and dashboard
|
||||
banner from the user's own game art. Only whitelisted presets and Steam app ids are stored, never raw CSS or URLs.
|
||||
- Opt-in public profile at `/u/<display name>`. Auth answers with a short-lived **profile ticket** (EdDSA JWT,
|
||||
10 minutes, `typ=public-profile`) that Core accepts for the public library data only. The ticket is not a login:
|
||||
both services reject every token with a `typ` claim on normal endpoints.
|
||||
|
||||
### Layer Breakdown
|
||||
## API
|
||||
|
||||
1. **API / Web Layer (`api`)**
|
||||
* Contains REST endpoints, DTOs (`Request`/`Response` records), and Global Exception Handlers.
|
||||
* Uses `ApiMapper` to map between DTOs and Domain Models.
|
||||
* **Rule:** This layer delegates to the Service layer and never accesses persistence directly.
|
||||
| Method | Path | Auth |
|
||||
|---|---|---|
|
||||
| `POST` | `/api/registration/new` | public |
|
||||
| `POST` | `/api/auth/login` | public |
|
||||
| `GET` | `/api/users/me` | JWT |
|
||||
| `PUT` | `/api/users/settings` | JWT |
|
||||
| `PUT` | `/api/users/settings/appearance` | JWT |
|
||||
| `GET` | `/api/public/profiles/{displayName}` | public |
|
||||
| `GET` | `/api/stats/public` | public |
|
||||
|
||||
2. **Business / Core Layer (`model`, `service`, `exception`)**
|
||||
* The heart of the application. Contains pure Java classes (Models) and Business Logic (Services).
|
||||
* Throws specific business exceptions (e.g., `InvalidCredentialsException`).
|
||||
* **Rule:** This layer has **no dependencies** on the API or Persistence layer. It defines interfaces that the persistence layer must implement.
|
||||
Endpoints use Spring's API versioning with a query parameter: `?version=1.0` (default `1.0`).
|
||||
|
||||
3. **Persistence Layer (`persistence`)**
|
||||
* Handles database operations. Contains JPA Entities (`UserEntity`), Spring Data Repositories, and the `EntityMapper`.
|
||||
* Implements the interfaces defined in the Domain layer.
|
||||
* **Rule:** Entities never leak into the API layer.
|
||||
## Tech stack
|
||||
|
||||
Architectural boundaries are enforced via ArchUnit (`src/test/java/auth_service/architecture/ArchitectureTest.java`), which runs as part of the standard `./mvnw test` quality gate. Abstract test bases live under `auth_service.shared.test` (`AbstractUnitTest`, `AbstractIntegrationTest`, `AbstractPersistenceTest`, `AbstractWebMvcTest`, `AbstractArchitectureTest`).
|
||||
- Java 25, Spring Boot 4.1 (Web MVC, Security, Data JPA, Validation, Cache with Caffeine), virtual threads
|
||||
- PostgreSQL 17, Flyway migrations
|
||||
- JJWT with Ed25519 keys
|
||||
- Spring Boot Actuator + Micrometer (Prometheus)
|
||||
- Tests: JUnit 5, Testcontainers (PostgreSQL), ArchUnit
|
||||
- CI: Forgejo Actions, multi-stage Docker image
|
||||
|
||||
## ⚙️ Local Development
|
||||
## Architecture
|
||||
|
||||
Running the application locally is fully automated via Spring Boot's Docker Compose Support.
|
||||
Package by feature (`modules/login`, `modules/registration`, `modules/user`, ...), and inside each feature three
|
||||
layers:
|
||||
|
||||
### Prerequisites
|
||||
| Layer | Package | Rule |
|
||||
|---|---|---|
|
||||
| API | `api` | REST controllers, request/response records, `ApiMapper`. Calls services, never repositories. |
|
||||
| Domain | `model`, `service`, `exception` | Plain Java models and business logic. No dependency on API or persistence. |
|
||||
| Persistence | `persistence` | JPA entities, Spring Data repositories, `EntityMapper`. Entities never reach the API layer. |
|
||||
|
||||
* Java 25 installed
|
||||
* Docker & Docker Compose running
|
||||
ArchUnit enforces these rules in `src/test/java/auth_service/architecture/ArchitectureTest.java`, so a layer
|
||||
violation fails the build. Test base classes (`AbstractUnitTest`, `AbstractWebMvcTest`, `AbstractPersistenceTest`,
|
||||
`AbstractIntegrationTest`) live in `auth_service.shared.test`; all integration tests share one PostgreSQL container.
|
||||
|
||||
### How to start
|
||||
## Run locally
|
||||
|
||||
1. Simply run the application from your IDE (e.g., IntelliJ) via the `AuthServiceApplication.java` main class.
|
||||
2. Spring Boot will automatically detect the `compose.yaml` and spin up a local PostgreSQL container (`tbp_auth_db_local`) on port `5432`.
|
||||
3. **Flyway** will automatically apply all database migrations located in `src/main/resources/db/migration`.
|
||||
4. The application will be available at `http://localhost:8080`.
|
||||
|
||||
To stop the environment and clean the database volume (if needed):
|
||||
Needs Java 25 and Docker.
|
||||
|
||||
```bash
|
||||
docker compose down -v
|
||||
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
|
||||
```
|
||||
|
||||
## 🔐 API Versioning
|
||||
Spring Boot's Docker Compose support starts PostgreSQL from `compose.yaml`, Flyway applies the migrations, and the
|
||||
service listens on `http://localhost:8080`. Health: `/actuator/health`, metrics: `/actuator/prometheus`.
|
||||
|
||||
All API endpoints utilize Query Parameter Versioning to keep URLs clean and manageable.
|
||||
Example: GET /api/users/me?v=1
|
||||
Configuration comes from environment variables:
|
||||
|
||||
## 🐳 CI/CD Pipeline
|
||||
| Variable | Purpose |
|
||||
|---|---|
|
||||
| `JWT_PRIVATE_KEY`, `JWT_PUBLIC_KEY` | Ed25519 keypair, Base64 of the DER bytes (PKCS#8 / X.509) |
|
||||
| `JWT_KID` | Key id written into the token header |
|
||||
| `AUTH_INVITATION_CODE` | Code required for registration |
|
||||
| `DB_PASSWORD` | Local PostgreSQL password |
|
||||
|
||||
The project uses Gitea Actions for Continuous Integration and Deployment.
|
||||
Generate a keypair:
|
||||
|
||||
Quality Gate: Runs ./mvnw clean verify to ensure tests and builds pass.
|
||||
```bash
|
||||
openssl genpkey -algorithm ed25519 -out private.pem
|
||||
openssl pkey -in private.pem -outform DER | base64 -w0 # JWT_PRIVATE_KEY
|
||||
openssl pkey -in private.pem -pubout -outform DER | base64 -w0 # JWT_PUBLIC_KEY
|
||||
```
|
||||
|
||||
Docker Build: Packages the application using a multi-stage Dockerfile.
|
||||
## Tests
|
||||
|
||||
Registry: Pushes the lightweight Alpine-based image to the private Gitea container registry (git.andreas-dahm.eu/lobo_corp/tbp-auth-service:latest).
|
||||
```bash
|
||||
./mvnw clean verify
|
||||
```
|
||||
|
||||
## 📈 Observability
|
||||
Runs unit, WebMvc, persistence and integration tests plus the ArchUnit rules. Needs Docker for Testcontainers.
|
||||
|
||||
Metrics and health checks are exposed out-of-the-box via Spring Boot Actuator:
|
||||
## Roadmap
|
||||
|
||||
Health: http://localhost:8080/actuator/health
|
||||
- Rate limit `/api/auth/login` and `/api/registration/**`.
|
||||
- JWKS endpoint (`/.well-known/jwks.json`), so Core fetches the public key and keys rotate without a redeploy.
|
||||
- Short-lived access token plus rotating refresh token in an httpOnly cookie, with CSRF protection. Today the token
|
||||
lives 24 h in `localStorage` and cannot be revoked.
|
||||
- Account deletion and data export.
|
||||
|
||||
Prometheus Metrics: http://localhost:8080/actuator/prometheus
|
||||
## License
|
||||
|
||||
## 🗺️ Roadmap / Open Topics
|
||||
|
||||
Tracking known work that is not yet implemented. Each item is independently actionable.
|
||||
|
||||
### Security
|
||||
|
||||
- ~~**Switch JWT signing from HS256 → EdDSA (Ed25519).**~~ Done. Auth signs with an Ed25519 private key (`JWT_PRIVATE_KEY`), Core verifies with the matching public key (`JWT_PUBLIC_KEY`). `kid` header (`JWT_KID`) is emitted to support future rotation. See `JwtService`, `JwtConfig`, `application.yml`, `JwtServiceTest`, and `ToBePlayed-Deployment/README.md` for the rollout / rotation procedure.
|
||||
- **Refresh-token rotation.** Current access tokens last 24h with no revocation path. Split into a short-lived (5–15 min) access token and a long-lived (7–30 day) refresh token, rotated on every use, persisted server-side so logout / password change / "log out everywhere" actually revoke. Add a `token_version` column on `users` (or a `jti` blacklist table) checked on each refresh.
|
||||
- **Move JWT delivery to an httpOnly `Secure` cookie.** Tokens are currently returned in the response body and stored by the frontend in `localStorage`, exposing them to any XSS payload. Set the cookie server-side, scope it to `.tbp.andreas-dahm.eu`, and add CSRF protection (Spring Security's `CookieCsrfTokenRepository.withHttpOnlyFalse()`).
|
||||
- **Tighten CORS for cookie auth.** Restrict allowed origins to `https://tbp.andreas-dahm.eu` (plus the local dev origin) and set `allowCredentials=true`. Required before switching to cookie-based JWT.
|
||||
- **Rate-limit `/api/auth/login` and `/api/registration/**`.** Currently no brake on credential stuffing or registration spam. Bucket4j or Spring Security's built-in `RateLimiter` are both options; key by IP + email.
|
||||
|
||||
### Observability and API
|
||||
|
||||
- **Publish a JWKS endpoint** (`GET /.well-known/jwks.json`) once on EdDSA. Lets Core (and any future verifier) fetch the public key automatically and supports key rotation without redeploying every consumer.
|
||||
- **API versioning review.** Query-parameter versioning (`?v=1`) is unusual; path- or `Accept`-header-based versioning is more conventional and plays better with HTTP caches. Worth a deliberate decision before more endpoints accumulate.
|
||||
|
||||
### Testing
|
||||
|
||||
- The abstract test bases under `auth_service.shared.test` already cover unit / integration / persistence / WebMvc slices. Continue adding controller-level `AbstractWebMvcTest` coverage for any new endpoint and persistence-layer tests for any new repository.
|
||||
|
||||
## 🎯 Product Roadmap (Auth-owned slices)
|
||||
|
||||
The full cross-service product roadmap lives in `ToBePlayed-Deployment/README.md`. This section lists only the slices that land in Auth.
|
||||
|
||||
### 1. ~~Username + public-profile opt-in~~ Done
|
||||
|
||||
- Display name (3–24 chars, unique case-insensitive) is the public handle; `/u/<display name>` in the frontend.
|
||||
- `user_settings.public_profile` (default `false`), set via `PUT /api/users/settings` (`publicProfile`). A public
|
||||
profile needs a display name (400 otherwise).
|
||||
- `GET /api/public/profiles/{displayName}` → `{displayName, memberSince, ticket}` for enabled users with a public
|
||||
profile; everything else is the same 404 `PROFILE_NOT_FOUND`.
|
||||
- The `ticket` is an EdDSA JWT (`typ=public-profile`, `profileUserId`, **valid 10 minutes**) for Core's
|
||||
`GET /api/public/profiles/library` (header `X-Profile-Ticket`). It is no login: `JwtService.isTokenValid` rejects
|
||||
every token with a `typ` claim, in Auth and in Core. Accepted trade-off: after a user makes the profile private, a
|
||||
ticket issued before still reads the public data for up to 10 minutes; Core stores no copy of the flag.
|
||||
|
||||
### 2. Friends / social graph
|
||||
|
||||
Once usernames exist:
|
||||
|
||||
- Schema: `user_friendship(user_id, friend_id, status, requested_at, accepted_at)` with `status` in `PENDING / ACCEPTED / BLOCKED`. Symmetric pair or single-row with a canonical ordering — pick one and stick with it.
|
||||
- Endpoints: `POST /api/users/me/friends?username=...` (request), `POST /api/users/me/friends/{id}/accept`, `DELETE /api/users/me/friends/{id}`, `GET /api/users/me/friends`.
|
||||
- Optional: discovery via the Steam friends API (Auth holds the `steam_id`; the friends list call itself can live in Core's `steam_adapter`, with Auth resolving Steam IDs back to TBP users).
|
||||
|
||||
### 3. Email-driven retention
|
||||
|
||||
The weekly digest, sale alerts, and friend-request notifications all need email infrastructure:
|
||||
|
||||
- Spring's `JavaMailSender` + templating (Thymeleaf or plain interpolation). Configure SMTP via env vars (see Deployment roadmap).
|
||||
- Per-user notification preferences: `notify_digest_weekly`, `notify_sale_alerts`, `notify_friend_requests` — all booleans on `users` with sensible defaults.
|
||||
- Endpoint to render `/api/users/me/notification-preferences` and update via `PUT`.
|
||||
- Triggered by either Core scheduled jobs (digest, sale alerts) calling an internal Auth endpoint, or by a shared message queue if/when one is introduced.
|
||||
|
||||
### 4. Invitation system enhancements
|
||||
|
||||
Today registration validates an `INVALID_INVITATION_CODE` — so invites already exist. Worth surfacing:
|
||||
|
||||
- Endpoint: `POST /api/users/me/invitations` to generate a single-use code (rate-limited, expires after N days). Used for friend invites and onboarding referrals.
|
||||
- Endpoint: `GET /api/users/me/invitations` to list outstanding codes.
|
||||
- Optional: tie invitation acceptance to an auto-friend-request so referred users are already connected on first login.
|
||||
|
||||
### 5. Account deletion / GDPR
|
||||
|
||||
- `DELETE /api/users/me` that soft-deletes (sets `deleted_at`, anonymises PII, keeps the row for foreign-key integrity), or hard-deletes with cascade depending on policy.
|
||||
- Data export endpoint: `GET /api/users/me/export` returning a downloadable JSON bundle of everything Auth knows about the user. Core should expose an equivalent and the Frontend can stitch them.
|
||||
- Document the data-retention policy here once decided.
|
||||
All rights reserved. The source is public for reading only.
|
||||
@@ -1,27 +0,0 @@
|
||||
# ─────────────────────────────────────────────
|
||||
# DEVELOPMENT CONFIGURATION
|
||||
# ─────────────────────────────────────────────
|
||||
# Overrides properties from application.yml when the 'dev' profile is active
|
||||
|
||||
app:
|
||||
cors:
|
||||
# Local development: allow all origins to prevent CORS issues
|
||||
allowed-origins: "*"
|
||||
|
||||
# Optional: Make SQL logs visible for debugging
|
||||
spring:
|
||||
jpa:
|
||||
show-sql: true
|
||||
|
||||
security:
|
||||
jwt:
|
||||
# Dev-only Ed25519 keypair. DO NOT use in production — regenerate per-env with:
|
||||
# openssl genpkey -algorithm Ed25519 -outform DER | base64 -w0
|
||||
# openssl pkey -inform DER -in <(base64 -d <<< "$PRIV") -pubout -outform DER | base64 -w0
|
||||
private-key: "MC4CAQAwBQYDK2VwBCIEIG9nxZFw9Fj7lGUqA7oi94oo/xollxwlZIZENlpfp+tc"
|
||||
public-key: "MCowBQYDK2VwAyEAPSmUISg0IUO1LrRPhF8RZAT/Yjh9HEVf5hzR84AL1Zw="
|
||||
kid: "tbp-auth-dev"
|
||||
|
||||
auth:
|
||||
registration:
|
||||
invitation-code: "Us*kksUuRpTE2CQRsv+^k0n)ksR}X}?#Fo2D4d8ZKG"
|
||||
@@ -60,8 +60,8 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
|
||||
var request = RegistrationRequest.builder()
|
||||
.email("test@example.com")
|
||||
.password("SuperSecret123!")
|
||||
.inviteCode("Us*kksUuRpTE2CQRsv+^k0n)ksR}X}?#Fo2D4d8ZKG")
|
||||
.steamId("76561198005679788")
|
||||
.inviteCode("test-invite-code")
|
||||
.steamId("76561198000000001")
|
||||
.build();
|
||||
|
||||
long expectedUserId = 42L;
|
||||
@@ -83,8 +83,8 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
|
||||
var request = RegistrationRequest.builder()
|
||||
.email("test@example.com")
|
||||
.password("SuperSecret123!")
|
||||
.inviteCode("Us*kksUuRpTE2CQRsv+^k0n)ksR}X}?#Fo2D4d8ZKG")
|
||||
.steamId("76561198005679788")
|
||||
.inviteCode("test-invite-code")
|
||||
.steamId("76561198000000001")
|
||||
.build();
|
||||
|
||||
when(registrationService.register(any(Registration.class)))
|
||||
@@ -106,7 +106,7 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
|
||||
.email("test@example.com")
|
||||
.password("SuperSecret123!")
|
||||
.inviteCode("code")
|
||||
.steamId("76561198005679788")
|
||||
.steamId("76561198000000001")
|
||||
.build();
|
||||
when(registrationService.register(any(Registration.class))).thenReturn(42L);
|
||||
|
||||
@@ -119,7 +119,7 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
|
||||
// Assert
|
||||
var captor = ArgumentCaptor.forClass(Registration.class);
|
||||
verify(registrationService).register(captor.capture());
|
||||
assertEquals(76561198005679788L, captor.getValue().getSteamId());
|
||||
assertEquals(76561198000000001L, captor.getValue().getSteamId());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+2
-2
@@ -42,8 +42,8 @@ class RegistrationServiceTest extends AbstractUnitTest {
|
||||
validRegistration = Registration.builder()
|
||||
.email("test@example.com")
|
||||
.rawPassword("SuperSecret123!")
|
||||
.inviteCode("Us*kksUuRpTE2CQRsv+^k0n)ksR}X}?#Fo2D4d8ZKG")
|
||||
.steamId(76561198005679788L)
|
||||
.inviteCode("test-invite-code")
|
||||
.steamId(76561198000000001L)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ class UserControllerTest extends AbstractWebMvcTest {
|
||||
// Arrange
|
||||
var email = "test@example.com";
|
||||
var settings = UserSettings.builder().language("en").theme("dark").build();
|
||||
var user = User.builder().email(email).steamId(76561198005679788L).settings(settings).build();
|
||||
var user = User.builder().email(email).steamId(76561198000000001L).settings(settings).build();
|
||||
var principal = new UserPrincipal("1", email, null);
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
|
||||
@@ -95,7 +95,7 @@ class UserControllerTest extends AbstractWebMvcTest {
|
||||
mockMvc.perform(get("/api/users/me")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.steamId").value("76561198005679788"));
|
||||
.andExpect(jsonPath("$.steamId").value("76561198000000001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -141,13 +141,13 @@ class UserControllerTest extends AbstractWebMvcTest {
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
|
||||
);
|
||||
when(userService.updateUserSettings(eq(1L), eq(email), eq(76561198005679788L), any(), any())).thenReturn("new-token");
|
||||
when(userService.updateUserSettings(eq(1L), eq(email), eq(76561198000000001L), any(), any())).thenReturn("new-token");
|
||||
|
||||
// Act & Assert
|
||||
mockMvc.perform(put("/api/users/settings")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"useDarkTheme":true,"language":"en","steamId":"76561198005679788"}"""))
|
||||
{"useDarkTheme":true,"language":"en","steamId":"76561198000000001"}"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.token").value("new-token"));
|
||||
}
|
||||
@@ -197,14 +197,14 @@ class UserControllerTest extends AbstractWebMvcTest {
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
|
||||
);
|
||||
when(userService.updateUserSettings(eq(1L), eq(email), eq(76561198005679788L), any(), any()))
|
||||
when(userService.updateUserSettings(eq(1L), eq(email), eq(76561198000000001L), any(), any()))
|
||||
.thenThrow(new DataIntegrityViolationException("duplicate key value violates unique constraint \"uc_steam_id\""));
|
||||
|
||||
// Act & Assert
|
||||
mockMvc.perform(put("/api/users/settings")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"useDarkTheme":true,"language":"en","steamId":"76561198005679788"}"""))
|
||||
{"useDarkTheme":true,"language":"en","steamId":"76561198000000001"}"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value("STEAM_ID_ALREADY_IN_USE"));
|
||||
}
|
||||
@@ -235,14 +235,14 @@ class UserControllerTest extends AbstractWebMvcTest {
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
|
||||
);
|
||||
when(userService.updateUserSettings(eq(1L), eq(email), eq(76561198005679788L), any(), any()))
|
||||
when(userService.updateUserSettings(eq(1L), eq(email), eq(76561198000000001L), any(), any()))
|
||||
.thenThrow(new SteamIdAlreadyInUseException("taken"));
|
||||
|
||||
// Act & Assert
|
||||
mockMvc.perform(put("/api/users/settings")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"useDarkTheme":true,"language":"en","steamId":"76561198005679788"}"""))
|
||||
{"useDarkTheme":true,"language":"en","steamId":"76561198000000001"}"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value("STEAM_ID_ALREADY_IN_USE"));
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
|
||||
.email("owner@example.com")
|
||||
.password("password")
|
||||
.enabled(true)
|
||||
.steamId(76561198005679788L)
|
||||
.steamId(76561198000000001L)
|
||||
.registrationDate(LocalDateTime.now())
|
||||
.build());
|
||||
var other = entityManager.persist(UserEntity.builder()
|
||||
@@ -135,8 +135,8 @@ class UserRepositoryTest extends AbstractPersistenceTest {
|
||||
entityManager.flush();
|
||||
|
||||
// Act & Assert
|
||||
assertFalse(userRepository.isSteamIdUsedByOtherUser(owner.getId(), 76561198005679788L));
|
||||
assertTrue(userRepository.isSteamIdUsedByOtherUser(other.getId(), 76561198005679788L));
|
||||
assertFalse(userRepository.isSteamIdUsedByOtherUser(owner.getId(), 76561198000000001L));
|
||||
assertTrue(userRepository.isSteamIdUsedByOtherUser(other.getId(), 76561198000000001L));
|
||||
assertFalse(userRepository.isSteamIdUsedByOtherUser(other.getId(), 76561198000000000L));
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ class UserServiceTest extends AbstractUnitTest {
|
||||
// Arrange
|
||||
long userId = 1L;
|
||||
var email = "test@example.com";
|
||||
Long steamId = 76561198005679788L;
|
||||
Long steamId = 76561198000000001L;
|
||||
var userSettings = UserSettings.builder().language("en").build();
|
||||
when(userRepository.isSteamIdUsedByOtherUser(userId, steamId)).thenReturn(false);
|
||||
when(tokenGenerator.generateToken("1", email, steamId)).thenReturn("new-token");
|
||||
@@ -98,7 +98,7 @@ class UserServiceTest extends AbstractUnitTest {
|
||||
void updateUserSettings_SteamIdOfOtherUser() {
|
||||
// Arrange
|
||||
long userId = 1L;
|
||||
Long steamId = 76561198005679788L;
|
||||
Long steamId = 76561198000000001L;
|
||||
var userSettings = UserSettings.builder().language("en").build();
|
||||
when(userRepository.isSteamIdUsedByOtherUser(userId, steamId)).thenReturn(true);
|
||||
|
||||
|
||||
@@ -4,8 +4,15 @@ import org.junit.jupiter.api.Tag;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Base class for integration tests.
|
||||
* Full Spring context with Testcontainers.
|
||||
@@ -23,5 +30,23 @@ public abstract class AbstractIntegrationTest {
|
||||
static {
|
||||
postgres.start();
|
||||
}
|
||||
|
||||
// No key is committed: a fresh one per JVM.
|
||||
private static final KeyPair KEYS = generateKeys();
|
||||
|
||||
@DynamicPropertySource
|
||||
static void secrets(DynamicPropertyRegistry registry) {
|
||||
registry.add("security.jwt.private-key", () -> Base64.getEncoder().encodeToString(KEYS.getPrivate().getEncoded()));
|
||||
registry.add("security.jwt.public-key", () -> Base64.getEncoder().encodeToString(KEYS.getPublic().getEncoded()));
|
||||
registry.add("auth.registration.invitation-code", () -> "test-invite-code");
|
||||
}
|
||||
|
||||
private static KeyPair generateKeys() {
|
||||
try {
|
||||
return KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user