Compare commits
4 commits
ab1cdb358f
...
350dfcfd7b
| Author | SHA1 | Date | |
|---|---|---|---|
| 350dfcfd7b | |||
| 93ece8128a | |||
| 75911dfffa | |||
| 4385f43b08 |
30 changed files with 338 additions and 67 deletions
|
|
@ -26,3 +26,8 @@ STRIPE_PRICE_ID=price_...
|
|||
# ---------- Swish (Phase 0) ----------
|
||||
SWISH_NUMBER=0701234567
|
||||
|
||||
# ---------- Production admin (prod profile only) ----------
|
||||
# Strong password; never use test1234. Dev seeds use test@bilhej.se instead.
|
||||
ADMIN_EMAIL=admin@bilhej.se
|
||||
ADMIN_PASSWORD=change_me_to_a_strong_password
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ jobs:
|
|||
STRIPE_WEBHOOK_SECRET=${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
||||
STRIPE_PRICE_ID=${{ secrets.STRIPE_PRICE_ID }}
|
||||
SWISH_NUMBER=${{ secrets.SWISH_NUMBER }}
|
||||
ADMIN_EMAIL=${{ secrets.ADMIN_EMAIL }}
|
||||
ADMIN_PASSWORD=${{ secrets.ADMIN_PASSWORD }}
|
||||
EOF
|
||||
|
||||
- name: Build and start production stack
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ live in `backend/src/main/resources/db/migration/`. Naming: `V<number>__descript
|
|||
|
||||
To reset: `docker compose down -v && docker compose up -d`.
|
||||
|
||||
Flyway schema migrations live in `db/migration/`; dev-only seeds (test users,
|
||||
sample orders) are in `db/dev-migration/` and run only without the `prod` profile.
|
||||
Production admin is created from `ADMIN_EMAIL` / `ADMIN_PASSWORD` on first boot.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
|
|
|||
98
README.md
98
README.md
|
|
@ -73,8 +73,8 @@ No CORS configuration needed in development — the browser never calls the back
|
|||
| Profile | Datasource | Use |
|
||||
|---------|-----------|-----|
|
||||
| default | H2 in-memory | Local IDE dev (`./gradlew :backend:bootRun`) |
|
||||
| `docker` | PostgreSQL via `docker-compose.yml` | Docker Compose dev |
|
||||
| `prod` | PostgreSQL (production config) | Deploy (`docker-compose.prod.yml`) |
|
||||
| `docker` | PostgreSQL + dev Flyway seeds | Docker Compose dev / CI |
|
||||
| `docker,prod` | PostgreSQL, schema only, admin bootstrap | Deploy (`docker-compose.prod.yml`) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -91,6 +91,89 @@ Copy `.env.example` to `.env` and fill in:
|
|||
| `STRIPE_SECRET_KEY` | Stripe secret key |
|
||||
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret |
|
||||
| `STRIPE_PRICE_ID` | Stripe price ID for single letter |
|
||||
| `SWISH_NUMBER` | Swish number for payment instructions |
|
||||
| `ADMIN_EMAIL` | Production admin login (e.g. `admin@bilhej.se`) |
|
||||
| `ADMIN_PASSWORD` | Strong production admin password (not `test1234`) |
|
||||
|
||||
**Dev-only accounts** (from `db/dev-migration`, not used in production):
|
||||
|
||||
| Email | Password | Role |
|
||||
|-------|----------|------|
|
||||
| `test@bilhej.se` | `test1234` | user (e2e / local) |
|
||||
| `admin@bilhalsning.se` | `test1234` | admin (local docker only) |
|
||||
|
||||
---
|
||||
|
||||
## Database access
|
||||
|
||||
PostgreSQL runs in Docker. Any GUI client works (IntelliJ IDEA, DBeaver, TablePlus,
|
||||
pgAdmin) — same idea as a normal remote database.
|
||||
|
||||
### Local dev (`docker compose up`)
|
||||
|
||||
| Setting | Value |
|
||||
|---------|--------|
|
||||
| Host | `localhost` |
|
||||
| Port | `5432` |
|
||||
| Database | from `.env` → `POSTGRES_DB` (default `bilhej`) |
|
||||
| User / password | `POSTGRES_USER` / `POSTGRES_PASSWORD` |
|
||||
|
||||
**IntelliJ IDEA:** Database tool window → `+` → Data Source → PostgreSQL → fill in above →
|
||||
Test Connection → OK.
|
||||
|
||||
**CLI:**
|
||||
|
||||
```bash
|
||||
docker exec -it bilhej-postgres psql -U bilhej -d bilhej
|
||||
```
|
||||
|
||||
### Production (server)
|
||||
|
||||
Postgres is bound to **localhost only** on the server (`127.0.0.1:5433`) so it is not
|
||||
exposed to the internet. Use an **SSH tunnel** from your laptop, then point IntelliJ (or
|
||||
DBeaver) at `localhost`.
|
||||
|
||||
1. On the server, recreate the stack once so the port mapping is active (after deploy).
|
||||
|
||||
2. From your laptop:
|
||||
|
||||
```bash
|
||||
ssh -N -L 5433:127.0.0.1:5433 you@srvr.nu
|
||||
```
|
||||
|
||||
3. In IntelliJ / DBeaver:
|
||||
|
||||
| Setting | Value |
|
||||
|---------|--------|
|
||||
| Host | `localhost` |
|
||||
| Port | `5433` |
|
||||
| Database | prod `POSTGRES_DB` |
|
||||
| User / password | prod secrets |
|
||||
|
||||
**CLI on the server** (no GUI):
|
||||
|
||||
```bash
|
||||
docker exec -it bilhej-postgres-prod psql -U bilhej -d bilhej
|
||||
```
|
||||
|
||||
### Manual prod cleanup (keep data, remove dev seeds)
|
||||
|
||||
```sql
|
||||
DELETE FROM orders
|
||||
WHERE user_id = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11';
|
||||
|
||||
DELETE FROM users
|
||||
WHERE email IN ('test@bilhalsning.se', 'test@bilhej.se', 'admin@bilhalsning.se');
|
||||
```
|
||||
|
||||
Then deploy with `ADMIN_EMAIL` / `ADMIN_PASSWORD` set — the app creates the production
|
||||
admin on startup. No need to insert a password hash by hand.
|
||||
|
||||
To generate a bcrypt hash manually (optional):
|
||||
|
||||
```bash
|
||||
./gradlew hashPassword -Ppassword='your-strong-password'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -187,6 +270,15 @@ Before the first deploy, complete these steps on the production server (`srvr.nu
|
|||
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret |
|
||||
| `STRIPE_PRICE_ID` | Stripe price ID for single letter |
|
||||
| `SWISH_NUMBER` | Swish phone number for payment instructions |
|
||||
| `ADMIN_EMAIL` | Production admin email (e.g. `admin@bilhej.se`) |
|
||||
| `ADMIN_PASSWORD` | Strong unique admin password (password manager) |
|
||||
|
||||
Production does **not** seed `test@bilhej.se` or demo orders. On first start, the
|
||||
backend creates one admin from `ADMIN_EMAIL` / `ADMIN_PASSWORD` if no admin exists.
|
||||
|
||||
If prod already has dev seed users, clean them with SQL (see [Database access](#database-access))
|
||||
instead of wiping the volume. Then redeploy with the new secrets so bootstrap can create
|
||||
`ADMIN_EMAIL`.
|
||||
|
||||
2. **Point DNS**
|
||||
|
||||
|
|
@ -225,7 +317,7 @@ Before the first deploy, complete these steps on the production server (`srvr.nu
|
|||
| Tag | Git tag `v0.1.0` is created and pushed |
|
||||
| Build | Production backend JAR and frontend bundle are built |
|
||||
| Images | Multi-stage Docker images are built locally on the server |
|
||||
| Start | `docker compose -f docker-compose.prod.yml up -d` |
|
||||
| Start | `docker compose -f docker-compose.prod.yml up -d` (`SPRING_PROFILES_ACTIVE=docker,prod`) |
|
||||
| Verify | Health checks confirm backend API and frontend are responding |
|
||||
|
||||
### Architecture on Server
|
||||
|
|
|
|||
|
|
@ -82,3 +82,11 @@ jacocoTestCoverageVerification {
|
|||
tasks.named('check').configure {
|
||||
dependsOn jacocoTestCoverageVerification
|
||||
}
|
||||
|
||||
tasks.register('hashPassword', JavaExec) {
|
||||
group = 'utility'
|
||||
description = 'Print BCrypt hash for a password (strength 12). Usage: -Ppassword=secret'
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'se.bilhalsning.tools.BcryptHashCli'
|
||||
args = project.findProperty('password') ? [project.property('password')] : []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package se.bilhalsning.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import se.bilhalsning.entity.User;
|
||||
import se.bilhalsning.repository.UserRepository;
|
||||
|
||||
@Component
|
||||
@Profile("prod")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AdminBootstrap implements ApplicationRunner {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Value("${app.admin.email:}")
|
||||
private String adminEmail;
|
||||
|
||||
@Value("${app.admin.password:}")
|
||||
private String adminPassword;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
if (userRepository.existsByRole("admin")) {
|
||||
log.info("Admin account already present, skipping bootstrap");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(adminEmail) || !StringUtils.hasText(adminPassword)) {
|
||||
throw new IllegalStateException(
|
||||
"Production requires ADMIN_EMAIL and ADMIN_PASSWORD when no admin user exists");
|
||||
}
|
||||
|
||||
User admin = new User();
|
||||
admin.setEmail(adminEmail.trim());
|
||||
admin.setPasswordHash(passwordEncoder.encode(adminPassword));
|
||||
admin.setRole("admin");
|
||||
userRepository.save(admin);
|
||||
|
||||
log.info("Created production admin account for {}", admin.getEmail());
|
||||
}
|
||||
}
|
||||
|
|
@ -12,4 +12,6 @@ public interface UserRepository extends JpaRepository<User, UUID> {
|
|||
Optional<User> findByEmail(String email);
|
||||
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
boolean existsByRole(String role);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
spring:
|
||||
flyway:
|
||||
locations: classpath:db/migration,classpath:db/dev-migration
|
||||
|
||||
datasource:
|
||||
url: jdbc:postgresql://postgres:5432/${POSTGRES_DB}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
|
|
|||
10
backend/src/main/resources/application-prod.yml
Normal file
10
backend/src/main/resources/application-prod.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
spring:
|
||||
flyway:
|
||||
locations: classpath:db/migration
|
||||
# Prod DBs created before seeds moved to db/dev-migration still list V2/V4/V6 in history.
|
||||
ignore-migration-patterns: '*:missing'
|
||||
|
||||
app:
|
||||
admin:
|
||||
email: ${ADMIN_EMAIL}
|
||||
password: ${ADMIN_PASSWORD}
|
||||
|
|
@ -22,7 +22,7 @@ spring:
|
|||
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
locations: classpath:db/migration,classpath:db/dev-migration
|
||||
|
||||
app:
|
||||
payment:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
-- Dev/CI only: test user for local docker and e2e (password: test1234)
|
||||
INSERT INTO users (id, email, password_hash, subscription)
|
||||
VALUES (
|
||||
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
|
||||
'test@bilhalsning.se',
|
||||
'test@bilhej.se',
|
||||
'$2b$12$18UFRDPgHWuw5FYeu6X1ReisFjjuxs5XxDafi6.wZbsywoU7vUaLG',
|
||||
'none'
|
||||
);
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
-- Dev/CI only: admin for local docker and e2e (password: test1234)
|
||||
INSERT INTO users (id, email, password_hash, subscription, role)
|
||||
VALUES (
|
||||
'b1eebc99-9c0b-4ef8-bb6d-6bb9bd380a12',
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
-- Seed orders for test user (test@bilhalsning.se, id: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11)
|
||||
-- Dev/CI only: sample orders for test@bilhej.se (id: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11)
|
||||
INSERT INTO orders (id, user_id, plate, letter_text, status, amount_paid, tracking_id, created_at, updated_at)
|
||||
VALUES
|
||||
('c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'ABC123', 'Hej! Jag ville bara säga att du har en väldigt fin bil. Hälsningar från en bilentusiast!', 'sent', 49.00, 'PN123456789', TIMESTAMP '2026-05-11 12:00:00', TIMESTAMP '2026-05-13 12:00:00'),
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package se.bilhalsning.config;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.boot.DefaultApplicationArguments;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import se.bilhalsning.entity.User;
|
||||
import se.bilhalsning.repository.UserRepository;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AdminBootstrapTest {
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Mock
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@InjectMocks
|
||||
private AdminBootstrap adminBootstrap;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(adminBootstrap, "adminEmail", "admin@bilhej.se");
|
||||
ReflectionTestUtils.setField(adminBootstrap, "adminPassword", "secure-production-password");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSkipBootstrapWhenAdminAlreadyExists() {
|
||||
when(userRepository.existsByRole("admin")).thenReturn(true);
|
||||
|
||||
adminBootstrap.run(new DefaultApplicationArguments(new String[] {}));
|
||||
|
||||
verify(userRepository, never()).save(any(User.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateAdminWhenMissing() {
|
||||
when(userRepository.existsByRole("admin")).thenReturn(false);
|
||||
when(passwordEncoder.encode("secure-production-password")).thenReturn("encoded-hash");
|
||||
|
||||
adminBootstrap.run(new DefaultApplicationArguments(new String[] {}));
|
||||
|
||||
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
|
||||
verify(userRepository).save(captor.capture());
|
||||
User saved = captor.getValue();
|
||||
org.junit.jupiter.api.Assertions.assertEquals("admin@bilhej.se", saved.getEmail());
|
||||
org.junit.jupiter.api.Assertions.assertEquals("encoded-hash", saved.getPasswordHash());
|
||||
org.junit.jupiter.api.Assertions.assertEquals("admin", saved.getRole());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenCredentialsMissingAndNoAdmin() {
|
||||
ReflectionTestUtils.setField(adminBootstrap, "adminPassword", "");
|
||||
when(userRepository.existsByRole("admin")).thenReturn(false);
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> adminBootstrap.run(new DefaultApplicationArguments(new String[] {})));
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ class AdminControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "test@bilhalsning.se", roles = "USER")
|
||||
@WithMockUser(username = "test@bilhej.se", roles = "USER")
|
||||
void shouldReturn403ForNonAdminUser() throws Exception {
|
||||
mockMvc.perform(get("/api/admin/orders"))
|
||||
.andExpect(status().isForbidden());
|
||||
|
|
@ -52,14 +52,14 @@ class AdminControllerTest {
|
|||
@Test
|
||||
@WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN")
|
||||
void shouldReturnAllOrdersForAdmin() throws Exception {
|
||||
Order order = createOrder(UUID.randomUUID(), "ABC123", "test@bilhalsning.se", OrderStatus.SENT);
|
||||
Order order = createOrder(UUID.randomUUID(), "ABC123", "test@bilhej.se", OrderStatus.SENT);
|
||||
when(orderService.getAllOrders()).thenReturn(List.of(order));
|
||||
|
||||
mockMvc.perform(get("/api/admin/orders"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isArray())
|
||||
.andExpect(jsonPath("$[0].id").value(order.getId().toString()))
|
||||
.andExpect(jsonPath("$[0].email").value("test@bilhalsning.se"))
|
||||
.andExpect(jsonPath("$[0].email").value("test@bilhej.se"))
|
||||
.andExpect(jsonPath("$[0].plate").value("ABC123"))
|
||||
.andExpect(jsonPath("$[0].letterText").value("Test letter"))
|
||||
.andExpect(jsonPath("$[0].status").value("sent"));
|
||||
|
|
@ -86,7 +86,7 @@ class AdminControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "test@bilhalsning.se", roles = "USER")
|
||||
@WithMockUser(username = "test@bilhej.se", roles = "USER")
|
||||
void shouldReturn403WhenPatchingStatusAsNonAdmin() throws Exception {
|
||||
mockMvc.perform(patch("/api/admin/orders/{id}/status",
|
||||
"c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
|
||||
|
|
@ -99,7 +99,7 @@ class AdminControllerTest {
|
|||
@WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN")
|
||||
void shouldUpdateOrderStatusSuccessfully() throws Exception {
|
||||
UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
||||
Order order = createOrder(orderId, "ABC123", "test@bilhalsning.se", OrderStatus.PAID);
|
||||
Order order = createOrder(orderId, "ABC123", "test@bilhej.se", OrderStatus.PAID);
|
||||
|
||||
when(orderService.updateOrderStatus(eq(orderId), eq("paid"))).thenReturn(order);
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ class AdminControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "test@bilhalsning.se", roles = "USER")
|
||||
@WithMockUser(username = "test@bilhej.se", roles = "USER")
|
||||
void shouldReturn403WhenPatchingTrackingAsNonAdmin() throws Exception {
|
||||
mockMvc.perform(patch("/api/admin/orders/{id}",
|
||||
"c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
|
||||
|
|
@ -167,7 +167,7 @@ class AdminControllerTest {
|
|||
@WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN")
|
||||
void shouldUpdateTrackingSuccessfully() throws Exception {
|
||||
UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
||||
Order order = createOrder(orderId, "ABC123", "test@bilhalsning.se", OrderStatus.SENT);
|
||||
Order order = createOrder(orderId, "ABC123", "test@bilhej.se", OrderStatus.SENT);
|
||||
order.setTrackingId("PN123456789");
|
||||
|
||||
when(orderService.updateTracking(eq(orderId), eq("PN123456789"))).thenReturn(order);
|
||||
|
|
@ -184,7 +184,7 @@ class AdminControllerTest {
|
|||
@WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN")
|
||||
void shouldClearTrackingWhenNull() throws Exception {
|
||||
UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
||||
Order order = createOrder(orderId, "ABC123", "test@bilhalsning.se", OrderStatus.SENT);
|
||||
Order order = createOrder(orderId, "ABC123", "test@bilhej.se", OrderStatus.SENT);
|
||||
order.setTrackingId(null);
|
||||
|
||||
when(orderService.updateTracking(eq(orderId), eq(null))).thenReturn(order);
|
||||
|
|
|
|||
|
|
@ -42,14 +42,14 @@ class OrderControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "test@bilhalsning.se")
|
||||
@WithMockUser(username = "test@bilhej.se")
|
||||
void shouldReturnOrdersForAuthenticatedUser() throws Exception {
|
||||
UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
||||
User user = new User();
|
||||
user.setId(userId);
|
||||
user.setEmail("test@bilhalsning.se");
|
||||
user.setEmail("test@bilhej.se");
|
||||
|
||||
when(userService.findByEmail("test@bilhalsning.se")).thenReturn(Optional.of(user));
|
||||
when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user));
|
||||
|
||||
when(orderService.getOrdersByUserId(userId)).thenReturn(List.of());
|
||||
|
||||
|
|
@ -60,14 +60,14 @@ class OrderControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "test@bilhalsning.se")
|
||||
@WithMockUser(username = "test@bilhej.se")
|
||||
void shouldReturnOrderWithAllFields() throws Exception {
|
||||
UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
||||
User user = new User();
|
||||
user.setId(userId);
|
||||
user.setEmail("test@bilhalsning.se");
|
||||
user.setEmail("test@bilhej.se");
|
||||
|
||||
when(userService.findByEmail("test@bilhalsning.se")).thenReturn(Optional.of(user));
|
||||
when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user));
|
||||
|
||||
se.bilhalsning.entity.Order order = new se.bilhalsning.entity.Order();
|
||||
order.setId(UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"));
|
||||
|
|
@ -106,14 +106,14 @@ class OrderControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "test@bilhalsning.se")
|
||||
@WithMockUser(username = "test@bilhej.se")
|
||||
void shouldCreateOrderSuccessfully() throws Exception {
|
||||
UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
||||
User user = new User();
|
||||
user.setId(userId);
|
||||
user.setEmail("test@bilhalsning.se");
|
||||
user.setEmail("test@bilhej.se");
|
||||
|
||||
when(userService.findByEmail("test@bilhalsning.se")).thenReturn(Optional.of(user));
|
||||
when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user));
|
||||
|
||||
se.bilhalsning.entity.Order savedOrder = new se.bilhalsning.entity.Order();
|
||||
savedOrder.setId(UUID.fromString("d1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"));
|
||||
|
|
@ -136,7 +136,7 @@ class OrderControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "test@bilhalsning.se")
|
||||
@WithMockUser(username = "test@bilhej.se")
|
||||
void shouldRejectInvalidPlateFormat() throws Exception {
|
||||
mockMvc.perform(post("/api/orders")
|
||||
.contentType("application/json")
|
||||
|
|
@ -146,7 +146,7 @@ class OrderControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "test@bilhalsning.se")
|
||||
@WithMockUser(username = "test@bilhej.se")
|
||||
void shouldRejectEmptyLetterText() throws Exception {
|
||||
mockMvc.perform(post("/api/orders")
|
||||
.contentType("application/json")
|
||||
|
|
@ -155,7 +155,7 @@ class OrderControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "test@bilhalsning.se")
|
||||
@WithMockUser(username = "test@bilhej.se")
|
||||
void shouldRejectLetterTextOver1000Chars() throws Exception {
|
||||
String longText = "a".repeat(1001);
|
||||
mockMvc.perform(post("/api/orders")
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class PaymentControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "test@bilhalsning.se")
|
||||
@WithMockUser(username = "test@bilhej.se")
|
||||
void shouldConfirmPaymentSuccessfully() throws Exception {
|
||||
UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
||||
Order order = new Order();
|
||||
|
|
@ -57,7 +57,7 @@ class PaymentControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "test@bilhalsning.se")
|
||||
@WithMockUser(username = "test@bilhej.se")
|
||||
void shouldReturn404WhenOrderNotFound() throws Exception {
|
||||
UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11");
|
||||
when(orderService.confirmPayment(eq(orderId)))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package se.bilhalsning.tools;
|
||||
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
|
||||
/** Run: ./gradlew hashPassword -Ppassword='your-password' */
|
||||
public final class BcryptHashCli {
|
||||
|
||||
private BcryptHashCli() {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 1) {
|
||||
System.err.println("Usage: gradlew hashPassword -Ppassword='...'");
|
||||
System.exit(1);
|
||||
}
|
||||
System.out.println(new BCryptPasswordEncoder(12).encode(args[0]));
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ services:
|
|||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- pgdata-prod:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "127.0.0.1:5433:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
|
|
@ -21,7 +23,7 @@ services:
|
|||
context: .
|
||||
container_name: bilhej-backend-prod
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: docker
|
||||
SPRING_PROFILES_ACTIVE: docker,prod
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
|
|
@ -30,6 +32,8 @@ services:
|
|||
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
|
||||
STRIPE_PRICE_ID: ${STRIPE_PRICE_ID}
|
||||
SWISH_NUMBER: ${SWISH_NUMBER}
|
||||
ADMIN_EMAIL: ${ADMIN_EMAIL}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ test.describe('Admin dashboard', () => {
|
|||
test('non-admin user is redirected away from admin', async ({ page }) => {
|
||||
await page.evaluate(() => localStorage.clear())
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ test.describe('Compose flow', () => {
|
|||
|
||||
test('shows error when no plate is provided', async ({ page }) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
@ -21,7 +21,7 @@ test.describe('Compose flow', () => {
|
|||
|
||||
test('displays plate and textarea', async ({ page }) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
@ -37,7 +37,7 @@ test.describe('Compose flow', () => {
|
|||
|
||||
test('submit button disabled when textarea is empty', async ({ page }) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
@ -50,7 +50,7 @@ test.describe('Compose flow', () => {
|
|||
|
||||
test('can create order and navigate to payment page', async ({ page }) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
@ -69,7 +69,7 @@ test.describe('Compose flow', () => {
|
|||
|
||||
test('preview shows letter content and GDPR footer', async ({ page }) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
@ -86,7 +86,7 @@ test.describe('Compose flow', () => {
|
|||
|
||||
test('Visa mallar button opens template picker', async ({ page }) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
@ -104,7 +104,7 @@ test.describe('Compose flow', () => {
|
|||
page,
|
||||
}) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ test.describe('Deferred payment and admin lookup', () => {
|
|||
|
||||
async function loginAsTestUser(page: import('@playwright/test').Page) {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ test.describe('Header auth state', () => {
|
|||
})
|
||||
|
||||
test('shows email and logout when authenticated', async ({ page }) => {
|
||||
const jwt = makeJwt({ sub: 'test@bilhalsning.se', role: 'user' })
|
||||
const jwt = makeJwt({ sub: 'test@bilhej.se', role: 'user' })
|
||||
await page.goto('/')
|
||||
await page.evaluate(
|
||||
(token) => localStorage.setItem('auth_token', token),
|
||||
|
|
@ -32,14 +32,14 @@ test.describe('Header auth state', () => {
|
|||
await page.goto('/')
|
||||
|
||||
const header = page.locator('header')
|
||||
await expect(header.getByText('test@bilhalsning.se')).toBeVisible()
|
||||
await expect(header.getByText('test@bilhej.se')).toBeVisible()
|
||||
await expect(
|
||||
header.getByRole('button', { name: 'Logga ut' }),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('shows orders link when authenticated', async ({ page }) => {
|
||||
const jwt = makeJwt({ sub: 'test@bilhalsning.se', role: 'user' })
|
||||
const jwt = makeJwt({ sub: 'test@bilhej.se', role: 'user' })
|
||||
await page.goto('/')
|
||||
await page.evaluate(
|
||||
(token) => localStorage.setItem('auth_token', token),
|
||||
|
|
@ -58,7 +58,7 @@ test.describe('Header auth state', () => {
|
|||
test('hides login and register links when authenticated', async ({
|
||||
page,
|
||||
}) => {
|
||||
const jwt = makeJwt({ sub: 'test@bilhalsning.se', role: 'user' })
|
||||
const jwt = makeJwt({ sub: 'test@bilhej.se', role: 'user' })
|
||||
await page.goto('/')
|
||||
await page.evaluate(
|
||||
(token) => localStorage.setItem('auth_token', token),
|
||||
|
|
@ -76,7 +76,7 @@ test.describe('Header auth state', () => {
|
|||
})
|
||||
|
||||
test('logout restores login and register links', async ({ page }) => {
|
||||
const jwt = makeJwt({ sub: 'test@bilhalsning.se', role: 'user' })
|
||||
const jwt = makeJwt({ sub: 'test@bilhej.se', role: 'user' })
|
||||
await page.goto('/')
|
||||
await page.evaluate(
|
||||
(token) => localStorage.setItem('auth_token', token),
|
||||
|
|
@ -96,11 +96,11 @@ test.describe('Header auth state', () => {
|
|||
await expect(
|
||||
header.getByRole('button', { name: 'Logga ut' }),
|
||||
).not.toBeVisible()
|
||||
await expect(header.getByText('test@bilhalsning.se')).not.toBeVisible()
|
||||
await expect(header.getByText('test@bilhej.se')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('logout redirects to home page', async ({ page }) => {
|
||||
const jwt = makeJwt({ sub: 'test@bilhalsning.se', role: 'user' })
|
||||
const jwt = makeJwt({ sub: 'test@bilhej.se', role: 'user' })
|
||||
await page.goto('/orders')
|
||||
await page.evaluate(
|
||||
(token) => localStorage.setItem('auth_token', token),
|
||||
|
|
@ -130,7 +130,7 @@ test.describe('Header auth state', () => {
|
|||
})
|
||||
|
||||
test('does not show admin link for regular user', async ({ page }) => {
|
||||
const jwt = makeJwt({ sub: 'test@bilhalsning.se', role: 'user' })
|
||||
const jwt = makeJwt({ sub: 'test@bilhej.se', role: 'user' })
|
||||
await page.goto('/')
|
||||
await page.evaluate(
|
||||
(token) => localStorage.setItem('auth_token', token),
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ test.describe('Login page', () => {
|
|||
|
||||
test('redirects to home after successful login', async ({ page }) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ test.describe('Order history', () => {
|
|||
page,
|
||||
}) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
@ -27,7 +27,7 @@ test.describe('Order history', () => {
|
|||
|
||||
test('displays page heading and seeded orders', async ({ page }) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
@ -42,7 +42,7 @@ test.describe('Order history', () => {
|
|||
|
||||
test('shows correct status badges', async ({ page }) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
@ -58,7 +58,7 @@ test.describe('Order history', () => {
|
|||
page,
|
||||
}) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
@ -76,7 +76,7 @@ test.describe('Order history', () => {
|
|||
|
||||
test('shows tracking links for orders with tracking ID', async ({ page }) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test'
|
|||
test.describe('Payment redirect', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ test.describe('Vehicle lookup', () => {
|
|||
|
||||
test('CTA navigates to compose when authenticated', async ({ page }) => {
|
||||
await page.goto('/logga-in')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhalsning.se')
|
||||
await page.getByLabel('E-postadress').fill('test@bilhej.se')
|
||||
await page.getByLabel('Lösenord').fill('test1234')
|
||||
await page.getByRole('button', { name: 'Logga in' }).click()
|
||||
await page.waitForURL('/')
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ function mountPage() {
|
|||
const mockOrders = [
|
||||
{
|
||||
id: 'c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
|
||||
email: 'test@bilhalsning.se',
|
||||
email: 'test@bilhej.se',
|
||||
plate: 'ABC123',
|
||||
letterText: 'Hej fin bil!',
|
||||
status: 'sent',
|
||||
|
|
@ -111,7 +111,7 @@ describe('AdminDashboard', () => {
|
|||
it('renders order data in rows', async () => {
|
||||
const { wrapper } = mountPage()
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
expect(wrapper.text()).toContain('test@bilhalsning.se')
|
||||
expect(wrapper.text()).toContain('test@bilhej.se')
|
||||
expect(wrapper.text()).toContain('ABC123')
|
||||
expect(wrapper.text()).toContain('user@example.com')
|
||||
expect(wrapper.text()).toContain('XYZ789')
|
||||
|
|
@ -350,7 +350,7 @@ describe('AdminDashboard', () => {
|
|||
await new Promise((r) => setTimeout(r, 50))
|
||||
|
||||
expect(wrapper.text()).toContain('pending@example.com')
|
||||
expect(wrapper.text()).not.toContain('test@bilhalsning.se')
|
||||
expect(wrapper.text()).not.toContain('test@bilhej.se')
|
||||
expect(wrapper.text()).not.toContain('user@example.com')
|
||||
})
|
||||
|
||||
|
|
@ -362,7 +362,7 @@ describe('AdminDashboard', () => {
|
|||
await new Promise((r) => setTimeout(r, 50))
|
||||
|
||||
expect(wrapper.text()).toContain('user@example.com')
|
||||
expect(wrapper.text()).not.toContain('test@bilhalsning.se')
|
||||
expect(wrapper.text()).not.toContain('test@bilhej.se')
|
||||
expect(wrapper.text()).not.toContain('pending@example.com')
|
||||
})
|
||||
|
||||
|
|
@ -373,7 +373,7 @@ describe('AdminDashboard', () => {
|
|||
await wrapper.find('#admin-order-search').setValue('abc123')
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
|
||||
expect(wrapper.text()).toContain('test@bilhalsning.se')
|
||||
expect(wrapper.text()).toContain('test@bilhej.se')
|
||||
expect(wrapper.text()).not.toContain('user@example.com')
|
||||
expect(wrapper.text()).not.toContain('pending@example.com')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ describe('AppHeader', () => {
|
|||
|
||||
describe('when authenticated', () => {
|
||||
function mountAuthenticated(role = 'user') {
|
||||
const jwt = makeJwt({ sub: 'test@bilhalsning.se', role })
|
||||
const jwt = makeJwt({ sub: 'test@bilhej.se', role })
|
||||
localStorage.setItem('auth_token', jwt)
|
||||
const pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
|
|
@ -132,7 +132,7 @@ describe('AppHeader', () => {
|
|||
|
||||
it('shows user email', () => {
|
||||
const { wrapper } = mountAuthenticated()
|
||||
expect(wrapper.text()).toContain('test@bilhalsning.se')
|
||||
expect(wrapper.text()).toContain('test@bilhej.se')
|
||||
})
|
||||
|
||||
it('shows logout button', () => {
|
||||
|
|
|
|||
|
|
@ -181,15 +181,15 @@ describe('authStore', () => {
|
|||
})
|
||||
|
||||
it('extracts email from JWT sub claim', async () => {
|
||||
const jwt = makeJwt({ sub: 'test@bilhalsning.se', role: 'user' })
|
||||
const jwt = makeJwt({ sub: 'test@bilhej.se', role: 'user' })
|
||||
vi.mocked(globalThis.fetch).mockResolvedValue(
|
||||
mockFetchResponse(200, { token: jwt }),
|
||||
)
|
||||
const store = useAuthStore()
|
||||
|
||||
await store.loginUser('test@bilhalsning.se', 'test1234')
|
||||
await store.loginUser('test@bilhej.se', 'test1234')
|
||||
|
||||
expect(store.email).toBe('test@bilhalsning.se')
|
||||
expect(store.email).toBe('test@bilhej.se')
|
||||
})
|
||||
|
||||
it('returns null email when not authenticated', () => {
|
||||
|
|
@ -198,14 +198,14 @@ describe('authStore', () => {
|
|||
})
|
||||
|
||||
it('clears email on logout', async () => {
|
||||
const jwt = makeJwt({ sub: 'test@bilhalsning.se', role: 'user' })
|
||||
const jwt = makeJwt({ sub: 'test@bilhej.se', role: 'user' })
|
||||
vi.mocked(globalThis.fetch).mockResolvedValue(
|
||||
mockFetchResponse(200, { token: jwt }),
|
||||
)
|
||||
const store = useAuthStore()
|
||||
|
||||
await store.loginUser('test@bilhalsning.se', 'test1234')
|
||||
expect(store.email).toBe('test@bilhalsning.se')
|
||||
await store.loginUser('test@bilhej.se', 'test1234')
|
||||
expect(store.email).toBe('test@bilhej.se')
|
||||
|
||||
store.logout()
|
||||
expect(store.email).toBeNull()
|
||||
|
|
|
|||
Loading…
Reference in a new issue