Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cc48bfe
test: Add end-to-end tests and setup for test environment
Polliog Nov 27, 2025
a70f22e
test: Add end-to-end tests for Ingestion API and enhance API key hand…
Polliog Nov 27, 2025
560b165
fix: Update API key handling in tests and normalize log trace IDs
Polliog Nov 27, 2025
03a1212
test: Add end-to-end tests for API key management and organization is…
Polliog Nov 27, 2025
4ddb01b
test: Add end-to-end tests for alert rule evaluation and worker relia…
Polliog Nov 27, 2025
d672254
test: Add end-to-end tests for rate limiting and input validation
Polliog Nov 27, 2025
9337ec1
test: Add end-to-end load tests for ingestion and query performance
Polliog Nov 27, 2025
b9e2753
test: Add end-to-end load testing infrastructure and scripts
Polliog Nov 27, 2025
7edf476
test: Add end-to-end tests for authentication and organization manage…
Polliog Nov 28, 2025
bcafd58
test: Add end-to-end tests for alerts and notifications routes
Polliog Nov 28, 2025
57f39bc
test: Add CI configuration and update README with badges
Polliog Nov 28, 2025
33cb83c
test: Update pnpm version to 10 in CI configuration and package.json
Polliog Nov 28, 2025
964f81c
test: Add MailHog service to CI configuration for email testing
Polliog Nov 28, 2025
57ad571
test: Enhance webhook notification tests with fetch mocking
Polliog Nov 28, 2025
1fccfa8
test: Remove staging deployment steps from CI configuration
Polliog Nov 28, 2025
3c1386a
test: Add E2E testing setup with Playwright and enhance test coverage
Polliog Nov 28, 2025
1b3f0c6
Merge remote-tracking branch 'logward-dev/main' into E2E-Tests
Polliog Nov 28, 2025
e69b48a
fix: Change import to type for FullConfig in global setup and teardown
Polliog Nov 28, 2025
f67bc72
fix: Update CI configuration to use 'docker compose' command for test…
Polliog Nov 28, 2025
98a6076
test: Enhance E2E tests with organization context and validation erro…
Polliog Nov 28, 2025
537ea2c
test: Improve E2E test for handling invalid tokens in network spec
Polliog Nov 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 71 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ jobs:
run: pnpm test:ci -- --coverage

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: packages/backend/coverage/lcov.info
directory: packages/backend/coverage
flags: backend
name: backend-coverage
fail_ci_if_error: false
Expand Down Expand Up @@ -148,13 +148,81 @@ jobs:
PUBLIC_API_URL: http://localhost:8080
run: pnpm --filter '@logward/frontend' typecheck

# ====================
# E2E Tests (Playwright)
# ====================
e2e-test:
name: E2E Tests
runs-on: ubuntu-latest
needs: [backend-test, typecheck]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install Playwright browsers
working-directory: packages/frontend
run: npx playwright install --with-deps chromium

- name: Start test infrastructure
run: |
docker compose -f docker-compose.test.yml up -d --build
# Wait for services to be healthy
echo "Waiting for services to be ready..."
timeout 120 bash -c 'until curl -s http://localhost:3001/health > /dev/null; do sleep 2; done'
echo "Backend is ready"
timeout 120 bash -c 'until curl -s http://localhost:3002 > /dev/null; do sleep 2; done'
echo "Frontend is ready"

- name: Run E2E tests
working-directory: packages/frontend
env:
E2E: 'true'
TEST_API_URL: http://localhost:3001
TEST_FRONTEND_URL: http://localhost:3002
run: npx playwright test --reporter=list

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: packages/frontend/playwright-report/
retention-days: 7

- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results
path: packages/frontend/test-results/
retention-days: 7

- name: Stop test infrastructure
if: always()
run: docker compose -f docker-compose.test.yml down -v

# ====================
# Build Docker Images
# ====================
build:
name: Build Docker Images
runs-on: ubuntu-latest
needs: [backend-test, typecheck]
needs: [backend-test, typecheck, e2e-test]

steps:
- name: Checkout
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ Thumbs.db
# Testing
coverage/
.nyc_output/
test-results/
playwright-report/
playwright/.cache/

# Misc
.cache/
Expand Down
32 changes: 32 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,34 @@ services:
networks:
- logward-test-network

# Frontend for E2E testing
frontend-test:
build:
context: .
dockerfile: packages/frontend/Dockerfile
args:
PUBLIC_API_URL: http://localhost:3001
container_name: logward-frontend-test
environment:
NODE_ENV: production
PORT: 3000
HOST: 0.0.0.0
PUBLIC_API_URL: http://localhost:3001
ORIGIN: http://localhost:3002
ports:
- "3002:3000"
depends_on:
backend-test:
condition: service_healthy
healthcheck:
test: [ "CMD", "node", "-e", "require('http').get('http://localhost:3000/', (r) => r.statusCode === 200 ? process.exit(0) : process.exit(1))" ]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
networks:
- logward-test-network

