From d04e7e6cef517f58211be3c54d48cfbbeea6d24e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20M=C3=B3r=C3=A1sz?= Date: Sun, 5 Apr 2026 10:03:13 +0200 Subject: [PATCH] Add e2e tests (#84) * add basic e2e tests * add e2e to ci * fix docker pull * fix images in ci * maybe this will help * push latest on main for e2e * fix e2e on main * rename jobs --- .github/workflows/ci.yml | 177 +++++++++++++++++++++++++-------- .gitignore | 10 +- README.md | 10 ++ docker-compose.e2e.yaml | 82 +++++++++++++++ e2e/.dockerignore | 5 + e2e/Dockerfile | 18 ++++ e2e/README.md | 57 +++++++++++ e2e/docker-entrypoint.sh | 43 ++++++++ e2e/package.json | 14 +++ e2e/playwright.config.ts | 43 ++++++++ e2e/tests/public-pages.spec.ts | 49 +++++++++ e2e/yarn.lock | 29 ++++++ frontend/Dockerfile | 2 + frontend/package.json | 6 +- nginx.e2e.conf | 96 ++++++++++++++++++ scripts/generate-e2e-certs.sh | 61 ++++++++++++ 16 files changed, 659 insertions(+), 43 deletions(-) create mode 100644 docker-compose.e2e.yaml create mode 100644 e2e/.dockerignore create mode 100644 e2e/Dockerfile create mode 100644 e2e/README.md create mode 100644 e2e/docker-entrypoint.sh create mode 100644 e2e/package.json create mode 100644 e2e/playwright.config.ts create mode 100644 e2e/tests/public-pages.spec.ts create mode 100644 e2e/yarn.lock create mode 100644 nginx.e2e.conf create mode 100644 scripts/generate-e2e-certs.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 704c019..f6e3114 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,13 +1,18 @@ -name: CI compile & test & build +name: CI on: push: branches: [ "main" ] pull_request: branches: [ "main" ] + workflow_dispatch: + inputs: + backend_image: + description: 'Backend image to use in E2E tests (optional, default: ghcr.io/morbalint/kemkas-backend:b8565f8e)' + required: false jobs: - ci-fe: + unit-test-and-lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -37,46 +42,136 @@ jobs: cd ./frontend yarn run lint - docker-fe-build: + build-fe: runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write outputs: - image_tag: ${{ steps.get_image_tag.outputs.image_tag }} + image_sha_tag: ${{ steps.image-tag.outputs.image_sha_tag }} steps: - - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - name: Log in to GitHub Container Registry - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - id: get_image_tag - run: | - echo "GITHUB_SHA_SHORT=$(echo ${GITHUB_SHA::8})" >> $GITHUB_ENV - echo "ghcr.io/${{ github.repository_owner }}/kemkas-frontend:${GITHUB_SHA::8}" - echo "image_tag=ghcr.io/${{ github.repository_owner }}/kemkas-fe:${GITHUB_SHA::8}" >> $GITHUB_OUTPUT - - uses: docker/build-push-action@v5 - with: - context: ./frontend/ - file: ./frontend/Dockerfile - cache-from: type=gha - cache-to: type=gha,mode=max - push: true - tags: | - ghcr.io/${{ github.repository_owner }}/kemkas-frontend:${{ env.GITHUB_SHA_SHORT }} + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Compute frontend image tag + id: image-tag + run: | + image_repo="ghcr.io/${{ github.repository_owner }}/kemkas-frontend" + sha_tag="${image_repo}:${GITHUB_SHA::8}" -# No longer used, and token expired. - # deploy-to-beta: - # runs-on: ubuntu-latest - # needs: - # - docker-fe-build - # steps: - # - uses: actions/checkout@v4 - # with: - # repository: ${{ github.repository_owner }}/kemkas-deployment - # ref: refs/heads/main - # ssh-key: ${{ secrets.DEPLOYMENT_REPO_DEPLOY_KEY }} - # - uses: mikefarah/yq@v4 - # with: - # cmd: yq -i '.spec.template.spec.containers[0].image = "${{ needs.docker-fe-build.outputs.image_tag }}"' ./kemkas-beta/kemkas-fe-deployment.yaml - # - run: | - # git config --add user.email "beta-fe-deployment-bot@kemkas.hu" - # git config --add user.name "Deployment Bot Beta FE" - # git commit -a -m "deploy ${{ needs.docker-fe-build.outputs.image_tag }}" - # git push + tags="$sha_tag" + if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then + tags+=$'\n'"${image_repo}:latest" + fi + + echo "image_sha_tag=${sha_tag}" >> "$GITHUB_OUTPUT" + { + echo "tags<> "$GITHUB_OUTPUT" + - name: Build and push frontend image + uses: docker/build-push-action@v5 + with: + context: ./frontend/ + file: ./frontend/Dockerfile + cache-from: type=gha + cache-to: type=gha,mode=max + push: true + tags: | + ${{ steps.image-tag.outputs.tags }} + + build-e2e: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write + outputs: + image_sha_tag: ${{ steps.image-tag.outputs.image_sha_tag }} + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Compute e2e image tag + id: image-tag + run: | + image_repo="ghcr.io/${{ github.repository_owner }}/kemkas-e2e" + sha_tag="${image_repo}:${GITHUB_SHA::8}" + + tags="$sha_tag" + if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then + tags+=$'\n'"${image_repo}:latest" + fi + + echo "image_sha_tag=${sha_tag}" >> "$GITHUB_OUTPUT" + { + echo "tags<> "$GITHUB_OUTPUT" + - name: Build and push e2e image + uses: docker/build-push-action@v5 + with: + context: ./e2e/ + file: ./e2e/Dockerfile + cache-from: type=gha + cache-to: type=gha,mode=max + push: true + tags: | + ${{ steps.image-tag.outputs.tags }} + + run-e2e: + runs-on: ubuntu-latest + needs: + - build-fe + - build-e2e + permissions: + contents: read + packages: read + env: + BACKEND_IMAGE: ${{ github.event.inputs.backend_image || 'ghcr.io/morbalint/kemkas-backend:b8565f8e' }} + FRONTEND_IMAGE: ${{ needs.build-fe.outputs.image_sha_tag }} + E2E_IMAGE: ${{ needs.build-e2e.outputs.image_sha_tag }} + steps: + - uses: actions/checkout@v4 + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Pull images for test run + run: docker compose -f docker-compose.e2e.yaml pull + - name: Run Docker Compose E2E tests + run: docker compose -f docker-compose.e2e.yaml up --no-build --abort-on-container-exit --exit-code-from e2e e2e + - name: Upload Playwright HTML report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: e2e/playwright-report + if-no-files-found: ignore + - name: Upload Playwright test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-test-results + path: e2e/test-results + if-no-files-found: ignore + - name: Tear down Docker Compose stack + if: always() + run: docker compose -f docker-compose.e2e.yaml down -v --remove-orphans diff --git a/.gitignore b/.gitignore index 26ed2ba..a09c279 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,12 @@ yarn-error.log* obj/ bin/ -dist/ \ No newline at end of file +dist/ + +# IDEs and editors +.idea/ +.vscode/ + +# playwright +/e2e/playwright-report/ +/e2e/test-results/ \ No newline at end of file diff --git a/README.md b/README.md index c29c563..52aad80 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,13 @@ dotnet dev-certs https --format PEM --no-password -ep ~/.aspnet/https/kemkas.pem ``` Later start the docker compose first and the project launch settings after the DB is up and running. + +## E2E with Docker Compose + +For a fully containerized end-to-end run (tagged backend image + freshly built frontend/e2e images): + +```shell +docker compose -f docker-compose.e2e.yaml up --build --abort-on-container-exit --exit-code-from e2e e2e +``` + +Use `BACKEND_IMAGE` to select a specific backend tag, and note this compose stack auto-generates TLS certificates for the nginx proxy. diff --git a/docker-compose.e2e.yaml b/docker-compose.e2e.yaml new file mode 100644 index 0000000..99cfd38 --- /dev/null +++ b/docker-compose.e2e.yaml @@ -0,0 +1,82 @@ +services: + certs: + image: alpine:3.21 + volumes: + - tls-certs:/certs + - ./scripts/generate-e2e-certs.sh:/scripts/generate-e2e-certs.sh:ro + command: ["sh", "/scripts/generate-e2e-certs.sh"] + + db: + image: postgres:16.13-alpine3.23 + environment: + POSTGRES_USER: "kemkas" + POSTGRES_PASSWORD: "kemkas-dev42" + volumes: + - db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U kemkas -d postgres"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 5s + + backend: + image: ${BACKEND_IMAGE:-ghcr.io/morbalint/kemkas-backend:b8565f8e} + environment: + ConnectionStrings__DefaultConnection: "Server=db;User Id=kemkas;Password=kemkas-dev42" + Email__DomainName: "mail.kemkas.hu" + Email__ApiKey: "" + depends_on: + db: + condition: service_healthy + + frontend: + image: ${FRONTEND_IMAGE:-ghcr.io/morbalint/kemkas-frontend:local} + build: + context: ./frontend + dockerfile: Dockerfile + + proxy: + image: nginx:1.29.7-alpine3.23 + ports: + - "8000:8000" + volumes: + - ./nginx.e2e.conf:/etc/nginx/nginx.conf:ro + - tls-certs:/etc/nginx/certs:ro + depends_on: + certs: + condition: service_completed_successfully + backend: + condition: service_started + frontend: + condition: service_started + healthcheck: + test: ["CMD-SHELL", "wget -q --spider --no-check-certificate https://127.0.0.1:8000 || exit 1"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 5s + + e2e: + image: ${E2E_IMAGE:-ghcr.io/morbalint/kemkas-e2e:local} + build: + context: ./e2e + dockerfile: Dockerfile + environment: + E2E_BASE_URL: https://proxy:8000 + E2E_USE_WEBSERVER: "false" + E2E_TRUSTED_CERT_PATH: /certs/rootCA.pem + E2E_WAIT_FOR_URL: https://proxy:8000 + volumes: + - tls-certs:/certs:ro + - ./e2e/playwright-report:/work/playwright-report + - ./e2e/test-results:/work/test-results + depends_on: + certs: + condition: service_completed_successfully + proxy: + condition: service_healthy + +volumes: + db-data: + tls-certs: diff --git a/e2e/.dockerignore b/e2e/.dockerignore new file mode 100644 index 0000000..84ec0e6 --- /dev/null +++ b/e2e/.dockerignore @@ -0,0 +1,5 @@ +node_modules +playwright-report +test-results +.vscode +.git diff --git a/e2e/Dockerfile b/e2e/Dockerfile new file mode 100644 index 0000000..3a154e5 --- /dev/null +++ b/e2e/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/playwright:v1.59.1-noble + +LABEL org.opencontainers.image.source = "https://github.com/morbalint/kemkas" + +WORKDIR /work + +RUN apt-get update \ + && apt-get install -y --no-install-recommends libnss3-tools \ + && rm -rf /var/lib/apt/lists/* + +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile + +COPY . . +RUN chmod +x /work/docker-entrypoint.sh + +ENTRYPOINT ["/work/docker-entrypoint.sh"] +CMD ["yarn", "test"] diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000..5dd3010 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,57 @@ +# E2E tests (Playwright) + +This folder contains browser-level end-to-end tests for public, backend-independent user flows. + +## Current scope + +- Guest redirect from `/` to `/2e/karakter` +- Guest header links (`Register`, `Login`) +- Public legal pages (`/privacy.html`, `/ogl.html`, `/aelf.html`) + +## Run locally + +From `frontend/`: + +- Install Playwright browser binaries: + - `yarn test:e2e:install` +- Run tests: + - `yarn test:e2e` +- Run headed mode: + - `yarn test:e2e:headed` +- Run interactive UI mode: + - `yarn test:e2e:ui` + +## Notes + +- Tests use the config in `../e2e/playwright.config.ts`. +- The Playwright web server starts Vite on `http://127.0.0.1:4173`. +- Tests mock `GET /api/User/me` so they stay deterministic without the backend. + +## Run with Docker Compose + +This mode runs against a full containerized stack: + +- tagged backend image (`BACKEND_IMAGE`) +- supporting services (`postgres`, `nginx` proxy) +- freshly built `frontend` image +- freshly built `e2e` image + +Prerequisites: + +- Docker with Compose + +Run: + +```shell +docker compose -f docker-compose.e2e.yaml up --build --abort-on-container-exit --exit-code-from e2e e2e +``` + +Run with a different backend tag: + +```shell +BACKEND_IMAGE=ghcr.io/morbalint/kemkas-backend: \ +docker compose -f docker-compose.e2e.yaml up --build --abort-on-container-exit --exit-code-from e2e e2e +``` + +The `certs` service generates a local CA and nginx server certificate in a shared volume. +The `e2e` image imports that CA (`/certs/rootCA.pem`) into both the system trust store and Chromium's NSS store before running tests. diff --git a/e2e/docker-entrypoint.sh b/e2e/docker-entrypoint.sh new file mode 100644 index 0000000..5d4f0a2 --- /dev/null +++ b/e2e/docker-entrypoint.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +trusted_cert_path="${E2E_TRUSTED_CERT_PATH:-}" +if [[ -n "$trusted_cert_path" ]]; then + if [[ ! -f "$trusted_cert_path" ]]; then + echo "Trusted certificate not found at: $trusted_cert_path" >&2 + exit 1 + fi + + install -m 0644 -D "$trusted_cert_path" /usr/local/share/ca-certificates/kemkas-proxy.crt + update-ca-certificates + + if command -v certutil >/dev/null 2>&1; then + for candidate_home in /root /home/pwuser; do + if [[ -d "$candidate_home" ]]; then + nss_db_dir="$candidate_home/.pki/nssdb" + mkdir -p "$nss_db_dir" + certutil -d "sql:$nss_db_dir" -N --empty-password >/dev/null 2>&1 || true + certutil -d "sql:$nss_db_dir" -D -n kemkas-proxy >/dev/null 2>&1 || true + certutil -d "sql:$nss_db_dir" -A -t "C,," -n kemkas-proxy -i "$trusted_cert_path" + fi + done + fi +fi + +if [[ -n "${E2E_WAIT_FOR_URL:-}" ]]; then + echo "Waiting for target URL: ${E2E_WAIT_FOR_URL}" + for attempt in {1..60}; do + if curl --silent --show-error --fail "${E2E_WAIT_FOR_URL}" >/dev/null; then + break + fi + + if [[ "$attempt" -eq 60 ]]; then + echo "Timed out waiting for ${E2E_WAIT_FOR_URL}" >&2 + exit 1 + fi + + sleep 2 + done +fi + +exec "$@" diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..90a1d08 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,14 @@ +{ + "name": "kemkas-e2e", + "version": "0.1.0", + "private": true, + "scripts": { + "test": "playwright test", + "test:headed": "playwright test --headed", + "test:ui": "playwright test --ui", + "install:browsers": "playwright install chromium" + }, + "devDependencies": { + "@playwright/test": "1.59.1" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000..83d29ad --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,43 @@ +import { defineConfig, devices } from '@playwright/test'; +import path from 'node:path'; + +const port = Number(process.env.E2E_PORT ?? 4173); +const baseURL = process.env.E2E_BASE_URL ?? `http://127.0.0.1:${port}`; +const useWebServer = process.env.E2E_USE_WEBSERVER !== 'false'; + +export default defineConfig({ + testDir: path.join(__dirname, 'tests'), + outputDir: path.join(__dirname, 'test-results'), + fullyParallel: true, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 2 : undefined, + timeout: 30_000, + reporter: [ + ['list'], + ['html', { outputFolder: path.join(__dirname, 'playwright-report'), open: 'never' }], + ], + use: { + baseURL, + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + ...(useWebServer + ? { + webServer: { + command: `node ./node_modules/vite/bin/vite.js --host 127.0.0.1 --port ${port}`, + cwd: path.resolve(__dirname, '../frontend'), + port, + timeout: 120_000, + reuseExistingServer: !process.env.CI, + }, + } + : {}), + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/e2e/tests/public-pages.spec.ts b/e2e/tests/public-pages.spec.ts new file mode 100644 index 0000000..76bb0b3 --- /dev/null +++ b/e2e/tests/public-pages.spec.ts @@ -0,0 +1,49 @@ +import { expect, test } from '@playwright/test'; + +async function mockGuestSession(page: import('@playwright/test').Page): Promise { + await page.route('**/api/User/me', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/plain; charset=utf-8', + body: '', + }); + }); +} + +test('guest users are redirected to 2e character creation', async ({ page }) => { + await mockGuestSession(page); + + await page.goto('/'); + + await expect(page).toHaveURL(/\/2e\/karakter$/); + await expect(page.getByRole('heading', { name: 'Karakter létrehozása' })).toBeVisible(); + await expect(page.getByRole('button', { name: /Mentés/ })).toBeVisible(); + await expect(page.getByRole('button', { name: /PDF/ })).toBeVisible(); +}); + +test('guest navigation shows Register and Login links', async ({ page }) => { + await mockGuestSession(page); + + await page.goto('/2e/karakter'); + + const registerLink = page.getByRole('link', { name: 'Register' }); + const loginLink = page.getByRole('link', { name: 'Login' }); + + await expect(registerLink).toBeVisible(); + await expect(registerLink).toHaveAttribute('href', '/Identity/Account/Register'); + await expect(loginLink).toBeVisible(); + await expect(loginLink).toHaveAttribute('href', '/Identity/Account/Login'); +}); + +test('public legal pages are served', async ({ page }) => { + await page.goto('/privacy.html'); + await expect(page.getByRole('heading', { name: 'Privacy Policy', level: 1 })).toBeVisible(); + + await page.goto('/ogl.html'); + await expect(page).toHaveTitle(/Open Game License v1\.0a/); + await expect(page.locator('body')).toContainText('OPEN GAME LICENSE Version 1.0a'); + + await page.goto('/aelf.html'); + await expect(page).toHaveTitle(/AELF Open License version 1\.0a/); + await expect(page.locator('body')).toContainText('AELF OPEN LICENSE VERSION 1.0a'); +}); diff --git a/e2e/yarn.lock b/e2e/yarn.lock new file mode 100644 index 0000000..e6d3b88 --- /dev/null +++ b/e2e/yarn.lock @@ -0,0 +1,29 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@playwright/test@1.59.1": + version "1.59.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.59.1.tgz#5c4d38eac84a61527af466602ae20277685a02d6" + integrity sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg== + dependencies: + playwright "1.59.1" + +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +playwright-core@1.59.1: + version "1.59.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.59.1.tgz#d8a2b28bcb8f2bd08ef3df93b02ae83c813244b2" + integrity sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg== + +playwright@1.59.1: + version "1.59.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.59.1.tgz#f7b0ca61637ae25264cec370df671bbe1f368a4a" + integrity sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw== + dependencies: + playwright-core "1.59.1" + optionalDependencies: + fsevents "2.3.2" diff --git a/frontend/Dockerfile b/frontend/Dockerfile index c901fa0..ac674b3 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -12,5 +12,7 @@ RUN yarn build FROM nginx:1.29.7-alpine3.23 AS proxy +LABEL org.opencontainers.image.source = "https://github.com/morbalint/kemkas" + COPY --from=builder "/src/dist" "/usr/share/nginx/html" COPY ./nginx.conf /etc/nginx/nginx.conf diff --git a/frontend/package.json b/frontend/package.json index 97d79e6..c3cc87c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -56,7 +56,11 @@ "lint": "eslint \"src/**/*.{ts,tsx}\" vite.config.mts vitest.setup.ts", "preview": "vite preview", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "test:e2e": "yarn --cwd ../e2e test", + "test:e2e:headed": "yarn --cwd ../e2e test:headed", + "test:e2e:ui": "yarn --cwd ../e2e test:ui", + "test:e2e:install": "yarn --cwd ../e2e install:browsers" }, "browserslist": { "production": [ diff --git a/nginx.e2e.conf b/nginx.e2e.conf new file mode 100644 index 0000000..8a69c61 --- /dev/null +++ b/nginx.e2e.conf @@ -0,0 +1,96 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + + upstream frontend { + server frontend:80; + } + + upstream backend { + server backend:8080; + } + + server { + listen 8000 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/certs/kemkas.pem; + ssl_certificate_key /etc/nginx/certs/kemkas.key; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / { + proxy_pass http://frontend; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + + location /Identity/ { + proxy_pass_request_headers on; + proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for"; + proxy_set_header X-Forwarded-Proto "$scheme"; + proxy_set_header X-Forwarded-Host "$http_host"; + proxy_pass http://backend; + } + + location /api/ { + proxy_pass_request_headers on; + proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for"; + proxy_set_header X-Forwarded-Proto "$scheme"; + proxy_set_header X-Forwarded-Host "$http_host"; + proxy_pass http://backend; + } + + location /signin- { + proxy_pass_request_headers on; + proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for"; + proxy_set_header X-Forwarded-Proto "$scheme"; + proxy_set_header X-Forwarded-Host "$http_host"; + proxy_pass http://backend; + } + + location /lib/ { + proxy_pass_request_headers on; + proxy_pass http://backend; + } + + location /js/ { + proxy_pass_request_headers on; + proxy_pass http://backend; + } + + location /css/ { + proxy_pass_request_headers on; + proxy_pass http://backend; + } + + location /img/ { + proxy_pass_request_headers on; + proxy_pass http://backend; + } + } +} diff --git a/scripts/generate-e2e-certs.sh b/scripts/generate-e2e-certs.sh new file mode 100644 index 0000000..ae147b8 --- /dev/null +++ b/scripts/generate-e2e-certs.sh @@ -0,0 +1,61 @@ +#!/bin/sh +set -eu + +CERT_DIR="${CERT_DIR:-/certs}" + +mkdir -p "$CERT_DIR" + +if [ -f "$CERT_DIR/rootCA.pem" ] && [ -f "$CERT_DIR/rootCA.key" ] && [ -f "$CERT_DIR/kemkas.pem" ] && [ -f "$CERT_DIR/kemkas.key" ]; then + echo "Using existing TLS material from $CERT_DIR" + exit 0 +fi + +apk add --no-cache openssl >/dev/null + +cat > /tmp/kemkas-server.cnf <<'EOF' +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = dn +req_extensions = req_ext + +[dn] +CN = localhost + +[req_ext] +subjectAltName = @alt_names +keyUsage = digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth + +[alt_names] +DNS.1 = localhost +DNS.2 = proxy +DNS.3 = host.docker.internal +IP.1 = 127.0.0.1 +IP.2 = ::1 +EOF + +cat > /tmp/kemkas-v3.ext <<'EOF' +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage=digitalSignature,keyEncipherment +extendedKeyUsage=serverAuth +subjectAltName=@alt_names + +[alt_names] +DNS.1=localhost +DNS.2=proxy +DNS.3=host.docker.internal +IP.1=127.0.0.1 +IP.2=::1 +EOF + +openssl genrsa -out "$CERT_DIR/rootCA.key" 2048 +openssl req -x509 -new -nodes -key "$CERT_DIR/rootCA.key" -sha256 -days 3650 -subj "/CN=kemkas-local-root" -out "$CERT_DIR/rootCA.pem" + +openssl genrsa -out "$CERT_DIR/kemkas.key" 2048 +openssl req -new -key "$CERT_DIR/kemkas.key" -out /tmp/kemkas.csr -config /tmp/kemkas-server.cnf +openssl x509 -req -in /tmp/kemkas.csr -CA "$CERT_DIR/rootCA.pem" -CAkey "$CERT_DIR/rootCA.key" -CAcreateserial -out "$CERT_DIR/kemkas.pem" -days 825 -sha256 -extfile /tmp/kemkas-v3.ext + +echo "Generated TLS CA and nginx certificate under $CERT_DIR"