mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 08:56:52 +00:00
ci: add sll support and enhance e2e workflow
This commit is contained in:
+21
-19
@@ -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
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ dist/
|
|||||||
|
|
||||||
# Certificates
|
# Certificates
|
||||||
certs/*.pem
|
certs/*.pem
|
||||||
|
tmp/
|
||||||
|
|||||||
+13
-11
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
@@ -42,7 +42,7 @@
|
|||||||
"noUnusedVariables": "error"
|
"noUnusedVariables": "error"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"includes": ["**", "!backend"]
|
"includes": ["**/src", "!backend"]
|
||||||
},
|
},
|
||||||
"assist": {
|
"assist": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 }),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
+25
-10
@@ -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, "../", "");
|
||||||
return {
|
const isSslEnabled = env.SSL_ENABLED === "true";
|
||||||
envDir: "../",
|
let ssl_certs: { key: Buffer; cert: Buffer };
|
||||||
plugins: [react(), tailwindcss()],
|
|
||||||
server: {
|
if (isSslEnabled) {
|
||||||
port: Number(env.FRONTEND_PORT),
|
ssl_certs = {
|
||||||
host: env.FRONTEND_DOMAIN,
|
|
||||||
https: {
|
|
||||||
key: fs.readFileSync(
|
key: fs.readFileSync(
|
||||||
path.resolve(__dirname, "../certs/localhost-key.pem"),
|
path.resolve(__dirname, "../certs/localhost-key.pem"),
|
||||||
),
|
),
|
||||||
cert: fs.readFileSync(
|
cert: fs.readFileSync(path.resolve(__dirname, "../certs/localhost.pem")),
|
||||||
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: isSslEnabled ? ssl_certs : undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* Running on https://127.0.0.1:8001
|
||||||
|
[33mPress CTRL+C to quit[0m
|
||||||
|
* 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] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 401 -
|
||||||
|
Unauthorized: /api/auth/refresh/
|
||||||
|
127.0.0.1 - - [16/Apr/2026 18:45:15] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 401 -
|
||||||
|
Unauthorized: /api/auth/refresh/
|
||||||
|
127.0.0.1 - - [16/Apr/2026 18:45:15] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 401 -
|
||||||
|
Unauthorized: /api/auth/refresh/
|
||||||
|
127.0.0.1 - - [16/Apr/2026 18:45:16] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 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] "[35m[1mPOST /api/auth/register/ HTTP/1.1[0m" 201 -
|
||||||
|
127.0.0.1 - - [16/Apr/2026 18:45:17] "[35m[1mPOST /api/auth/register/ HTTP/1.1[0m" 201 -
|
||||||
|
127.0.0.1 - - [16/Apr/2026 18:45:17] "[35m[1mPOST /api/auth/register/ HTTP/1.1[0m" 201 -
|
||||||
|
127.0.0.1 - - [16/Apr/2026 18:45:17] "[35m[1mPOST /api/auth/register/ HTTP/1.1[0m" 201 -
|
||||||
|
Unauthorized: /api/auth/refresh/
|
||||||
|
127.0.0.1 - - [16/Apr/2026 18:45:19] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 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] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 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] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 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] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 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] "[35m[1mPUT /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1[0m" 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] "[35m[1mPUT /api/letters/e8f47036-6e57-41f5-b057-8ea712589a73/ HTTP/1.1[0m" 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] "[35m[1mPUT /api/letters/4e2af91a-1651-4bcd-85b5-c469ee4a73e3/ HTTP/1.1[0m" 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] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 401 -
|
||||||
|
Unauthorized: /api/auth/refresh/
|
||||||
|
127.0.0.1 - - [16/Apr/2026 18:45:30] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 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] "[35m[1mPOST /api/auth/register/ HTTP/1.1[0m" 201 -
|
||||||
|
127.0.0.1 - - [16/Apr/2026 18:45:32] "[35m[1mPOST /api/auth/register/ HTTP/1.1[0m" 201 -
|
||||||
|
Unauthorized: /api/auth/refresh/
|
||||||
|
127.0.0.1 - - [16/Apr/2026 18:45:34] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 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] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 401 -
|
||||||
|
127.0.0.1 - - [16/Apr/2026 18:45:36] "[35m[1mPOST /api/auth/register/ HTTP/1.1[0m" 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] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 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] "[35m[1mPOST /api/auth/register/ HTTP/1.1[0m" 201 -
|
||||||
|
Unauthorized: /api/auth/refresh/
|
||||||
|
127.0.0.1 - - [16/Apr/2026 18:45:37] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 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] "[35m[1mPUT /api/letters/351f0e2c-e10f-4851-9836-bc387758dbad/ HTTP/1.1[0m" 201 -
|
||||||
|
Unauthorized: /api/auth/refresh/
|
||||||
|
127.0.0.1 - - [16/Apr/2026 18:45:40] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 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] "[31m[1mPOST /api/auth/refresh/ HTTP/1.1[0m" 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] "[35m[1mPUT /api/letters/5dec1a15-1d19-47de-b5a5-21bc1845f90a/ HTTP/1.1[0m" 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] "[35m[1mPUT /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1[0m" 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
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user