# Backend for E2E and load testing
backend-test:
build:
Expand All @@ -63,6 +91,10 @@ services:
# Higher rate limits for load testing (100 req/s = 6000/min, use 100000 for safety)
RATE_LIMIT_MAX: 100000
RATE_LIMIT_WINDOW: 60000
# Higher auth rate limits for E2E testing (many user registrations)
AUTH_RATE_LIMIT_REGISTER: 10000
AUTH_RATE_LIMIT_LOGIN: 10000
AUTH_RATE_LIMIT_WINDOW: 60000
ports:
- "3001:8080"
depends_on:
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
"build": "pnpm --recursive --filter \"./packages/**\" build",
"build:shared": "pnpm --filter \"@logward/shared\" build",
"test": "pnpm --recursive --filter \"./packages/**\" test",
"test:e2e": "pnpm --filter \"@logward/frontend\" test:e2e",
"test:e2e:up": "docker-compose -f docker-compose.test.yml up -d --build",
"test:e2e:down": "docker-compose -f docker-compose.test.yml down -v",
"test:e2e:run": "pnpm test:e2e:up && pnpm test:e2e && pnpm test:e2e:down",
"test:e2e:headed": "pnpm --filter \"@logward/frontend\" test:e2e:headed",
"test:e2e:debug": "pnpm --filter \"@logward/frontend\" test:e2e:debug",
"typecheck": "pnpm --recursive --filter \"./packages/**\" typecheck",
"clean": "pnpm --recursive --filter \"./packages/**\" clean"
},
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ const configSchema = z.object({
// Rate limiting
RATE_LIMIT_MAX: z.string().default('1000').transform(Number),
RATE_LIMIT_WINDOW: z.string().default('60000').transform(Number), // 1 minute in ms

// Auth rate limiting (separate from general rate limiting for security)
AUTH_RATE_LIMIT_REGISTER: z.string().default('10').transform(Number), // Registrations per window
AUTH_RATE_LIMIT_LOGIN: z.string().default('20').transform(Number), // Login attempts per window
AUTH_RATE_LIMIT_WINDOW: z.string().default('900000').transform(Number), // 15 minutes in ms
});

export type Config = z.infer<typeof configSchema>;
Expand Down
9 changes: 5 additions & 4 deletions packages/backend/src/modules/users/routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import { usersService } from './service.js';
import { config } from '../../config/index.js';

const registerSchema = z.object({
email: z.string().email(),
Expand Down Expand Up @@ -29,8 +30,8 @@ export async function usersRoutes(fastify: FastifyInstance) {
fastify.post('/register', {
config: {
rateLimit: {
max: 10, // 10 registrations per 15 minutes
timeWindow: '15 minutes'
max: config.AUTH_RATE_LIMIT_REGISTER, // Configurable via AUTH_RATE_LIMIT_REGISTER env var
timeWindow: config.AUTH_RATE_LIMIT_WINDOW // Configurable via AUTH_RATE_LIMIT_WINDOW env var
}
},
handler: async (request, reply) => {
Expand Down Expand Up @@ -82,8 +83,8 @@ export async function usersRoutes(fastify: FastifyInstance) {
fastify.post('/login', {
config: {
rateLimit: {
max: 20, // 20 login attempts per 15 minutes
timeWindow: '15 minutes'
max: config.AUTH_RATE_LIMIT_LOGIN, // Configurable via AUTH_RATE_LIMIT_LOGIN env var
timeWindow: config.AUTH_RATE_LIMIT_WINDOW // Configurable via AUTH_RATE_LIMIT_WINDOW env var
}
},
handler: async (request, reply) => {
Expand Down
8 changes: 7 additions & 1 deletion packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@
"build": "vite build",
"preview": "vite preview",
"test": "playwright test",
"test:e2e": "E2E=true playwright test",
"test:e2e:headed": "E2E=true playwright test --headed",
"test:e2e:debug": "E2E=true playwright test --debug",
"test:e2e:ui": "E2E=true playwright test --ui",
"test:journeys": "E2E=true playwright test tests/journeys",
"test:edge-cases": "E2E=true playwright test tests/edge-cases",
"typecheck": "svelte-kit sync && tsc --noEmit",
"clean": "rm -rf .svelte-kit build"
"clean": "rm -rf .svelte-kit build test-results"
},
"dependencies": {
"@logward/shared": "workspace:*",
Expand Down
111 changes: 88 additions & 23 deletions packages/frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,92 @@
import { defineConfig, devices } from '@playwright/test';

// E2E test environment URLs
const TEST_FRONTEND_URL = process.env.TEST_FRONTEND_URL || 'http://localhost:3002';
const TEST_API_URL = process.env.TEST_API_URL || 'http://localhost:3001';

// Check if running in E2E mode (using docker-compose test environment)
const isE2E = process.env.E2E === 'true' || process.env.CI === 'true';

export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
},

projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],

webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
testDir: './tests',
fullyParallel: false, // Run tests sequentially to avoid race conditions
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : 1, // Single worker for stability
reporter: [
['html', { open: 'never' }],
['list'],
...(process.env.CI ? [['github' as const]] : []),
],

// Global timeout
timeout: 60000, // 60 seconds per test
expect: {
timeout: 10000, // 10 seconds for assertions
},

use: {
// Use E2E frontend URL when in E2E mode
baseURL: isE2E ? TEST_FRONTEND_URL : 'http://localhost:5173',

// Capture trace on first retry
trace: 'on-first-retry',

// Screenshots on failure
screenshot: 'only-on-failure',

// Video on failure
video: 'on-first-retry',

// Browser context options
viewport: { width: 1280, height: 720 },
ignoreHTTPSErrors: true,

// Action timeout
actionTimeout: 10000,

// Navigation timeout
navigationTimeout: 30000,
},

// Test projects for different browsers
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// Enable these for full browser coverage
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
// Mobile viewports
// {
// name: 'mobile-chrome',
// use: { ...devices['Pixel 5'] },
// },
],

// Web server configuration (only for dev mode, not E2E)
...(isE2E
? {}
: {
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
}),

// Output directory for test artifacts
outputDir: 'test-results',

// Global setup/teardown
globalSetup: isE2E ? './tests/global-setup.ts' : undefined,
globalTeardown: isE2E ? './tests/global-teardown.ts' : undefined,
});
Loading
Loading