ci: add sll support and enhance e2e workflow

This commit is contained in:
ramvignesh-b
2026-04-17 01:22:03 +05:30
parent f5757b47de
commit c40e3d20cb
21 changed files with 368 additions and 162 deletions
+21 -19
View File
@@ -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) # SSL
E2E_DB_NAME=piku_e2e_db SSL_ENABLED=false
E2E_DB_PORT=5433
E2E_DB_USER=piku_test
E2E_DB_PASS=piku_test
E2E_DB_DB=piku_e2e
# Backend (Django) # DJANGO
E2E_BACKEND_PORT=8001
SECRET_KEY=e2e-secret-key-for-piku-testing
DEBUG=True DEBUG=True
ALLOWED_HOSTS=* SECRET_KEY=django-insecure-initial-key
CORS_ALLOWED_ORIGINS=https://localhost:5173 BACKEND_DOMAIN=127.0.0.1
FRONTEND_URL=https://localhost:5173 BACKEND_PORT=8001
EMAIL_HOST=localhost
EMAIL_PORT=1025 # EMAIL
EMAIL_HOST=127.0.0.1
EMAIL_PORT=1026
FROM_EMAIL="Test <test@pi-ku.app>"
EMAIL_HOST_USER= EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD= EMAIL_HOST_PASSWORD=
FROM_EMAIL=testing@piku.local EMAIL_API_PORT=8026
MAILPIT_API_URL=http://localhost:8025/api/v1
# Frontend (Vite/Playwright) # FRONTEND
VITE_API_URL=https://localhost:8001 FRONTEND_PORT=5199
FRONTEND_DOMAIN=127.0.0.1
+8 -9
View File
@@ -5,21 +5,20 @@ DB_PASSWORD=password123
DB_HOST=localhost DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
# SSL
SSL_ENABLED=true
# DJANGO # DJANGO
DEBUG=True DEBUG=True
SECRET_KEY=django-secret-key SECRET_KEY=django-secret-key
ALLOWED_HOSTS=localhost,127.0.0.1 BACKEND_DOMAIN=127.0.0.1
CORS_ALLOWED_ORIGINS=https://localhost:5173,https://127.0.0.1:5173 BACKEND_PORT=8000
# EMAIL # EMAIL
EMAIL_HOST=localhost EMAIL_HOST=127.0.0.1
EMAIL_PORT=1025 EMAIL_PORT=1025
FROM_EMAIL=Pi Ku <no-reply@piku.app> FROM_EMAIL=Pi Ku <no-reply@test.com>
EMAIL_HOST_USER=root
EMAIL_HOST_PASSWORD=password123
# FRONTEND # FRONTEND
VITE_API_URL=https://localhost:8000
FRONTEND_PORT=5173 FRONTEND_PORT=5173
FRONTEND_URL=https://localhost:5173 FRONTEND_DOMAIN=127.0.0.1
FRONTEND_DOMAIN=localhost
-15
View File
@@ -101,21 +101,6 @@ jobs:
name: E2E Tests name: E2E Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: setup-environment 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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
+1
View File
@@ -12,3 +12,4 @@ dist/
# Certificates # Certificates
certs/*.pem certs/*.pem
tmp/
+13 -11
View File
@@ -19,12 +19,17 @@ import environ
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# Load environment variables # Load dotenv files
env = environ.Env() env = environ.Env()
# Allow overriding the .env file path (useful for E2E testing/CI) env_file = os.path.join(BASE_DIR.parent, ".env")
env_file = os.environ.get("PIKU_ENV_FILE", os.path.join(BASE_DIR.parent, ".env"))
if os.path.exists(env_file): 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 # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ # 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env("DEBUG") DEBUG = env("DEBUG")
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS") or [] ALLOWED_HOSTS = [env("FRONTEND_DOMAIN")]
# Application definition # Application definition
@@ -50,6 +55,7 @@ INSTALLED_APPS = [
"corsheaders", "corsheaders",
"users", "users",
"letters", "letters",
"scripts",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -82,7 +88,7 @@ DATABASES = {
} }
} }
CORS_ALLOWED_ORIGINS = env.list("CORS_ALLOWED_ORIGINS") CORS_ALLOWED_ORIGINS = [FRONTEND_URL]
CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_CREDENTIALS = True
AUTH_USER_MODEL = "users.User" AUTH_USER_MODEL = "users.User"
@@ -107,7 +113,7 @@ NOTE: COOKIE_SAMESITE: Lax is used to allow cross-site redirection, like links
AUTH_COOKIE = { AUTH_COOKIE = {
"NAME": "refresh_token", "NAME": "refresh_token",
"DOMAIN": None, "DOMAIN": None,
"SECURE": True, "SECURE": SSL_ENABLED,
"HTTPONLY": True, "HTTPONLY": True,
"SAMESITE": "Lax", "SAMESITE": "Lax",
} }
@@ -117,12 +123,8 @@ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = env("EMAIL_HOST") EMAIL_HOST = env("EMAIL_HOST")
EMAIL_PORT = env("EMAIL_PORT") EMAIL_PORT = env("EMAIL_PORT")
EMAIL_USE_TLS = not DEBUG EMAIL_USE_TLS = not DEBUG
EMAIL_HOST_USER = env("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
FROM_EMAIL = env("FROM_EMAIL") FROM_EMAIL = env("FROM_EMAIL")
FRONTEND_URL = env("FRONTEND_URL")
# Password validation # Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
View File
@@ -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)
+4 -1
View File
@@ -1,3 +1,5 @@
from unittest.mock import _patch_dict
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.tokens import default_token_generator
from django.urls import reverse from django.urls import reverse
@@ -19,9 +21,10 @@ class AuthTests(APITestCase):
self.refresh_url = reverse("token_refresh") self.refresh_url = reverse("token_refresh")
self.logout_url = reverse("logout") self.logout_url = reverse("logout")
@_patch_dict("config.settings.AUTH_COOKIE", {"SECURE": True})
def test_login_sets_secure_cookie(self): 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} data = {"email": self.user.email, "password": self.password}
cookie_name = "refresh_token" cookie_name = "refresh_token"
+1 -1
View File
@@ -42,7 +42,7 @@
"noUnusedVariables": "error" "noUnusedVariables": "error"
} }
}, },
"includes": ["**", "!backend"] "includes": ["**/src", "!backend"]
}, },
"assist": { "assist": {
"actions": { "actions": {
+19
View File
@@ -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
+1
View File
@@ -34,6 +34,7 @@ export async function registerAndLogin(
// 2. Activation via Mailpit // 2. Activation via Mailpit
logger.info(`[Auth] Polling Mailpit for activation email...`); logger.info(`[Auth] Polling Mailpit for activation email...`);
const activationLink = await MailpitHelper.getActivationLink(email); const activationLink = await MailpitHelper.getActivationLink(email);
await page.goto(activationLink); await page.goto(activationLink);
await expect(page.getByText(/account activated/i)).toBeVisible(); await expect(page.getByText(/account activated/i)).toBeVisible();
+1 -2
View File
@@ -7,7 +7,7 @@ export interface MailpitMessage {
To: { Address: string }[]; 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 = { export const MailpitHelper = {
getActivationLink: async ( getActivationLink: async (
@@ -18,7 +18,6 @@ export const MailpitHelper = {
const requestContext = await request.newContext(); const requestContext = await request.newContext();
while (Date.now() - startTime < timeout) { while (Date.now() - startTime < timeout) {
// Search specifically for the recipient to reduce data transfer
const response = await requestContext.get(`${MAILPIT_API_URL}/search`, { const response = await requestContext.get(`${MAILPIT_API_URL}/search`, {
params: { query: `to:${email}`, limit: 1 }, params: { query: `to:${email}`, limit: 1 },
}); });
+16 -4
View File
@@ -1,12 +1,20 @@
import path from "node:path"; import path from "node:path";
import process from "node:process"; import process, { env } from "node:process";
import { defineConfig, devices } from "@playwright/test"; import { defineConfig, devices } from "@playwright/test";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { getBaseUrl } from "./utils/url-builder";
/** /**
* Read environment variables from file. * Read environment variables from file.
*/ */
dotenv.config({ path: path.resolve(process.cwd(), "../.env.e2e") }); 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({ export default defineConfig({
timeout: 60000, timeout: 60000,
expect: { expect: {
@@ -26,7 +34,7 @@ export default defineConfig({
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* 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). */ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 20000, actionTimeout: 20000,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* 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 */ /* Run your local dev server before starting the tests */
webServer: { webServer: {
command: "bun run dev", command: "bun run dev -- --mode e2e",
url: process.env.FRONTEND_URL, url: getBaseUrl(
process.env.SSL_ENABLED === "true",
process.env.FRONTEND_DOMAIN,
process.env.FRONTEND_PORT,
),
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
}, },
+10 -10
View File
@@ -13,7 +13,7 @@ import { server } from "../../test/mocks/server";
import { useAuthStore } from "../store/useAuthStore"; import { useAuthStore } from "../store/useAuthStore";
import { api } from "./apiClient"; import { api } from "./apiClient";
const API_URL = "http://piku-server"; const VITE_API_URL = "http://piku-server";
beforeEach(() => { beforeEach(() => {
useAuthStore.setState({ useAuthStore.setState({
@@ -24,7 +24,7 @@ beforeEach(() => {
}); });
beforeAll(() => { beforeAll(() => {
vi.stubEnv("API_URL", API_URL); vi.stubEnv("VITE_API_URL", VITE_API_URL);
}); });
afterAll(() => { afterAll(() => {
@@ -37,7 +37,7 @@ describe("request interceptor", () => {
let capturedAuthHeader = ""; let capturedAuthHeader = "";
server.use( server.use(
http.get(`${API_URL}/api/auth/me/`, ({ request }) => { http.get(`${VITE_API_URL}/api/auth/me/`, ({ request }) => {
capturedAuthHeader = request.headers.get("Authorization") ?? ""; capturedAuthHeader = request.headers.get("Authorization") ?? "";
return HttpResponse.json(mockUser); return HttpResponse.json(mockUser);
}), }),
@@ -51,7 +51,7 @@ describe("request interceptor", () => {
it("should not send Authorization header when the store has no token", async () => { it("should not send Authorization header when the store has no token", async () => {
let capturedAuthHeader: string | null = ""; let capturedAuthHeader: string | null = "";
server.use( server.use(
http.get(`${API_URL}/api/auth/me/`, ({ request }) => { http.get(`${VITE_API_URL}/api/auth/me/`, ({ request }) => {
capturedAuthHeader = request.headers.get("Authorization"); capturedAuthHeader = request.headers.get("Authorization");
return HttpResponse.json({}); return HttpResponse.json({});
}), }),
@@ -70,14 +70,14 @@ describe("response interceptor", () => {
let _refreshApiCallCount = 0; let _refreshApiCallCount = 0;
server.use( server.use(
http.get(`${API_URL}/api/auth/me/`, ({ request }) => { http.get(`${VITE_API_URL}/api/auth/me/`, ({ request }) => {
meApiCallCount++; meApiCallCount++;
if (request.headers.get("Authorization") === "Bearer expired-token") { if (request.headers.get("Authorization") === "Bearer expired-token") {
return new HttpResponse(null, { status: 401 }); return new HttpResponse(null, { status: 401 });
} }
return HttpResponse.json(mockUser); return HttpResponse.json(mockUser);
}), }),
http.post(`${API_URL}/api/auth/refresh/`, () => { http.post(`${VITE_API_URL}/api/auth/refresh/`, () => {
_refreshApiCallCount++; _refreshApiCallCount++;
return HttpResponse.json({ access: "refreshed-token" }); return HttpResponse.json({ access: "refreshed-token" });
}), }),
@@ -94,13 +94,13 @@ describe("response interceptor", () => {
useAuthStore.getState().setAuth("expired-token", mockUser); useAuthStore.getState().setAuth("expired-token", mockUser);
server.use( 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") { if (request.headers.get("Authorization") === "Bearer expired-token") {
return new HttpResponse(null, { status: 401 }); return new HttpResponse(null, { status: 401 });
} }
return HttpResponse.json(mockUser); return HttpResponse.json(mockUser);
}), }),
http.post(`${API_URL}/api/auth/refresh/`, () => http.post(`${VITE_API_URL}/api/auth/refresh/`, () =>
HttpResponse.json({ access: "refreshed-token" }), HttpResponse.json({ access: "refreshed-token" }),
), ),
); );
@@ -115,14 +115,14 @@ describe("response interceptor", () => {
server.use( server.use(
http.get( http.get(
`${API_URL}/api/auth/me/`, `${VITE_API_URL}/api/auth/me/`,
() => () =>
new HttpResponse(JSON.stringify({ detail: "Invalid token" }), { new HttpResponse(JSON.stringify({ detail: "Invalid token" }), {
status: 401, status: 401,
}), }),
), ),
http.post( http.post(
`${API_URL}/api/auth/refresh/`, `${VITE_API_URL}/api/auth/refresh/`,
() => () =>
new HttpResponse(JSON.stringify({ detail: "Refresh failed" }), { new HttpResponse(JSON.stringify({ detail: "Refresh failed" }), {
status: 401, status: 401,
+5 -5
View File
@@ -17,7 +17,7 @@ import { useAuth } from "./useAuth";
vi.mock("../utils/crypto"); vi.mock("../utils/crypto");
vi.mock("../utils/keystore"); vi.mock("../utils/keystore");
const API_URL = "http://piku-server"; const VITE_API_URL = "http://piku-server";
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@@ -112,7 +112,7 @@ describe("logout", () => {
it("should call the logout API endpoint", async () => { it("should call the logout API endpoint", async () => {
let logoutCalled = false; let logoutCalled = false;
server.use( server.use(
http.post(`${API_URL}/api/auth/logout/`, () => { http.post(`${VITE_API_URL}/api/auth/logout/`, () => {
logoutCalled = true; logoutCalled = true;
return HttpResponse.json({}); return HttpResponse.json({});
}), }),
@@ -139,7 +139,7 @@ describe("logout", () => {
it("should clear the auth store even if the API call fails", async () => { it("should clear the auth store even if the API call fails", async () => {
server.use( server.use(
http.post( http.post(
`${API_URL}/api/auth/logout/`, `${VITE_API_URL}/api/auth/logout/`,
() => new HttpResponse(null, { status: 500 }), () => new HttpResponse(null, { status: 500 }),
), ),
); );
@@ -163,7 +163,7 @@ describe("initialize", () => {
}); });
let refreshCalled = false; let refreshCalled = false;
server.use( server.use(
http.post(`${API_URL}/api/auth/refresh/`, () => { http.post(`${VITE_API_URL}/api/auth/refresh/`, () => {
refreshCalled = true; refreshCalled = true;
return HttpResponse.json({ access: "new-token" }); 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 () => { it("should preserve the master key even if the refresh attempt fails", async () => {
server.use( server.use(
http.post( http.post(
`${API_URL}/api/auth/refresh/`, `${VITE_API_URL}/api/auth/refresh/`,
() => new HttpResponse(null, { status: 401 }), () => new HttpResponse(null, { status: 401 }),
), ),
); );
+9
View File
@@ -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;
};
+23 -8
View File
@@ -3,24 +3,39 @@ import path from "node:path";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { defineConfig, loadEnv } from "vite"; import { defineConfig, loadEnv } from "vite";
import { getBaseUrl } from "./utils/url-builder";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(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 { return {
envDir: "../", envDir: "../",
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
define: {
"import.meta.env.VITE_API_URL": JSON.stringify(baseApiUrl),
},
server: { server: {
port: Number(env.FRONTEND_PORT), port: Number(env.FRONTEND_PORT),
host: env.FRONTEND_DOMAIN, host: env.FRONTEND_DOMAIN,
https: { https: isSslEnabled ? ssl_certs : undefined,
key: fs.readFileSync(
path.resolve(__dirname, "../certs/localhost-key.pem"),
),
cert: fs.readFileSync(
path.resolve(__dirname, "../certs/localhost.pem"),
),
},
}, },
}; };
}); });
+180
View File
@@ -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(
+25 -76
View File
@@ -1,98 +1,47 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Get absolute path of project root # Use podman if available. Not everyone has it
PROJECT_ROOT=$(pwd) CONTAINER_BIN=$(command -v podman || command -v docker)
# Configuration ENV_FILE="./.env.e2e"
ENV_FILE="$PROJECT_ROOT/.env.e2e"
if [ -f "$ENV_FILE" ]; then if [ -f "$ENV_FILE" ]; then
echo "[INFO] Loading configuration from $ENV_FILE..." echo "Loading settings..."
set -a set -a
source "$ENV_FILE" source "$ENV_FILE"
set +a 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 else
echo "[INFO] Running in CI mode (using direct environment variables)..." echo "Error: Configuration file $ENV_FILE is missing!!"
exit 1
fi fi
# Map E2E variables to Django expected names # This cleans up containers. Very useful for local e2e to free system resources immediately.
# 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
cleanup() { cleanup() {
echo "[CLEANUP] Stopping services..." echo "Cleaning up..."
if [ "$CI" != "true" ]; then $CONTAINER_BIN rm -f "$DB_NAME" 2>/dev/null || true
podman rm -f $E2E_DB_NAME || true [ -n "$BACKEND_PID" ] && kill "$BACKEND_PID" 2>/dev/null || true
fi
if [ ! -z "$BACKEND_PID" ]; then
kill "$BACKEND_PID" 2>/dev/null || true
fi
} }
trap cleanup EXIT trap cleanup EXIT
# 2. Prepare Backend echo "Starting Database..."
echo "[BACKEND] Running database migrations..." $CONTAINER_BIN compose -f "./docker-compose.e2e.yml" up -d
export PIKU_ENV_FILE="$ENV_FILE"
(cd backend && uv run manage.py migrate --noinput)
echo "[BACKEND] Starting server on port $E2E_BACKEND_PORT..." # postgress will take some time, so we wait, and no race condition. Also, no point in logging this output
(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 & 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=$! 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 if [ "$CI" = "true" ]; then
echo "[TEST] Running Playwright Tests (CI)..." cd frontend && bun run test:e2e "$@"
(cd frontend && bun run test:e2e "$@")
else else
echo "[TEST] Running Playwright Tests in Distrobox..." # Because playwright decided not to support Fedora :)
(cd frontend && distrobox-enter --name ubuntu-24.04 -- env VITE_API_URL=$VITE_API_URL bun run test:e2e "$@") cd frontend && distrobox-enter --name ubuntu-24.04 -- bun run test:e2e "$@"
fi fi
echo "[SUCCESS] E2E Tests Completed."
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/bash #!/bin/bash
(podman compose up -d) & (podman compose up -d) &
(cd backend && uv run manage.py runserver) & (cd backend && uv run manage.py serve) &
(cd frontend && bun run dev) (cd frontend && bun run dev)