From c40e3d20cb6d762d1bab496bfa6ded9da9199eca Mon Sep 17 00:00:00 2001 From: ramvignesh-b Date: Fri, 17 Apr 2026 01:22:03 +0530 Subject: [PATCH] ci: add sll support and enhance e2e workflow --- .env.e2e.example | 40 ++-- .env.example | 17 +- .github/workflows/ci.yml | 15 -- .gitignore | 1 + backend/config/settings.py | 24 +-- backend/scripts/__init__.py | 0 .../scripts/management/commands/__init__.py | 0 backend/scripts/management/commands/serve.py | 30 +++ backend/users/tests.py | 5 +- biome.json | 2 +- docker-compose.e2e.yml | 19 ++ frontend/e2e/utils/auth.ts | 1 + frontend/e2e/utils/mailpit.ts | 3 +- frontend/playwright.config.ts | 20 +- frontend/src/api/apiClient.test.ts | 20 +- frontend/src/hooks/useAuth.test.ts | 10 +- frontend/utils/url-builder.ts | 9 + frontend/vite.config.ts | 31 ++- logs/tmp/piku_e2e_backend.log | 180 ++++++++++++++++++ scripts/run-e2e.sh | 101 +++------- scripts/start.sh | 2 +- 21 files changed, 368 insertions(+), 162 deletions(-) create mode 100644 backend/scripts/__init__.py create mode 100644 backend/scripts/management/commands/__init__.py create mode 100644 backend/scripts/management/commands/serve.py create mode 100644 docker-compose.e2e.yml create mode 100644 frontend/utils/url-builder.ts create mode 100644 logs/tmp/piku_e2e_backend.log diff --git a/.env.e2e.example b/.env.e2e.example index f854d31..9c1f97b 100644 --- a/.env.e2e.example +++ b/.env.e2e.example @@ -1,25 +1,27 @@ -# Pi Ku E2E Environment Configuration Template +# DATABASE +DB_NAME=piku_test_db +DB_USER=test +DB_PASSWORD=password123 +DB_HOST=localhost +DB_PORT=5433 -# Database (Postgres) -E2E_DB_NAME=piku_e2e_db -E2E_DB_PORT=5433 -E2E_DB_USER=piku_test -E2E_DB_PASS=piku_test -E2E_DB_DB=piku_e2e +# SSL +SSL_ENABLED=false -# Backend (Django) -E2E_BACKEND_PORT=8001 -SECRET_KEY=e2e-secret-key-for-piku-testing +# DJANGO DEBUG=True -ALLOWED_HOSTS=* -CORS_ALLOWED_ORIGINS=https://localhost:5173 -FRONTEND_URL=https://localhost:5173 -EMAIL_HOST=localhost -EMAIL_PORT=1025 +SECRET_KEY=django-insecure-initial-key +BACKEND_DOMAIN=127.0.0.1 +BACKEND_PORT=8001 + +# EMAIL +EMAIL_HOST=127.0.0.1 +EMAIL_PORT=1026 +FROM_EMAIL="Test " EMAIL_HOST_USER= EMAIL_HOST_PASSWORD= -FROM_EMAIL=testing@piku.local -MAILPIT_API_URL=http://localhost:8025/api/v1 +EMAIL_API_PORT=8026 -# Frontend (Vite/Playwright) -VITE_API_URL=https://localhost:8001 +# FRONTEND +FRONTEND_PORT=5199 +FRONTEND_DOMAIN=127.0.0.1 diff --git a/.env.example b/.env.example index 2cdd922..5554422 100644 --- a/.env.example +++ b/.env.example @@ -5,21 +5,20 @@ DB_PASSWORD=password123 DB_HOST=localhost DB_PORT=5432 +# SSL +SSL_ENABLED=true + # DJANGO DEBUG=True SECRET_KEY=django-secret-key -ALLOWED_HOSTS=localhost,127.0.0.1 -CORS_ALLOWED_ORIGINS=https://localhost:5173,https://127.0.0.1:5173 +BACKEND_DOMAIN=127.0.0.1 +BACKEND_PORT=8000 # EMAIL -EMAIL_HOST=localhost +EMAIL_HOST=127.0.0.1 EMAIL_PORT=1025 -FROM_EMAIL=Pi Ku -EMAIL_HOST_USER=root -EMAIL_HOST_PASSWORD=password123 +FROM_EMAIL=Pi Ku # FRONTEND -VITE_API_URL=https://localhost:8000 FRONTEND_PORT=5173 -FRONTEND_URL=https://localhost:5173 -FRONTEND_DOMAIN=localhost +FRONTEND_DOMAIN=127.0.0.1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf57381..a3804d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,21 +101,6 @@ jobs: name: E2E Tests runs-on: ubuntu-latest needs: setup-environment - services: - postgres: - image: postgres:16-alpine - env: - POSTGRES_DB: piku_e2e - POSTGRES_USER: piku_test - POSTGRES_PASSWORD: piku_test - ports: - - 5433:5432 - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - mailpit: - image: axllent/mailpit:latest - ports: - - 8025:8025 - - 1025:1025 steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index f15cb11..97471a6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ dist/ # Certificates certs/*.pem +tmp/ diff --git a/backend/config/settings.py b/backend/config/settings.py index d5beede..bf70160 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -19,12 +19,17 @@ import environ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -# Load environment variables +# Load dotenv files env = environ.Env() -# Allow overriding the .env file path (useful for E2E testing/CI) -env_file = os.environ.get("PIKU_ENV_FILE", os.path.join(BASE_DIR.parent, ".env")) +env_file = os.path.join(BASE_DIR.parent, ".env") if os.path.exists(env_file): - environ.Env.read_env(env_file) + environ.Env.read_env(env_file, overwrite=False) + + +SSL_ENABLED = env("SSL_ENABLED") == "true" +FRONTEND_URL = f"https://{env('FRONTEND_DOMAIN')}" if SSL_ENABLED else f"http://{env('FRONTEND_DOMAIN')}" +if env("FRONTEND_PORT"): + FRONTEND_URL += f":{env('FRONTEND_PORT')}" # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ @@ -35,7 +40,7 @@ SECRET_KEY = env("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env("DEBUG") -ALLOWED_HOSTS = env.list("ALLOWED_HOSTS") or [] +ALLOWED_HOSTS = [env("FRONTEND_DOMAIN")] # Application definition @@ -50,6 +55,7 @@ INSTALLED_APPS = [ "corsheaders", "users", "letters", + "scripts", ] MIDDLEWARE = [ @@ -82,7 +88,7 @@ DATABASES = { } } -CORS_ALLOWED_ORIGINS = env.list("CORS_ALLOWED_ORIGINS") +CORS_ALLOWED_ORIGINS = [FRONTEND_URL] CORS_ALLOW_CREDENTIALS = True AUTH_USER_MODEL = "users.User" @@ -107,7 +113,7 @@ NOTE: COOKIE_SAMESITE: Lax is used to allow cross-site redirection, like links AUTH_COOKIE = { "NAME": "refresh_token", "DOMAIN": None, - "SECURE": True, + "SECURE": SSL_ENABLED, "HTTPONLY": True, "SAMESITE": "Lax", } @@ -117,12 +123,8 @@ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_HOST = env("EMAIL_HOST") EMAIL_PORT = env("EMAIL_PORT") EMAIL_USE_TLS = not DEBUG -EMAIL_HOST_USER = env("EMAIL_HOST_USER") -EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") FROM_EMAIL = env("FROM_EMAIL") -FRONTEND_URL = env("FRONTEND_URL") - # Password validation # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators diff --git a/backend/scripts/__init__.py b/backend/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/scripts/management/commands/__init__.py b/backend/scripts/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/scripts/management/commands/serve.py b/backend/scripts/management/commands/serve.py new file mode 100644 index 0000000..9da8067 --- /dev/null +++ b/backend/scripts/management/commands/serve.py @@ -0,0 +1,30 @@ +import os + +from django.conf import settings +from django.core.management import call_command +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + def handle(self, *args, **options): + """ + Check if SSL is enabled in the environment variables. + If SSL is enabled, use runserver_plus command. + If SSL is not enabled, use runserver command. + """ + ssl_enabled = os.getenv("SSL_ENABLED", "false").lower() == "true" + domain = os.getenv("BACKEND_DOMAIN", "127.0.0.1") + port = os.getenv("BACKEND_PORT", "8000") + addrport = f"{domain}:{port}" + + if ssl_enabled: + self.stdout.write(self.style.SUCCESS(f"Starting with SSL on {addrport}...")) + call_command( + "runserver_plus", + addrport, + cert_file=settings.BASE_DIR / "../certs/localhost.pem", + key_file=settings.BASE_DIR / "../certs/localhost-key.pem", + ) + else: + self.stdout.write(self.style.WARNING(f"Starting without SSL on {addrport}...")) + call_command("runserver", addrport) diff --git a/backend/users/tests.py b/backend/users/tests.py index 879f86a..cff262b 100644 --- a/backend/users/tests.py +++ b/backend/users/tests.py @@ -1,3 +1,5 @@ +from unittest.mock import _patch_dict + from django.contrib.auth import get_user_model from django.contrib.auth.tokens import default_token_generator from django.urls import reverse @@ -19,9 +21,10 @@ class AuthTests(APITestCase): self.refresh_url = reverse("token_refresh") self.logout_url = reverse("logout") + @_patch_dict("config.settings.AUTH_COOKIE", {"SECURE": True}) def test_login_sets_secure_cookie(self): """ - Tests if the Login API can generate access token and set secure cookie for refresh token. + Tests if the Login API can generate access token and set secure cookie (when ssl is enabled) for refresh token. """ data = {"email": self.user.email, "password": self.password} cookie_name = "refresh_token" diff --git a/biome.json b/biome.json index 1dca93a..07c7ff8 100644 --- a/biome.json +++ b/biome.json @@ -42,7 +42,7 @@ "noUnusedVariables": "error" } }, - "includes": ["**", "!backend"] + "includes": ["**/src", "!backend"] }, "assist": { "actions": { diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml new file mode 100644 index 0000000..7f045ff --- /dev/null +++ b/docker-compose.e2e.yml @@ -0,0 +1,19 @@ +services: + db: + image: postgres:16-alpine + container_name: ${DB_NAME} + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + ports: + - "${DB_PORT}:5432" + restart: never + + mailpit: + image: axllent/mailpit + container_name: piku_test_mail + ports: + - "${EMAIL_API_PORT}:8025" + - "${EMAIL_PORT}:1025" + restart: never diff --git a/frontend/e2e/utils/auth.ts b/frontend/e2e/utils/auth.ts index f056f4d..9210011 100644 --- a/frontend/e2e/utils/auth.ts +++ b/frontend/e2e/utils/auth.ts @@ -34,6 +34,7 @@ export async function registerAndLogin( // 2. Activation via Mailpit logger.info(`[Auth] Polling Mailpit for activation email...`); const activationLink = await MailpitHelper.getActivationLink(email); + await page.goto(activationLink); await expect(page.getByText(/account activated/i)).toBeVisible(); diff --git a/frontend/e2e/utils/mailpit.ts b/frontend/e2e/utils/mailpit.ts index f16df52..9f78b35 100644 --- a/frontend/e2e/utils/mailpit.ts +++ b/frontend/e2e/utils/mailpit.ts @@ -7,7 +7,7 @@ export interface MailpitMessage { To: { Address: string }[]; } -const MAILPIT_API_URL = process.env.MAILPIT_API_URL; +const MAILPIT_API_URL = `http://${process.env.EMAIL_HOST}:${process.env.EMAIL_API_PORT}/api/v1`; export const MailpitHelper = { getActivationLink: async ( @@ -18,7 +18,6 @@ export const MailpitHelper = { const requestContext = await request.newContext(); while (Date.now() - startTime < timeout) { - // Search specifically for the recipient to reduce data transfer const response = await requestContext.get(`${MAILPIT_API_URL}/search`, { params: { query: `to:${email}`, limit: 1 }, }); diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 65917cd..95c106c 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,12 +1,20 @@ import path from "node:path"; -import process from "node:process"; +import process, { env } from "node:process"; import { defineConfig, devices } from "@playwright/test"; import dotenv from "dotenv"; +import { getBaseUrl } from "./utils/url-builder"; /** * Read environment variables from file. */ dotenv.config({ path: path.resolve(process.cwd(), "../.env.e2e") }); +const baseUrl = getBaseUrl( + env.SSL_ENABLED === "true", + env.FRONTEND_DOMAIN, + env.FRONTEND_PORT, +); + +console.log(baseUrl); export default defineConfig({ timeout: 60000, expect: { @@ -26,7 +34,7 @@ export default defineConfig({ /* 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_URL, + baseURL: baseUrl, /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ actionTimeout: 20000, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ @@ -53,8 +61,12 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: "bun run dev", - url: process.env.FRONTEND_URL, + command: "bun run dev -- --mode e2e", + url: getBaseUrl( + process.env.SSL_ENABLED === "true", + process.env.FRONTEND_DOMAIN, + process.env.FRONTEND_PORT, + ), reuseExistingServer: !process.env.CI, ignoreHTTPSErrors: true, }, diff --git a/frontend/src/api/apiClient.test.ts b/frontend/src/api/apiClient.test.ts index 44a439f..074bc8e 100644 --- a/frontend/src/api/apiClient.test.ts +++ b/frontend/src/api/apiClient.test.ts @@ -13,7 +13,7 @@ import { server } from "../../test/mocks/server"; import { useAuthStore } from "../store/useAuthStore"; import { api } from "./apiClient"; -const API_URL = "http://piku-server"; +const VITE_API_URL = "http://piku-server"; beforeEach(() => { useAuthStore.setState({ @@ -24,7 +24,7 @@ beforeEach(() => { }); beforeAll(() => { - vi.stubEnv("API_URL", API_URL); + vi.stubEnv("VITE_API_URL", VITE_API_URL); }); afterAll(() => { @@ -37,7 +37,7 @@ describe("request interceptor", () => { let capturedAuthHeader = ""; server.use( - http.get(`${API_URL}/api/auth/me/`, ({ request }) => { + http.get(`${VITE_API_URL}/api/auth/me/`, ({ request }) => { capturedAuthHeader = request.headers.get("Authorization") ?? ""; return HttpResponse.json(mockUser); }), @@ -51,7 +51,7 @@ describe("request interceptor", () => { it("should not send Authorization header when the store has no token", async () => { let capturedAuthHeader: string | null = ""; server.use( - http.get(`${API_URL}/api/auth/me/`, ({ request }) => { + http.get(`${VITE_API_URL}/api/auth/me/`, ({ request }) => { capturedAuthHeader = request.headers.get("Authorization"); return HttpResponse.json({}); }), @@ -70,14 +70,14 @@ describe("response interceptor", () => { let _refreshApiCallCount = 0; server.use( - http.get(`${API_URL}/api/auth/me/`, ({ request }) => { + http.get(`${VITE_API_URL}/api/auth/me/`, ({ request }) => { meApiCallCount++; if (request.headers.get("Authorization") === "Bearer expired-token") { return new HttpResponse(null, { status: 401 }); } return HttpResponse.json(mockUser); }), - http.post(`${API_URL}/api/auth/refresh/`, () => { + http.post(`${VITE_API_URL}/api/auth/refresh/`, () => { _refreshApiCallCount++; return HttpResponse.json({ access: "refreshed-token" }); }), @@ -94,13 +94,13 @@ describe("response interceptor", () => { useAuthStore.getState().setAuth("expired-token", mockUser); server.use( - http.get(`${API_URL}/api/auth/me/`, ({ request }) => { + http.get(`${VITE_API_URL}/api/auth/me/`, ({ request }) => { if (request.headers.get("Authorization") === "Bearer expired-token") { return new HttpResponse(null, { status: 401 }); } return HttpResponse.json(mockUser); }), - http.post(`${API_URL}/api/auth/refresh/`, () => + http.post(`${VITE_API_URL}/api/auth/refresh/`, () => HttpResponse.json({ access: "refreshed-token" }), ), ); @@ -115,14 +115,14 @@ describe("response interceptor", () => { server.use( http.get( - `${API_URL}/api/auth/me/`, + `${VITE_API_URL}/api/auth/me/`, () => new HttpResponse(JSON.stringify({ detail: "Invalid token" }), { status: 401, }), ), http.post( - `${API_URL}/api/auth/refresh/`, + `${VITE_API_URL}/api/auth/refresh/`, () => new HttpResponse(JSON.stringify({ detail: "Refresh failed" }), { status: 401, diff --git a/frontend/src/hooks/useAuth.test.ts b/frontend/src/hooks/useAuth.test.ts index 4d3d481..b397d3b 100644 --- a/frontend/src/hooks/useAuth.test.ts +++ b/frontend/src/hooks/useAuth.test.ts @@ -17,7 +17,7 @@ import { useAuth } from "./useAuth"; vi.mock("../utils/crypto"); vi.mock("../utils/keystore"); -const API_URL = "http://piku-server"; +const VITE_API_URL = "http://piku-server"; beforeEach(() => { vi.clearAllMocks(); @@ -112,7 +112,7 @@ describe("logout", () => { it("should call the logout API endpoint", async () => { let logoutCalled = false; server.use( - http.post(`${API_URL}/api/auth/logout/`, () => { + http.post(`${VITE_API_URL}/api/auth/logout/`, () => { logoutCalled = true; return HttpResponse.json({}); }), @@ -139,7 +139,7 @@ describe("logout", () => { it("should clear the auth store even if the API call fails", async () => { server.use( http.post( - `${API_URL}/api/auth/logout/`, + `${VITE_API_URL}/api/auth/logout/`, () => new HttpResponse(null, { status: 500 }), ), ); @@ -163,7 +163,7 @@ describe("initialize", () => { }); let refreshCalled = false; server.use( - http.post(`${API_URL}/api/auth/refresh/`, () => { + http.post(`${VITE_API_URL}/api/auth/refresh/`, () => { refreshCalled = true; return HttpResponse.json({ access: "new-token" }); }), @@ -194,7 +194,7 @@ describe("initialize", () => { it("should preserve the master key even if the refresh attempt fails", async () => { server.use( http.post( - `${API_URL}/api/auth/refresh/`, + `${VITE_API_URL}/api/auth/refresh/`, () => new HttpResponse(null, { status: 401 }), ), ); diff --git a/frontend/utils/url-builder.ts b/frontend/utils/url-builder.ts new file mode 100644 index 0000000..1ba3c3f --- /dev/null +++ b/frontend/utils/url-builder.ts @@ -0,0 +1,9 @@ +export const getBaseUrl = ( + isSslEnabled: boolean, + domain: string | undefined, + port: string | undefined, +): string => { + const uriScheme = isSslEnabled ? "https" : "http"; + const baseURL = `${uriScheme}://${domain}${port ? `:${port}` : ""}`; + return baseURL; +}; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index c422b19..421555a 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,24 +3,39 @@ import path from "node:path"; import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; import { defineConfig, loadEnv } from "vite"; +import { getBaseUrl } from "./utils/url-builder"; // https://vite.dev/config/ export default defineConfig(({ mode }) => { const env = loadEnv(mode, "../", ""); + const isSslEnabled = env.SSL_ENABLED === "true"; + let ssl_certs: { key: Buffer; cert: Buffer }; + + if (isSslEnabled) { + ssl_certs = { + key: fs.readFileSync( + path.resolve(__dirname, "../certs/localhost-key.pem"), + ), + cert: fs.readFileSync(path.resolve(__dirname, "../certs/localhost.pem")), + }; + } + + const baseApiUrl = getBaseUrl( + isSslEnabled, + env.BACKEND_DOMAIN, + env.BACKEND_PORT, + ); + console.log(baseApiUrl); return { envDir: "../", plugins: [react(), tailwindcss()], + define: { + "import.meta.env.VITE_API_URL": JSON.stringify(baseApiUrl), + }, server: { port: Number(env.FRONTEND_PORT), host: env.FRONTEND_DOMAIN, - https: { - key: fs.readFileSync( - path.resolve(__dirname, "../certs/localhost-key.pem"), - ), - cert: fs.readFileSync( - path.resolve(__dirname, "../certs/localhost.pem"), - ), - }, + https: isSslEnabled ? ssl_certs : undefined, }, }; }); diff --git a/logs/tmp/piku_e2e_backend.log b/logs/tmp/piku_e2e_backend.log new file mode 100644 index 0000000..de8546e --- /dev/null +++ b/logs/tmp/piku_e2e_backend.log @@ -0,0 +1,180 @@ +WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on https://127.0.0.1:8001 +Press CTRL+C to quit + * Restarting with stat + * Debugger is active! + * Debugger PIN: 411-535-418 +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:15] "POST /api/auth/refresh/ HTTP/1.1" 401 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:15] "POST /api/auth/refresh/ HTTP/1.1" 401 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:15] "POST /api/auth/refresh/ HTTP/1.1" 401 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:16] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:16] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:16] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:16] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:16] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:17] "POST /api/auth/register/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:17] "POST /api/auth/register/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:17] "POST /api/auth/register/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:17] "POST /api/auth/register/ HTTP/1.1" 201 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:19] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:19] "GET /api/auth/activate/YmFlZTYyNTgtOTcxMi00ZjFmLWE1YTgtYzBiOGMwODdkY2Zi/d755fh-d81b8ac647be32c14f996ab09e783392/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:20] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:20] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:20] "GET /api/auth/activate/YzJjM2NkOWUtZmIxYS00MGI2LWFiM2EtYTQwYmI5MDJjZGY2/d755fh-a2be30e77657af68697d0b7be375e5ab/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:20] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:20] "GET /api/auth/activate/Yzc1MDFhMTctY2EzOC00YTY0LTkyNmYtNWYyZjgzNDUyZGQ1/d755fh-677d8f6662cce0e74bb9a776663617a9/ HTTP/1.1" 200 - +/var/home/atom/Documents/code/pi ku/backend/.venv/lib64/python3.14/site-packages/jwt/api_jwt.py:147: InsecureKeyLengthWarning: The HMAC key is 27 bytes long, which is below the minimum recommended length of 32 bytes for SHA256. See RFC 7518 Section 3.2. + return self._jws.encode( +127.0.0.1 - - [16/Apr/2026 18:45:20] "POST /api/auth/login/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:20] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:20] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +/var/home/atom/Documents/code/pi ku/backend/.venv/lib64/python3.14/site-packages/jwt/api_jwt.py:365: InsecureKeyLengthWarning: The HMAC key is 27 bytes long, which is below the minimum recommended length of 32 bytes for SHA256. See RFC 7518 Section 3.2. + decoded = self.decode_complete( +127.0.0.1 - - [16/Apr/2026 18:45:21] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "GET /api/auth/activate/YjI4MjczYWMtYWNlNC00OGRlLWJmZDItZmMyMTJiZjM2MDZl/d755fh-7728ee068008d7513a64dbe6282954f3/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "PUT /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "OPTIONS /api/letters/e8f47036-6e57-41f5-b057-8ea712589a73/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "PUT /api/letters/e8f47036-6e57-41f5-b057-8ea712589a73/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:24] "GET /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:24] "GET /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:24] "OPTIONS /api/letters/4e2af91a-1651-4bcd-85b5-c469ee4a73e3/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:24] "PUT /api/letters/4e2af91a-1651-4bcd-85b5-c469ee4a73e3/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "POST /api/auth/refresh/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "POST /api/auth/refresh/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/letters/4e2af91a-1651-4bcd-85b5-c469ee4a73e3/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/letters/4e2af91a-1651-4bcd-85b5-c469ee4a73e3/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:30] "POST /api/auth/refresh/ HTTP/1.1" 401 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:30] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:31] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:32] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:32] "POST /api/auth/register/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:32] "POST /api/auth/register/ HTTP/1.1" 201 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:34] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:35] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:35] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:36] "POST /api/auth/register/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:36] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:36] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:36] "GET /api/auth/activate/M2M0YzFiNTItMjE5Mi00Y2VmLWIwZmItMDlkNDg5NWE4NWU0/d755fw-9c2c43d45732c1cd5ccbef20d3dc4181/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:37] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:37] "POST /api/auth/register/ HTTP/1.1" 201 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:37] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:37] "GET /api/auth/activate/MjdiM2E1M2ItOTZlYS00Y2Y1LWFhMmQtZThjY2ZkNGQ5ZjQ1/d755fw-e60e3d0b66162c75962beea97a40b4c7/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:40] "OPTIONS /api/letters/351f0e2c-e10f-4851-9836-bc387758dbad/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:40] "PUT /api/letters/351f0e2c-e10f-4851-9836-bc387758dbad/ HTTP/1.1" 201 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:40] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:40] "GET /api/auth/activate/MDkwNTg5MDctNjAzOS00NDgwLTlkYTktNmUxOWE5ZTBjODJh/d755g0-546e5f4ee3ed111de64f011ac8d02836/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:41] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:41] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:41] "GET /api/auth/activate/MzAxODRjMWEtMTYxZC00ZTczLThlMWMtM2FmY2RmZDg2NThk/d755g1-2ae1dec8cbac57d223c0cd206a8741a8/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "OPTIONS /api/letters/5dec1a15-1d19-47de-b5a5-21bc1845f90a/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "PUT /api/letters/5dec1a15-1d19-47de-b5a5-21bc1845f90a/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "POST /api/auth/refresh/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "GET /api/letters/351f0e2c-e10f-4851-9836-bc387758dbad/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "GET /api/letters/351f0e2c-e10f-4851-9836-bc387758dbad/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "POST /api/auth/refresh/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "POST /api/auth/refresh/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "GET /api/letters/351f0e2c-e10f-4851-9836-bc387758dbad/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "GET /api/letters/351f0e2c-e10f-4851-9836-bc387758dbad/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:47] "OPTIONS /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:47] "PUT /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:47] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:47] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:49] "GET /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:49] "GET /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:51] "POST /api/auth/refresh/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:51] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:51] "GET /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:51] "GET /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:51] "GET /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:51] "GET /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +Starting with SSL on 127.0.0.1:8001... +/usr/lib64/python3.14/multiprocessing/resource_tracker.py:396: UserWarning: resource_tracker: There appear to be 1 leaked semaphore objects to clean up at shutdown: {'/mp-uf_ylwyc'} + warnings.warn( diff --git a/scripts/run-e2e.sh b/scripts/run-e2e.sh index ae5dd69..d68823e 100755 --- a/scripts/run-e2e.sh +++ b/scripts/run-e2e.sh @@ -1,98 +1,47 @@ #!/bin/bash set -e -# Get absolute path of project root -PROJECT_ROOT=$(pwd) +# Use podman if available. Not everyone has it +CONTAINER_BIN=$(command -v podman || command -v docker) -# Configuration -ENV_FILE="$PROJECT_ROOT/.env.e2e" +ENV_FILE="./.env.e2e" if [ -f "$ENV_FILE" ]; then - echo "[INFO] Loading configuration from $ENV_FILE..." + echo "Loading settings..." set -a source "$ENV_FILE" set +a -elif [ "$CI" != "true" ]; then - echo "[ERROR] $ENV_FILE not found! Please create it for local testing (use .env.e2e.example as template)." - exit 1 else - echo "[INFO] Running in CI mode (using direct environment variables)..." + echo "Error: Configuration file $ENV_FILE is missing!!" + exit 1 fi -# Map E2E variables to Django expected names -# In CI, these should be set via GitHub Actions env variables -export DB_NAME=${E2E_DB_DB:-piku_e2e} -export DB_USER=${E2E_DB_USER:-piku_test} -export DB_PASSWORD=${E2E_DB_PASS:-piku_test} -export DB_HOST=${E2E_DB_HOST:-localhost} -export DB_PORT=${E2E_DB_PORT:-5433} -export E2E_BACKEND_PORT=${E2E_BACKEND_PORT:-8001} - -echo "[START] Initializing E2E Test Environment..." - -# 1. Cleanup / Start Services (Skip in CI) -if [ "$CI" != "true" ]; then - if podman ps -a --format "{{.Names}}" | grep -q "^$E2E_DB_NAME$"; then - echo "[CLEANUP] Removing existing container $E2E_DB_NAME..." - podman rm -f $E2E_DB_NAME - fi - - echo "[DB] Starting disposable Postgres on port $DB_PORT..." - podman run --name $E2E_DB_NAME \ - -e POSTGRES_DB=$DB_NAME \ - -e POSTGRES_USER=$DB_USER \ - -e POSTGRES_PASSWORD=$DB_PASSWORD \ - -p $DB_PORT:5432 \ - -d docker.io/library/postgres:16-alpine > /dev/null - - echo "[DB] Waiting for Postgres to be ready..." - until podman exec $E2E_DB_NAME pg_isready -U $DB_USER > /dev/null 2>&1; do - sleep 1 - done - echo "[DB] Postgres is ready." -fi - -# Trap to ensure cleanup +# This cleans up containers. Very useful for local e2e to free system resources immediately. cleanup() { - echo "[CLEANUP] Stopping services..." - if [ "$CI" != "true" ]; then - podman rm -f $E2E_DB_NAME || true - fi - if [ ! -z "$BACKEND_PID" ]; then - kill "$BACKEND_PID" 2>/dev/null || true - fi + echo "Cleaning up..." + $CONTAINER_BIN rm -f "$DB_NAME" 2>/dev/null || true + [ -n "$BACKEND_PID" ] && kill "$BACKEND_PID" 2>/dev/null || true } trap cleanup EXIT -# 2. Prepare Backend -echo "[BACKEND] Running database migrations..." -export PIKU_ENV_FILE="$ENV_FILE" -(cd backend && uv run manage.py migrate --noinput) +echo "Starting Database..." +$CONTAINER_BIN compose -f "./docker-compose.e2e.yml" up -d -echo "[BACKEND] Starting server on port $E2E_BACKEND_PORT..." -(cd backend && uv run manage.py runserver_plus --cert-file ../certs/localhost.pem --key-file ../certs/localhost-key.pem $E2E_BACKEND_PORT) > /tmp/piku_e2e_backend.log 2>&1 & +# postgress will take some time, so we wait, and no race condition. Also, no point in logging this output +until $CONTAINER_BIN exec "$DB_NAME" pg_isready -U "${DB_USER:-test}" >/dev/null 2>&1; do + echo "Waiting for DB..." + sleep 2 +done + +echo "Starting Backend..." +mkdir -p ./tmp/logs +(cd backend && uv run manage.py migrate) +(cd backend && uv run manage.py serve) > ./tmp/logs/backend.log 2>&1 & BACKEND_PID=$! -echo "[BACKEND] Waiting for server to respond..." -until curl -sk https://localhost:$E2E_BACKEND_PORT > /dev/null; do - sleep 1 - if ! kill -0 $BACKEND_PID 2>/dev/null; then - echo "[ERROR] Backend failed to start. Logs:" - cat /tmp/piku_e2e_backend.log - exit 1 - fi -done -echo "[BACKEND] Server is ready." - -# 3. Run Playwright -export VITE_API_URL="https://localhost:$E2E_BACKEND_PORT" - if [ "$CI" = "true" ]; then - echo "[TEST] Running Playwright Tests (CI)..." - (cd frontend && bun run test:e2e "$@") + cd frontend && bun run test:e2e "$@" else - echo "[TEST] Running Playwright Tests in Distrobox..." - (cd frontend && distrobox-enter --name ubuntu-24.04 -- env VITE_API_URL=$VITE_API_URL bun run test:e2e "$@") + # Because playwright decided not to support Fedora :) + cd frontend && distrobox-enter --name ubuntu-24.04 -- bun run test:e2e "$@" fi - -echo "[SUCCESS] E2E Tests Completed." diff --git a/scripts/start.sh b/scripts/start.sh index 1cb9678..bcdb0ec 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -1,4 +1,4 @@ #!/bin/bash (podman compose up -d) & -(cd backend && uv run manage.py runserver) & +(cd backend && uv run manage.py serve) & (cd frontend && bun run dev)