mirror of
https://github.com/suitenumerique/messages.git
synced 2026-04-25 17:15:21 +02:00
🔧(e2e) setup e2e environment
Create a new project e2e based on playwright and setup a full dockerized e2e environment
This commit is contained in:
@@ -34,3 +34,5 @@ db.sqlite3
|
||||
|
||||
# Frontend
|
||||
node_modules
|
||||
out
|
||||
.next
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -41,6 +41,7 @@ env.bak/
|
||||
venv.bak/
|
||||
env.d/development/*
|
||||
!env.d/development/*.defaults
|
||||
!env.d/development/*.e2e
|
||||
env.d/terraform
|
||||
|
||||
# npm
|
||||
|
||||
89
Makefile
89
Makefile
@@ -26,6 +26,7 @@
|
||||
BOLD := \033[1m
|
||||
RESET := \033[0m
|
||||
GREEN := \033[1;32m
|
||||
BLUE := \033[1;34m
|
||||
|
||||
# -- Docker
|
||||
# Get the current user ID to use for docker run and docker exec commands
|
||||
@@ -33,6 +34,7 @@ DOCKER_UID = $(shell id -u)
|
||||
DOCKER_GID = $(shell id -g)
|
||||
DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID)
|
||||
COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose
|
||||
COMPOSE_E2E = DOCKER_USER=$(DOCKER_USER) docker compose -f src/e2e/compose.yaml
|
||||
COMPOSE_EXEC = $(COMPOSE) exec
|
||||
COMPOSE_EXEC_APP = $(COMPOSE_EXEC) backend-dev
|
||||
COMPOSE_RUN = $(COMPOSE) run --rm --build
|
||||
@@ -264,6 +266,93 @@ socks-proxy-test: ## run the socks-proxy tests
|
||||
@$(COMPOSE) run --build --rm socks-proxy-test
|
||||
.PHONY: socks-proxy-test
|
||||
|
||||
# -- E2E Tests
|
||||
|
||||
e2e-test: ## Setup, run and teardown e2e tests in headless mode
|
||||
@$(MAKE) e2e-setup
|
||||
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
$(MAKE) e2e-run-test args="$${args:-${1}}" || echo "$(BOLD)Tests failed$(RESET)"
|
||||
@$(MAKE) e2e-teardown
|
||||
.PHONY: e2e-test
|
||||
|
||||
e2e-test-ui: ## Setup, run and teardown e2e tests in UI mode
|
||||
@$(MAKE) e2e-setup
|
||||
@$(MAKE) e2e-run-test-ui
|
||||
@$(MAKE) e2e-teardown
|
||||
.PHONY: e2e-test-ui
|
||||
|
||||
e2e-test-dev: ## Setup, run and teardown e2e tests in UI mode with dev frontend
|
||||
@$(MAKE) e2e-setup
|
||||
@$(MAKE) e2e-run-test-dev
|
||||
@$(MAKE) e2e-teardown
|
||||
.PHONY: e2e-test-dev
|
||||
|
||||
e2e-test-ci: ## Setup and run e2e tests in CI mode
|
||||
@$(MAKE) e2e-setup
|
||||
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
$(MAKE) e2e-run-test -- $${args:-${1}}
|
||||
.PHONY: e2e-test-ci
|
||||
|
||||
|
||||
e2e-build: ## Build the e2e services
|
||||
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
$(COMPOSE_E2E) build --no-cache $${args:-${1}}
|
||||
.PHONY: e2e-build
|
||||
|
||||
e2e-log:
|
||||
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
$(MAKE) e2e-logs -- $${args:-${1}}
|
||||
.PHONY: e2e-log
|
||||
|
||||
e2e-logs: ## Show logs from e2e services
|
||||
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
$(COMPOSE_E2E) --profile dev logs $${args:-${1}}
|
||||
.PHONY: e2e-logs
|
||||
|
||||
e2e-run-test: ## Run e2e tests in headless mode
|
||||
@echo "$(BLUE)\n\n| 🎭 Running E2E tests... \n$(RESET)"
|
||||
$(COMPOSE_E2E) run --rm --service-ports runner npm run test -- $(args)
|
||||
@echo "$(GREEN)> 🎭 E2E tests completed!$(RESET)\n"
|
||||
.PHONY: e2e-run-test
|
||||
|
||||
e2e-run-test-ui: ## Run e2e tests in UI mode
|
||||
@echo "$(BLUE)\n\n| 🎭 Running E2E tests in UI mode... \n$(RESET)"
|
||||
# Note: || true allows graceful exit when user closes the UI
|
||||
@$(COMPOSE_E2E) run --rm --service-ports runner npm run test:ui || true
|
||||
@echo "$(GREEN)> 🎭 You killed the UI!$(RESET)\n"
|
||||
.PHONY: e2e-run-test-ui
|
||||
|
||||
e2e-run-test-dev: ## Run e2e tests in UI mode with dev frontend
|
||||
@echo "$(BLUE)\n\n| 🎭 Running E2E tests in dev mode... \n$(RESET)"
|
||||
# Note: || true allows graceful exit when user closes the UI
|
||||
E2E_PROFILE=dev $(COMPOSE_E2E) --profile dev run --rm --service-ports runner npm run test:ui || true
|
||||
@echo "$(GREEN)> 🎭 You killed the UI!$(RESET)\n"
|
||||
.PHONY: e2e-run-test-dev
|
||||
|
||||
e2e-down: ## Stop and remove all e2e services
|
||||
@echo "$(BOLD)Stopping E2E services...$(RESET)"
|
||||
@$(COMPOSE_E2E) --profile dev down -v
|
||||
@echo "$(GREEN)✓ E2E services stopped$(RESET)"
|
||||
.PHONY: e2e-down
|
||||
|
||||
e2e-demo: ## Populate the e2e database with demo data
|
||||
@echo "$(BLUE)\n\n| 📝 Bootstrapping E2E demo data... \n$(RESET)"
|
||||
@$(COMPOSE_E2E) run --rm backend python manage.py e2e_demo
|
||||
.PHONY: e2e-demo
|
||||
|
||||
e2e-setup: ## Setup e2e services
|
||||
@echo "$(BLUE)\n\n| 🔧 Setting up E2E services... \n$(RESET)"
|
||||
@$(COMPOSE_E2E) run --rm objectstorage-createbucket
|
||||
@$(COMPOSE_E2E) run --rm backend python manage.py migrate --noinput
|
||||
@$(COMPOSE_E2E) run --rm backend python manage.py search_index_create || true
|
||||
@$(MAKE) e2e-demo
|
||||
.PHONY: e2e-setup
|
||||
|
||||
e2e-teardown: ## Teardown e2e services
|
||||
@echo "$(BLUE)\n\n| 🧹 Cleaning up E2E services... \n$(RESET)"
|
||||
@$(COMPOSE_E2E) --profile dev down -v
|
||||
.PHONY: e2e-teardown
|
||||
|
||||
# -- Backend
|
||||
|
||||
migrations: ## run django makemigrations for the messages project.
|
||||
|
||||
86
compose.yaml
86
compose.yaml
@@ -75,28 +75,22 @@ services:
|
||||
depends_on:
|
||||
objectstorage:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- MC_HOST=http://objectstorage:9000
|
||||
entrypoint: >
|
||||
sh -c "
|
||||
/usr/bin/mc alias set st-messages http://objectstorage:9000 st-messages password && \
|
||||
/usr/bin/mc alias set st-messages $${MC_HOST} st-messages password && \
|
||||
/usr/bin/mc mb st-messages/msg-imports --ignore-existing && \
|
||||
/usr/bin/mc ilm rule rm --all --force st-messages/msg-imports || true && \
|
||||
/usr/bin/mc ilm rule add --expire-days 1 st-messages/msg-imports"
|
||||
|
||||
backend-dev:
|
||||
backend-base:
|
||||
build:
|
||||
context: src/backend
|
||||
target: runtime-dev
|
||||
args:
|
||||
DOCKER_USER: ${DOCKER_USER:-1000}
|
||||
user: ${DOCKER_USER:-1000}
|
||||
environment:
|
||||
- PYLINTHOME=/app/.pylint.d
|
||||
- DJANGO_CONFIGURATION=Development
|
||||
env_file:
|
||||
- env.d/development/backend.defaults
|
||||
- env.d/development/backend.local
|
||||
ports:
|
||||
- "8901:8000"
|
||||
volumes:
|
||||
- ./src/backend:/app
|
||||
- ./data/static:/data/static
|
||||
@@ -105,6 +99,17 @@ services:
|
||||
interval: 3s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
backend-dev:
|
||||
extends: backend-base
|
||||
environment:
|
||||
- PYLINTHOME=/app/.pylint.d
|
||||
- DJANGO_CONFIGURATION=Development
|
||||
env_file:
|
||||
- env.d/development/backend.defaults
|
||||
- env.d/development/backend.local
|
||||
ports:
|
||||
- "8901:8000"
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
@@ -121,14 +126,9 @@ services:
|
||||
condition: service_started
|
||||
|
||||
backend-db:
|
||||
extends: backend-base
|
||||
profiles:
|
||||
- tools
|
||||
build:
|
||||
context: src/backend
|
||||
target: runtime-dev
|
||||
args:
|
||||
DOCKER_USER: ${DOCKER_USER:-1000}
|
||||
user: ${DOCKER_USER:-1000}
|
||||
environment:
|
||||
- DJANGO_CONFIGURATION=DevelopmentMinimal
|
||||
env_file:
|
||||
@@ -136,18 +136,6 @@ services:
|
||||
- env.d/development/backend.local
|
||||
ports:
|
||||
- "8901:8000"
|
||||
volumes:
|
||||
- ./src/backend:/app
|
||||
- ./data/static:/data/static
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request as u; u.urlopen('http://localhost:8000/healthz/', timeout=1)"]
|
||||
interval: 3s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
|
||||
backend-poetry:
|
||||
profiles:
|
||||
@@ -199,23 +187,17 @@ services:
|
||||
- "8903:8803"
|
||||
command: celery -A messages.celery_app flower --port=8803
|
||||
|
||||
# nginx:
|
||||
# image: nginx:1.25
|
||||
# ports:
|
||||
# - "8083:8083"
|
||||
# volumes:
|
||||
# - ./docker/files/development/etc/nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
# depends_on:
|
||||
# - keycloak
|
||||
# - backend-dev
|
||||
# - mta-in
|
||||
# - mta-out
|
||||
|
||||
frontend-dev:
|
||||
frontend-base:
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
build:
|
||||
context: ./src/frontend
|
||||
dockerfile: Dockerfile
|
||||
target: frontend-deps
|
||||
args:
|
||||
DOCKER_USER: ${DOCKER_USER:-1000}
|
||||
|
||||
frontend-dev:
|
||||
extends: frontend-base
|
||||
env_file:
|
||||
- env.d/development/frontend.defaults
|
||||
- env.d/development/frontend.local
|
||||
@@ -226,27 +208,16 @@ services:
|
||||
- "8900:3000"
|
||||
|
||||
frontend-tools:
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
extends: frontend-base
|
||||
profiles:
|
||||
- frontend-tools
|
||||
build:
|
||||
context: ./src/frontend
|
||||
dockerfile: Dockerfile
|
||||
volumes:
|
||||
- ./src/backend/core/api/openapi.json:/home/backend/core/api/openapi.json
|
||||
- ./src/frontend/:/home/frontend/
|
||||
|
||||
frontend-tools-amd64:
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
profiles:
|
||||
- frontend-tools
|
||||
extends: frontend-tools
|
||||
platform: linux/amd64
|
||||
build:
|
||||
context: ./src/frontend
|
||||
dockerfile: Dockerfile
|
||||
volumes:
|
||||
- ./src/backend/core/api/openapi.json:/home/backend/core/api/openapi.json
|
||||
- ./src/frontend/:/home/frontend/
|
||||
|
||||
crowdin:
|
||||
image: crowdin/cli:4.11.0
|
||||
@@ -366,13 +337,16 @@ services:
|
||||
volumes:
|
||||
- ./src/keycloak/realm.json:/opt/keycloak/data/import/realm.json:ro
|
||||
- ./src/keycloak/themes/dsfr-2.2.1.jar:/opt/keycloak/providers/keycloak-theme.jar:ro
|
||||
environment:
|
||||
- HOST=http://localhost:8902
|
||||
- ADMIN_HOST=http://localhost:8902
|
||||
command:
|
||||
- start-dev
|
||||
- --features=preview
|
||||
- --import-realm
|
||||
- --proxy=edge
|
||||
- --hostname=http://localhost:8902
|
||||
- --hostname-admin=http://localhost:8902/
|
||||
- --hostname=$${HOST}
|
||||
- --hostname-admin=$${ADMIN_HOST}
|
||||
- --http-port=8802
|
||||
env_file:
|
||||
- env.d/development/keycloak.defaults
|
||||
|
||||
@@ -229,7 +229,6 @@ The application uses a new environment file structure with `.defaults` and `.loc
|
||||
|----------|---------|-------------|----------|
|
||||
| `FRONTEND_THEME` | `dsfr` | Frontend theme identifier | Optional |
|
||||
| `NEXT_PUBLIC_API_ORIGIN` | `http://localhost:8901` | Frontend API origin | Dev |
|
||||
| `NEXT_PUBLIC_S3_DOMAIN_REPLACE` | `http://localhost:9000` | S3 domain replacement for frontend | Dev |
|
||||
| `NEXT_PUBLIC_LANGUAGES` | `[["en-US","English"],["fr-FR","Français"],["nl-NL","Nederlands"]]` | Languages available for frontend | Optional |
|
||||
| `NEXT_PUBLIC_DEFAULT_LANGUAGE` | `en-US` | Default language for frontend | Optional |
|
||||
|
||||
|
||||
34
env.d/development/backend.e2e
Normal file
34
env.d/development/backend.e2e
Normal file
@@ -0,0 +1,34 @@
|
||||
# App database configuration
|
||||
DB_HOST=postgresql
|
||||
|
||||
# Redis configuration
|
||||
REDIS_URL=redis://redis:6379
|
||||
CELERY_BROKER_URL=redis://redis:6379
|
||||
|
||||
# OpenSearch configuration
|
||||
OPENSEARCH_URL=http://opensearch:9200
|
||||
|
||||
# OIDC
|
||||
OIDC_OP_JWKS_ENDPOINT=http://keycloak:8802/realms/messages/protocol/openid-connect/certs
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT=http://keycloak:8802/realms/messages/protocol/openid-connect/auth
|
||||
OIDC_OP_TOKEN_ENDPOINT=http://keycloak:8802/realms/messages/protocol/openid-connect/token
|
||||
OIDC_OP_USER_ENDPOINT=http://keycloak:8802/realms/messages/protocol/openid-connect/userinfo
|
||||
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS=["http://keycloak:8802", "http://nginx"]
|
||||
|
||||
LOGIN_REDIRECT_URL=http://nginx
|
||||
LOGIN_REDIRECT_URL_FAILURE=http://nginx
|
||||
LOGOUT_REDIRECT_URL=http://nginx
|
||||
|
||||
# Keycloak
|
||||
KEYCLOAK_URL=http://keycloak:8802
|
||||
|
||||
# Object storage configuration
|
||||
STORAGE_MESSAGE_IMPORTS_ENDPOINT_URL=http://objectstorage:9000
|
||||
AWS_S3_DOMAIN_REPLACE=
|
||||
|
||||
# Email configuration (use mailcatcher from main services or disable)
|
||||
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
|
||||
|
||||
# Debug
|
||||
DJANGO_DEBUG=True
|
||||
@@ -1,5 +1,4 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://localhost:8901
|
||||
NEXT_PUBLIC_S3_DOMAIN_REPLACE=http://localhost:9000
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
NEXT_PUBLIC_FEEDBACK_WIDGET_API_URL=http://localhost:8901/api/v1.0/inbound/widget/
|
||||
NEXT_PUBLIC_FEEDBACK_WIDGET_PATH=http://localhost:8905/dist/
|
||||
|
||||
4
env.d/development/frontend.e2e
Normal file
4
env.d/development/frontend.e2e
Normal file
@@ -0,0 +1,4 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://nginx
|
||||
NEXT_PUBLIC_FEEDBACK_WIDGET_API_URL=
|
||||
NEXT_PUBLIC_FEEDBACK_WIDGET_PATH=
|
||||
NEXT_PUBLIC_FEEDBACK_WIDGET_CHANNEL=
|
||||
1
env.d/development/keycloak.e2e
Normal file
1
env.d/development/keycloak.e2e
Normal file
@@ -0,0 +1 @@
|
||||
KC_DB_URL_HOST=postgresql
|
||||
1
env.d/development/mta-in.e2e
Normal file
1
env.d/development/mta-in.e2e
Normal file
@@ -0,0 +1 @@
|
||||
MDA_API_BASE_URL=http://backend:8000/api/v1.0/
|
||||
228
src/backend/core/management/commands/e2e_demo.py
Normal file
228
src/backend/core/management/commands/e2e_demo.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
Django management command to bootstrap E2E demo data.
|
||||
|
||||
This command creates demo users, mailboxes, and shared mailboxes for E2E testing
|
||||
across different BROWSERS (chromium, firefox, webkit).
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
|
||||
from core import models
|
||||
from core.enums import MailboxRoleChoices, MailDomainAccessRoleChoices
|
||||
from core.services.identity.keycloak import get_keycloak_admin_client
|
||||
|
||||
BROWSERS = ["chromium", "firefox", "webkit"]
|
||||
DOMAIN_NAME = "example.local"
|
||||
SHARED_MAILBOX_LOCAL_PART = "shared.e2e"
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Create data for E2E demo data for testing."""
|
||||
|
||||
help = "Create data for E2E demo (users and mailboxes)"
|
||||
|
||||
@transaction.atomic
|
||||
def handle(self, *args, **options):
|
||||
"""Execute the command."""
|
||||
if not settings.ENVIRONMENT == "e2e":
|
||||
self.stdout.write(self.style.WARNING("Not in E2E environment"))
|
||||
return
|
||||
|
||||
self.stdout.write(self.style.WARNING("\n\n| Creating E2E Demo Data\n"))
|
||||
|
||||
# Step 1: Get or create the domain
|
||||
self.stdout.write(f"\n-- 1/4 📦 Setting up domain: {DOMAIN_NAME}")
|
||||
domain, domain_created = models.MailDomain.objects.get_or_create(
|
||||
name=DOMAIN_NAME,
|
||||
defaults={
|
||||
"oidc_autojoin": True,
|
||||
"identity_sync": True,
|
||||
},
|
||||
)
|
||||
if domain_created:
|
||||
self.stdout.write(self.style.SUCCESS(f" ✓ Created domain: {DOMAIN_NAME}"))
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f" ✓ Domain already exists: {DOMAIN_NAME}")
|
||||
)
|
||||
|
||||
# Step 2: Create users per browser
|
||||
self.stdout.write(
|
||||
f"\n-- 2/4 👥 Creating users for BROWSERS: {', '.join(BROWSERS)}"
|
||||
)
|
||||
|
||||
regular_users = []
|
||||
mailbox_admin_users = []
|
||||
|
||||
for browser in BROWSERS:
|
||||
self.stdout.write(f"\n---- Browser: {browser}")
|
||||
|
||||
# Create superuser
|
||||
superuser_email = f"super_admin.e2e.{browser}@{DOMAIN_NAME}"
|
||||
self._create_user_with_mailbox(superuser_email, domain, is_superuser=True)
|
||||
|
||||
# Create domain admin user and mailbox
|
||||
domain_admin_email = f"domain_admin.e2e.{browser}@{DOMAIN_NAME}"
|
||||
domain_admin_user, domain_admin_mailbox = self._create_user_with_mailbox(
|
||||
domain_admin_email, domain, is_domain_admin=True
|
||||
)
|
||||
|
||||
# Create regular user and mailbox
|
||||
regular_email = f"user.e2e.{browser}@{DOMAIN_NAME}"
|
||||
regular_user, regular_mailbox = self._create_user_with_mailbox(
|
||||
regular_email, domain
|
||||
)
|
||||
regular_users.append((regular_user, regular_mailbox))
|
||||
|
||||
# Create mailbox admin user and mailbox
|
||||
mailbox_admin_email = f"mailbox_admin.e2e.{browser}@{DOMAIN_NAME}"
|
||||
mailbox_admin_user, mailbox_admin_mailbox = self._create_user_with_mailbox(
|
||||
mailbox_admin_email, domain
|
||||
)
|
||||
mailbox_admin_users.append((mailbox_admin_user, mailbox_admin_mailbox))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f" ✓ Mailbox admin: {mailbox_admin_email}")
|
||||
)
|
||||
|
||||
# Step 3: Create shared mailbox
|
||||
self.stdout.write(
|
||||
f"\n-- 3/4 📥 Creating shared mailbox: {SHARED_MAILBOX_LOCAL_PART}@{DOMAIN_NAME}"
|
||||
)
|
||||
shared_mailbox = self._create_shared_mailbox(SHARED_MAILBOX_LOCAL_PART, domain)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f" ✓ Shared mailbox created: {SHARED_MAILBOX_LOCAL_PART}@{DOMAIN_NAME}"
|
||||
)
|
||||
)
|
||||
|
||||
# Step 4: Add all regular users with sender role to the shared mailbox
|
||||
self.stdout.write(
|
||||
"\n-- 4/4 🔐 Adding users to shared mailbox with appropriate roles"
|
||||
)
|
||||
for user, _ in regular_users:
|
||||
self._add_mailbox_access(shared_mailbox, user, MailboxRoleChoices.SENDER)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f" ✓ Added {user.email} as SENDER to shared mailbox"
|
||||
)
|
||||
)
|
||||
|
||||
# Step 5: Add mailbox admin users with admin role to the shared mailbox
|
||||
for user, _ in mailbox_admin_users:
|
||||
self._add_mailbox_access(shared_mailbox, user, MailboxRoleChoices.ADMIN)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f" ✓ Added {user.email} as ADMIN to shared mailbox")
|
||||
)
|
||||
|
||||
def _create_user_with_mailbox(
|
||||
self, email, domain, is_domain_admin=False, is_superuser=False
|
||||
):
|
||||
"""Create a user with a personal mailbox."""
|
||||
local_part = email.split("@")[0]
|
||||
full_name = local_part.replace(".", " ").replace("-", " ").title()
|
||||
|
||||
# Create or get user
|
||||
user, user_created = models.User.objects.get_or_create(
|
||||
email=email,
|
||||
defaults={
|
||||
"is_superuser": is_superuser,
|
||||
"full_name": full_name,
|
||||
"password": "!",
|
||||
},
|
||||
)
|
||||
|
||||
keycloak_admin = get_keycloak_admin_client()
|
||||
user_id = None
|
||||
|
||||
# Create or get mailbox
|
||||
mailbox, mailbox_created = models.Mailbox.objects.get_or_create(
|
||||
local_part=local_part,
|
||||
domain=domain,
|
||||
defaults={
|
||||
"is_identity": True,
|
||||
},
|
||||
)
|
||||
|
||||
# Create or get contact
|
||||
contact, _ = models.Contact.objects.get_or_create(
|
||||
email=email,
|
||||
mailbox=mailbox,
|
||||
defaults={"name": full_name},
|
||||
)
|
||||
if not mailbox.contact:
|
||||
mailbox.contact = contact
|
||||
mailbox.save()
|
||||
|
||||
# Give the user admin access to their own mailbox
|
||||
models.MailboxAccess.objects.get_or_create(
|
||||
mailbox=mailbox,
|
||||
user=user,
|
||||
defaults={"role": MailboxRoleChoices.ADMIN},
|
||||
)
|
||||
|
||||
# If this is a domain admin, grant domain access
|
||||
if is_domain_admin:
|
||||
models.MailDomainAccess.objects.get_or_create(
|
||||
maildomain=domain,
|
||||
user=user,
|
||||
defaults={"role": MailDomainAccessRoleChoices.ADMIN},
|
||||
)
|
||||
|
||||
# Set password for user in OIDC
|
||||
users = get_keycloak_admin_client().get_users({"email": str(mailbox)})
|
||||
if len(users) > 0:
|
||||
user_id = users[0].get("id")
|
||||
keycloak_admin.set_user_password(
|
||||
user_id=user_id,
|
||||
password="e2e", # noqa: S106
|
||||
temporary=False,
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"✓ Password set for user {user.email} in Keycloak.")
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f"✗ User {user.email} not found in Keycloak.")
|
||||
)
|
||||
|
||||
return user, mailbox
|
||||
|
||||
def _create_shared_mailbox(self, local_part, domain):
|
||||
"""Create a shared mailbox."""
|
||||
email = f"{local_part}@{domain.name}"
|
||||
mailbox_name = local_part.replace("-", " ").title()
|
||||
|
||||
# Create or get mailbox
|
||||
mailbox, mailbox_created = models.Mailbox.objects.get_or_create(
|
||||
local_part=local_part,
|
||||
domain=domain,
|
||||
defaults={
|
||||
"is_identity": False, # Shared mailbox
|
||||
},
|
||||
)
|
||||
|
||||
# Create or get contact for the shared mailbox
|
||||
contact, _ = models.Contact.objects.get_or_create(
|
||||
email=email,
|
||||
mailbox=mailbox,
|
||||
defaults={"name": mailbox_name},
|
||||
)
|
||||
if not mailbox.contact:
|
||||
mailbox.contact = contact
|
||||
mailbox.save()
|
||||
|
||||
return mailbox
|
||||
|
||||
def _add_mailbox_access(self, mailbox, user, role):
|
||||
"""Add or update mailbox access for a user."""
|
||||
access, created = models.MailboxAccess.objects.get_or_create(
|
||||
mailbox=mailbox,
|
||||
user=user,
|
||||
defaults={"role": role},
|
||||
)
|
||||
if not created and access.role != role:
|
||||
access.role = role
|
||||
access.save()
|
||||
return access
|
||||
@@ -940,6 +940,20 @@ class Development(Base):
|
||||
self.INSTALLED_APPS += ["django_extensions", "drf_spectacular_sidecar"]
|
||||
|
||||
|
||||
class E2E(Development):
|
||||
"""
|
||||
End2End environment settings
|
||||
|
||||
Uses nginx reverse proxy to serve both frontend and backend on the same origin,
|
||||
avoiding cross-origin cookie issues.
|
||||
"""
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = ["http://nginx", "http://keycloak:8802"]
|
||||
|
||||
# Trust X-Forwarded-* headers from nginx proxy
|
||||
USE_X_FORWARDED_HOST = True
|
||||
|
||||
|
||||
class DevelopmentMinimal(Development):
|
||||
"""
|
||||
Development environment settings with minimal dependencies
|
||||
|
||||
1
src/e2e/.dockerignore
Normal file
1
src/e2e/.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
6
src/e2e/.gitignore
vendored
Normal file
6
src/e2e/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
test-results/
|
||||
playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
.auth
|
||||
41
src/e2e/Dockerfile
Normal file
41
src/e2e/Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
||||
# E2E tests Dockerfile
|
||||
|
||||
# Use docker CLI image to get docker binaries
|
||||
FROM docker:27-cli AS docker-cli
|
||||
|
||||
# Main image
|
||||
FROM node:22-slim
|
||||
|
||||
ENV npm_config_cache=/tmp/npm-cache
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/tmp/playwright-browsers
|
||||
|
||||
ENV HOME=/tmp
|
||||
RUN npm install -g npm@11.6.2 && npm cache clean -f
|
||||
|
||||
# Copy docker and docker-compose binaries from docker-cli image
|
||||
# Create the cli-plugins directory first
|
||||
ARG DOCKER_USER
|
||||
RUN mkdir -p /usr/local/libexec/docker/cli-plugins
|
||||
COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker
|
||||
COPY --from=docker-cli /usr/local/libexec/docker/cli-plugins/docker-compose /usr/local/libexec/docker/cli-plugins/docker-compose
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
# Install deps and run all npm commands as the user running the container
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies and postinstall playwright with deps
|
||||
RUN npm install
|
||||
|
||||
# Copy test files
|
||||
COPY . ./
|
||||
|
||||
# Fix ownership for the user (use numeric UID to avoid lookup issues)
|
||||
RUN chown -R $DOCKER_USER /tmp/npm-cache /app
|
||||
|
||||
# Run test with unprivileged user
|
||||
USER $DOCKER_USER
|
||||
# Default command
|
||||
CMD ["npm", "test"]
|
||||
|
||||
52
src/e2e/README.md
Normal file
52
src/e2e/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Messages E2E Tests
|
||||
|
||||
End-to-end tests for the Messages application using Playwright.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed
|
||||
- Messages project configured
|
||||
|
||||
## Running the tests
|
||||
|
||||
### In headless mode (CI)
|
||||
|
||||
```bash
|
||||
make e2e-test
|
||||
```
|
||||
|
||||
### In UI mode
|
||||
|
||||
```bash
|
||||
make e2e-test-ui
|
||||
```
|
||||
|
||||
Open the Playwright UI on http://localhost:8932 to write and debug the tests interactively.
|
||||
|
||||
### In Dev mode
|
||||
|
||||
Start playwright in UI Mode and use the dev frontend service to avoid rebuilding
|
||||
the frontend after each change.
|
||||
```bash
|
||||
make e2e-test-dev
|
||||
```
|
||||
|
||||
Open the Playwright UI on http://localhost:8932 to write and debug the tests interactively.
|
||||
|
||||
## Explanation
|
||||
|
||||
### Isolated services
|
||||
|
||||
E2E tests use [dedicated services](./compose.yaml) especially for the database and the object storage.
|
||||
|
||||
### Nginx to serve the frontend and the backend
|
||||
|
||||
Nginx is used to serve the frontend and the backend on the same origin, avoiding cross-origin cookie issues.
|
||||
|
||||
### Environment variables
|
||||
|
||||
E2E configuration files are located in `env.d/development/*.e2e`:
|
||||
- `backend.e2e`: Backend configuration for tests
|
||||
- `frontend.e2e`: Frontend configuration for tests
|
||||
- `keycloak.e2e`: Keycloak configuration for tests
|
||||
|
||||
18
src/e2e/bin/backend-manage.sh
Executable file
18
src/e2e/bin/backend-manage.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# Helper script to run Django management commands in the backend container.
|
||||
# Through the docker sock proxy service, it is possible to run commands in the
|
||||
# backend container from within the runner container.
|
||||
#
|
||||
# Usage: ./backend-manage.sh <command> [args...]
|
||||
# Examples:
|
||||
# ./backend-manage.sh flush --noinput
|
||||
# ./backend-manage.sh drop_all_tables
|
||||
# ./backend-manage.sh migrate
|
||||
# ./backend-manage.sh createsuperuser
|
||||
|
||||
set -e
|
||||
|
||||
# Run the Django management command in the backend container
|
||||
docker compose -f /app/compose.yaml -p st-messages-e2e \
|
||||
exec -T backend python manage.py "$@"
|
||||
|
||||
176
src/e2e/compose.yaml
Normal file
176
src/e2e/compose.yaml
Normal file
@@ -0,0 +1,176 @@
|
||||
name: st-messages-e2e
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
extends:
|
||||
file: ../../compose.yaml
|
||||
service: postgresql
|
||||
ports: !reset []
|
||||
|
||||
redis:
|
||||
extends:
|
||||
file: ../../compose.yaml
|
||||
service: redis
|
||||
ports: !reset []
|
||||
|
||||
celery:
|
||||
extends:
|
||||
file: ../../compose.yaml
|
||||
service: celery-dev
|
||||
environment:
|
||||
- DJANGO_CONFIGURATION=E2E
|
||||
env_file:
|
||||
- ../../env.d/development/backend.defaults
|
||||
- ../../env.d/development/backend.e2e
|
||||
depends_on: !override
|
||||
- backend
|
||||
|
||||
opensearch:
|
||||
extends:
|
||||
file: ../../compose.yaml
|
||||
service: opensearch
|
||||
ports: !reset []
|
||||
|
||||
objectstorage:
|
||||
extends:
|
||||
file: ../../compose.yaml
|
||||
service: objectstorage
|
||||
volumes: !reset []
|
||||
ports: !reset []
|
||||
|
||||
objectstorage-createbucket:
|
||||
extends:
|
||||
file: ../../compose.yaml
|
||||
service: objectstorage-createbucket
|
||||
|
||||
keycloak:
|
||||
extends:
|
||||
file: ../../compose.yaml
|
||||
service: keycloak
|
||||
environment:
|
||||
- HOST=http://keycloak:8802
|
||||
- ADMIN_HOST=http://keycloak:8802
|
||||
command:
|
||||
- start-dev
|
||||
- --features=preview
|
||||
- --import-realm
|
||||
- --proxy=edge
|
||||
- --hostname=$${HOST}
|
||||
- --hostname-admin=$${ADMIN_HOST}
|
||||
- --http-port=8802
|
||||
env_file:
|
||||
- ../../env.d/development/keycloak.defaults
|
||||
- ../../env.d/development/keycloak.e2e
|
||||
ports: !reset []
|
||||
depends_on: !override
|
||||
- postgresql
|
||||
|
||||
backend:
|
||||
extends:
|
||||
file: ../../compose.yaml
|
||||
service: backend-base
|
||||
environment:
|
||||
- DJANGO_CONFIGURATION=E2E
|
||||
env_file:
|
||||
- ../../env.d/development/backend.defaults
|
||||
- ../../env.d/development/backend.e2e
|
||||
depends_on: !override
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
objectstorage:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
opensearch:
|
||||
condition: service_healthy
|
||||
keycloak:
|
||||
condition: service_started
|
||||
|
||||
mta-in:
|
||||
extends:
|
||||
file: ../../compose.yaml
|
||||
service: mta-in
|
||||
env_file:
|
||||
- ../../env.d/development/mta-in.defaults
|
||||
- ../../env.d/development/mta-in.e2e
|
||||
ports: !reset []
|
||||
depends_on: !override
|
||||
- backend
|
||||
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
environment:
|
||||
- E2E_PROFILE=${E2E_PROFILE:-e2e}
|
||||
volumes:
|
||||
- ./nginx/e2e.conf.template:/etc/nginx/templates/e2e.conf.template:ro
|
||||
- ./nginx/entrypoint.sh:/entrypoint.sh:ro
|
||||
entrypoint: "/entrypoint.sh"
|
||||
depends_on:
|
||||
frontend:
|
||||
condition: service_started
|
||||
frontend-dev:
|
||||
condition: service_started
|
||||
required: false
|
||||
backend:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
extends:
|
||||
file: ../../compose.yaml
|
||||
service: frontend-base
|
||||
build:
|
||||
target: frontend-build
|
||||
args:
|
||||
API_ORIGIN: http://nginx
|
||||
command: ["npx", "serve", "-s", "out"]
|
||||
ports: !reset []
|
||||
|
||||
frontend-dev:
|
||||
profiles: [dev]
|
||||
extends:
|
||||
file: ../../compose.yaml
|
||||
service: frontend-base
|
||||
command: ["npm", "run", "dev"]
|
||||
env_file:
|
||||
- ../../env.d/development/frontend.defaults
|
||||
- ../../env.d/development/frontend.e2e
|
||||
volumes:
|
||||
- ../frontend/:/home/frontend/
|
||||
ports: !reset []
|
||||
|
||||
# Service to proxy the Docker socket to the tools container
|
||||
# This is necessary to keep the tools container rootless
|
||||
docker-sock-proxy:
|
||||
image: alpine/socat
|
||||
command: tcp-listen:2375,fork,reuseaddr unix-connect:/var/run/docker.sock
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
|
||||
runner:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
DOCKER_USER: ${DOCKER_USER:-1000}
|
||||
user: ${DOCKER_USER:-1000}
|
||||
environment:
|
||||
- FRONTEND_BASE_URL=http://nginx
|
||||
- BACKEND_BASE_URL=http://nginx
|
||||
- KEYCLOAK_BASE_URL=http://keycloak:8802
|
||||
- DOCKER_HOST=tcp://docker-sock-proxy:2375
|
||||
volumes:
|
||||
- ./src:/app/src
|
||||
- ./playwright.config.ts:/app/playwright.config.ts
|
||||
- ../../env.d/development:/app/env.d/development:ro
|
||||
depends_on:
|
||||
docker-sock-proxy:
|
||||
condition: service_started
|
||||
nginx:
|
||||
condition: service_started
|
||||
mta-in:
|
||||
condition: service_started
|
||||
celery:
|
||||
condition: service_started
|
||||
command: npm run test
|
||||
ports:
|
||||
- "8932:8932"
|
||||
57
src/e2e/nginx/e2e.conf.template
Normal file
57
src/e2e/nginx/e2e.conf.template
Normal file
@@ -0,0 +1,57 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# Frontend - serve on root
|
||||
location / {
|
||||
proxy_pass http://${FRONTEND_SERVICE_NAME}:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Backend API - serve on /api prefix
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
}
|
||||
|
||||
# Backend OIDC authentication
|
||||
location /oidc/ {
|
||||
proxy_pass http://backend:8000/oidc/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
}
|
||||
|
||||
# Backend admin (if needed)
|
||||
location /admin/ {
|
||||
proxy_pass http://backend:8000/admin/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Backend healthcheck
|
||||
location /healthz/ {
|
||||
proxy_pass http://backend:8000/healthz/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
|
||||
23
src/e2e/nginx/entrypoint.sh
Executable file
23
src/e2e/nginx/entrypoint.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Generate the nginx config from the template according to the E2E_PROFILE environment variable
|
||||
# If the E2E_PROFILE is dev, use the frontend-dev service, otherwise use the frontend service
|
||||
|
||||
set -e
|
||||
|
||||
# Set FRONTEND_SUFFIX based on E2E_PROFILE
|
||||
if [ "$E2E_PROFILE" = "dev" ]; then
|
||||
export FRONTEND_SERVICE_NAME="frontend-dev"
|
||||
else
|
||||
export FRONTEND_SERVICE_NAME="frontend"
|
||||
fi
|
||||
|
||||
# Generate nginx config from template
|
||||
envsubst '${FRONTEND_SERVICE_NAME}' < /etc/nginx/templates/e2e.conf.template > /etc/nginx/conf.d/default.conf
|
||||
|
||||
echo "Generated nginx config for E2E_PROFILE=${E2E_PROFILE:-e2e} (FRONTEND_SERVICE_NAME=${FRONTEND_SERVICE_NAME})"
|
||||
|
||||
# Start nginx
|
||||
exec nginx -g 'daemon off;'
|
||||
|
||||
|
||||
97
src/e2e/package-lock.json
generated
Normal file
97
src/e2e/package-lock.json
generated
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"name": "messages-e2e",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "messages-e2e",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.48.0",
|
||||
"@types/node": "^22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
|
||||
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.56.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
|
||||
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
|
||||
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.56.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
|
||||
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/e2e/package.json
Normal file
31
src/e2e/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "st-messages-e2e",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "End-to-end tests for Messages application",
|
||||
"engines": {
|
||||
"node": ">=22.0.0 <23.0.0",
|
||||
"npm": ">=10.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:ui": "playwright test --ui --ui-host=0.0.0.0 --ui-port=8932",
|
||||
"test:headed": "playwright test --headed",
|
||||
"test:debug": "playwright test --debug",
|
||||
"codegen": "playwright codegen",
|
||||
"report": "open ./playwright-report/index.html",
|
||||
"postinstall": "playwright install --with-deps",
|
||||
"django": "./bin/backend-manage.sh",
|
||||
"db:flush": "npm run django flush -- --no-input",
|
||||
"db:bootstrap": "npm run django e2e_demo",
|
||||
"db:reset": "npm run db:flush && npm run db:bootstrap"
|
||||
},
|
||||
"keywords": ["e2e", "playwright", "testing"],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.56.1",
|
||||
"@types/node": "22.19.1"
|
||||
}
|
||||
}
|
||||
|
||||
64
src/e2e/playwright.config.ts
Normal file
64
src/e2e/playwright.config.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './src/__tests__',
|
||||
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: false,
|
||||
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 1,
|
||||
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: process.env.CI ? 'dot' : [['list'], ['html', { host: '0.0.0.0', port: 8932, outputDir: './src/__tests__/playwright-report' }]],
|
||||
outputDir: './src/__tests__/playwright-report',
|
||||
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: process.env.FRONTEND_BASE_URL,
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
|
||||
/* Screenshot on failure */
|
||||
screenshot: 'only-on-failure',
|
||||
|
||||
/* Video on failure */
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: process.env.SKIP_WEBSERVER ? undefined : {
|
||||
command: 'echo "Waiting for services to be ready..."',
|
||||
url: process.env.FRONTEND_BASE_URL,
|
||||
reuseExistingServer: true,
|
||||
timeout: 120 * 1000,
|
||||
},
|
||||
});
|
||||
|
||||
16
src/e2e/tsconfig.json
Normal file
16
src/e2e/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node", "@playwright/test"]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
3
src/frontend/.dockerignore
Normal file
3
src/frontend/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
out
|
||||
.next
|
||||
@@ -1,10 +1,26 @@
|
||||
FROM node:22-slim AS frontend-deps
|
||||
|
||||
WORKDIR /home/frontend/
|
||||
|
||||
ENV npm_config_cache=/tmp/npm-cache
|
||||
RUN npm install -g npm@11.6.2 && npm cache clean -f
|
||||
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
WORKDIR /home/frontend/
|
||||
|
||||
ENV npm_config_cache=/tmp/npm-cache
|
||||
RUN chown -R ${DOCKER_USER} /tmp/npm-cache
|
||||
RUN chown -R ${DOCKER_USER} /home/frontend
|
||||
|
||||
# Install deps and run all npm commands as the user running the container
|
||||
USER ${DOCKER_USER}
|
||||
COPY --chown=${DOCKER_USER} package*.json ./
|
||||
RUN npm install
|
||||
|
||||
|
||||
FROM frontend-deps AS frontend-build
|
||||
|
||||
WORKDIR /home/frontend/
|
||||
COPY . ./
|
||||
|
||||
ARG API_ORIGIN
|
||||
ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN}
|
||||
|
||||
RUN npm run build
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "npx serve -s out",
|
||||
"lint": "next lint",
|
||||
"build-theme": "cunningham -g css,scss -o src/styles",
|
||||
"api:update": "orval --config ./orval.config.ts",
|
||||
|
||||
@@ -503,70 +503,7 @@
|
||||
"realmRoles" : [ "user" ],
|
||||
"notBefore" : 0,
|
||||
"groups" : [ ]
|
||||
}, {
|
||||
"id" : "5385a8dc-1d4e-4d48-b67b-8cc393122323",
|
||||
"username" : "user-e2e-chromium",
|
||||
"firstName" : "E2E",
|
||||
"lastName" : "Chromium",
|
||||
"email" : "user@chromium.e2e",
|
||||
"emailVerified" : false,
|
||||
"enabled" : true,
|
||||
"totp" : false,
|
||||
"credentials" : [ {
|
||||
"id" : "f67a812e-2989-4b0c-8e29-59d52877cd13",
|
||||
"type" : "password",
|
||||
"createdDate" : 1749245683785,
|
||||
"secretData" : "{\"value\":\"984c+yrQ7Ss7gEUyBKG1hU1rHbs0fWvKh6Y2cykkWZ4=\",\"salt\":\"KQ6A8Ch4tpZkVmsE5tbthw==\",\"additionalParameters\":{}}",
|
||||
"credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
|
||||
} ],
|
||||
"disableableCredentialTypes" : [ ],
|
||||
"requiredActions" : [ ],
|
||||
"realmRoles" : [ "user" ],
|
||||
"notBefore" : 0,
|
||||
"groups" : [ ]
|
||||
}, {
|
||||
"id" : "a6b2fb46-2db3-4d98-abea-7be68c2b81ac",
|
||||
"username" : "user-e2e-firefox",
|
||||
"firstName" : "E2E",
|
||||
"lastName" : "Firefox",
|
||||
"email" : "user@firefox.e2e",
|
||||
"emailVerified" : false,
|
||||
"enabled" : true,
|
||||
"totp" : false,
|
||||
"credentials" : [ {
|
||||
"id" : "d94f507b-7a93-4f82-a29b-d3024da1e6cb",
|
||||
"type" : "password",
|
||||
"createdDate" : 1749245683848,
|
||||
"secretData" : "{\"value\":\"qTc3ucpr7axbMM/ex++wM3K1S0OoLIZ9yAiZCog8ydY=\",\"salt\":\"AbBiB8zqIfy0fXsBr0zjbQ==\",\"additionalParameters\":{}}",
|
||||
"credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
|
||||
} ],
|
||||
"disableableCredentialTypes" : [ ],
|
||||
"requiredActions" : [ ],
|
||||
"realmRoles" : [ "user" ],
|
||||
"notBefore" : 0,
|
||||
"groups" : [ ]
|
||||
}, {
|
||||
"id" : "7315d5ac-0cd5-4ce1-8759-4bf95d076ba1",
|
||||
"username" : "user-e2e-webkit",
|
||||
"firstName" : "E2E",
|
||||
"lastName" : "Webkit",
|
||||
"email" : "user@webkit.e2e",
|
||||
"emailVerified" : false,
|
||||
"enabled" : true,
|
||||
"totp" : false,
|
||||
"credentials" : [ {
|
||||
"id" : "9d2e6d7d-21d0-4e3d-a7d1-40ac820435c4",
|
||||
"type" : "password",
|
||||
"createdDate" : 1749245683819,
|
||||
"secretData" : "{\"value\":\"akhpAqR4qEgEAPpBng130u9xMNP1allIPOwx2Q3tCiw=\",\"salt\":\"THM80YVFqeGJmaXvb8izLA==\",\"additionalParameters\":{}}",
|
||||
"credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
|
||||
} ],
|
||||
"disableableCredentialTypes" : [ ],
|
||||
"requiredActions" : [ ],
|
||||
"realmRoles" : [ "user" ],
|
||||
"notBefore" : 0,
|
||||
"groups" : [ ]
|
||||
} ],
|
||||
}],
|
||||
"scopeMappings" : [ {
|
||||
"clientScope" : "offline_access",
|
||||
"roles" : [ "offline_access" ]
|
||||
@@ -720,8 +657,8 @@
|
||||
"alwaysDisplayInConsole" : false,
|
||||
"clientAuthenticatorType" : "client-secret",
|
||||
"secret" : "ThisIsAnExampleKeyForDevPurposeOnly",
|
||||
"redirectUris" : [ "http://localhost:8900/*", "http://localhost:8901/*", "http://localhost:8902/*" ],
|
||||
"webOrigins" : [ "http://localhost:8900", "http://localhost:8901", "http://localhost:8902" ],
|
||||
"redirectUris" : [ "http://localhost:8900/*", "http://localhost:8901/*", "http://localhost:8902/*", "http://backend:8000/*", "http://frontend:3000/*", "http://keycloak:8802/*", "http://nginx/*"],
|
||||
"webOrigins" : [ "http://localhost:8900", "http://localhost:8901", "http://localhost:8902", "http://backend:8000", "http://frontend:3000", "http://keycloak:8802", "http://nginx" ],
|
||||
"notBefore" : 0,
|
||||
"bearerOnly" : false,
|
||||
"consentRequired" : false,
|
||||
@@ -736,7 +673,7 @@
|
||||
"access.token.lifespan" : "-1",
|
||||
"client.secret.creation.time" : "1707820779",
|
||||
"user.info.response.signature.alg" : "RS256",
|
||||
"post.logout.redirect.uris" : "http://localhost:8900/*##http://localhost:8901/*##http://localhost:8902/*",
|
||||
"post.logout.redirect.uris" : "http://localhost:8900/*##http://localhost:8901/*##http://localhost:8902/*##http://backend:8000/*##http://frontend:3000/*##http://keycloak:8802/*##http://nginx/*",
|
||||
"oauth2.device.authorization.grant.enabled" : "false",
|
||||
"use.jwks.url" : "false",
|
||||
"backchannel.logout.revoke.offline.tokens" : "false",
|
||||
|
||||
Reference in New Issue
Block a user