mirror of
https://github.com/apidoorman/doorman.git
synced 2025-12-30 06:00:10 -06:00
cleanup
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -76,5 +76,5 @@ jobs:
|
||||
- name: Build web client
|
||||
working-directory: web-client
|
||||
env:
|
||||
NEXT_PUBLIC_SERVER_URL: http://localhost:5001
|
||||
NEXT_PUBLIC_SERVER_URL: http://localhost:3001
|
||||
run: npm run build
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -39,6 +39,8 @@ backend-services/proto/
|
||||
**/*.pid
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Cookie jars
|
||||
cookies.txt
|
||||
@@ -62,6 +64,7 @@ scripts/dedupe_docblocks.py
|
||||
# Logs and runtime artifacts
|
||||
*.log
|
||||
**/*.log
|
||||
pytest_backend_verbose.log
|
||||
backend-services/platform-logs/
|
||||
**/platform-logs/
|
||||
backend-services/platform-logs/doorman.log
|
||||
@@ -70,6 +73,10 @@ backend-services/platform-logs/doorman.log
|
||||
backend-services/generated/
|
||||
**/generated/
|
||||
backend-services/proto/*.bin
|
||||
backend-services/proto/*
|
||||
backend-services/routes/proto/*
|
||||
**/*memory_dump*.bin
|
||||
**/*.bin
|
||||
.production.env
|
||||
.coverage
|
||||
coverage_html/
|
||||
|
||||
22
Makefile
22
Makefile
@@ -1,7 +1,7 @@
|
||||
SHELL := /bin/bash
|
||||
|
||||
# Configurable envs (can override on CLI or set in backend-services/.env)
|
||||
BASE_URL ?= http://localhost:5001
|
||||
BASE_URL ?= http://localhost:3001
|
||||
ADMIN_EMAIL ?= $(shell grep '^DOORMAN_ADMIN_EMAIL=' backend-services/.env 2>/dev/null | cut -d'=' -f2)
|
||||
ADMIN_PASSWORD ?= $(shell grep '^DOORMAN_ADMIN_PASSWORD=' backend-services/.env 2>/dev/null | cut -d'=' -f2)
|
||||
|
||||
@@ -61,3 +61,23 @@ coverage-html:
|
||||
coverage-all:
|
||||
BASE_URL=$(BASE_URL) DOORMAN_ADMIN_EMAIL=$(ADMIN_EMAIL) DOORMAN_ADMIN_PASSWORD=$(ADMIN_PASSWORD) \
|
||||
bash scripts/coverage_all.sh
|
||||
|
||||
.PHONY: clean clean-deep
|
||||
|
||||
# Remove common local build/test artifacts without touching dependencies
|
||||
clean:
|
||||
@echo "Cleaning caches and runtime artifacts..."
|
||||
@find . -type d -name "__pycache__" -prune -exec rm -rf {} + || true
|
||||
@find . -type d -name ".pytest_cache" -prune -exec rm -rf {} + || true
|
||||
@find . -type f -name "*.py[co]" -delete || true
|
||||
@find . -type f -name ".DS_Store" -delete || true
|
||||
@rm -rf backend-services/platform-logs/*.log backend-services/doorman.pid doorman.pid uvicorn.pid || true
|
||||
@rm -rf web-client/.next || true
|
||||
@rm -f pytest_backend_verbose.log || true
|
||||
@echo "Done."
|
||||
|
||||
# Deeper cleanup that also removes generated dev artifacts
|
||||
clean-deep: clean
|
||||
@echo "Removing generated dev artifacts..."
|
||||
@rm -rf generated backend-services/generated || true
|
||||
@echo "Done."
|
||||
|
||||
12
README.md
12
README.md
@@ -20,8 +20,18 @@ A lightweight API gateway built for AI, REST, SOAP, GraphQL, and gRPC APIs. No s
|
||||
## Features
|
||||
Doorman supports user management, authentication, authorizaiton, dynamic routing, roles, groups, rate limiting, throttling, logging, redis caching, mongodb, and endpoint request payload validation. It allows you to manage REST, AI, SOAP, GraphQL, and gRPC APIs.
|
||||
|
||||
## Repository Structure
|
||||
- `backend-services/`: Python gateway core, routes, services, tests, and runtime assets.
|
||||
- `web-client/`: Next.js frontend (uses `next.config.mjs`).
|
||||
- `docker/`: Entrypoint and container scripts.
|
||||
- `user-docs/`: End-user documentation and guides.
|
||||
- `scripts/`: Helper scripts (preflight, coverage, maintenance).
|
||||
- `user-docs/examples/tests/`: Example tests previously at the repo root.
|
||||
- `generated/`: Local dev artifacts (migrated into `backend-services/generated` at runtime).
|
||||
- Dev shims moved to `scripts/test-shims/`: helper wrappers to run specific backend tests directly if desired.
|
||||
|
||||
## Launch With Docker
|
||||
Ensure an env file exists at the repo root: `./.env` (use `./.env.example` as a reference). Keep this file outside the image and pass it at runtime. Note - this is set for development, update variables and hosts to reflect a production environment.
|
||||
Ensure an env file exists at the repo root: `./.env` (use `./backend-services/.env.example` as a reference). Keep this file outside the image and pass it at runtime. Note - this is set for development, update variables and hosts to reflect a production environment.
|
||||
|
||||
### Quickstart
|
||||
See commands below.
|
||||
|
||||
@@ -175,8 +175,8 @@ LOG_FORMAT=plain
|
||||
# LOGS_DIR=/path/to/logs
|
||||
|
||||
# App
|
||||
PORT=5001
|
||||
THREADS=4
|
||||
PORT=3001
|
||||
THREADS=1
|
||||
DEV_RELOAD=false
|
||||
# Production environment flag (enforces HTTPS checks)
|
||||
ENV=development
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
|
||||
BASE_URL = os.getenv('DOORMAN_BASE_URL', 'http://localhost:5001').rstrip('/')
|
||||
BASE_URL = os.getenv('DOORMAN_BASE_URL', 'http://localhost:3001').rstrip('/')
|
||||
ADMIN_EMAIL = os.getenv('DOORMAN_ADMIN_EMAIL', 'admin@doorman.dev')
|
||||
ADMIN_PASSWORD = os.getenv('DOORMAN_ADMIN_PASSWORD')
|
||||
if not ADMIN_PASSWORD:
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
/**
|
||||
* Doorman API Gateway - k6 Load Test
|
||||
*
|
||||
* Tests multiple scenarios to establish performance baseline:
|
||||
* 1. Authentication (login)
|
||||
* 2. REST Gateway (API proxying)
|
||||
* 3. GraphQL Gateway
|
||||
* 4. SOAP Gateway
|
||||
* 5. Mixed workload
|
||||
*
|
||||
* Run with:
|
||||
* k6 run load-tests/k6-load-test.js
|
||||
*
|
||||
* Generate HTML report:
|
||||
* k6 run --out json=load-tests/results.json load-tests/k6-load-test.js
|
||||
* k6 report load-tests/results.json --output load-tests/report.html
|
||||
*/
|
||||
|
||||
import http from 'k6/http';
|
||||
import { check, group, sleep } from 'k6';
|
||||
import { Rate, Trend, Counter } from 'k6/metrics';
|
||||
|
||||
// Custom metrics
|
||||
const authSuccessRate = new Rate('auth_success_rate');
|
||||
const restGatewayLatency = new Trend('rest_gateway_latency');
|
||||
const graphqlGatewayLatency = new Trend('graphql_gateway_latency');
|
||||
const soapGatewayLatency = new Trend('soap_gateway_latency');
|
||||
const errorCount = new Counter('error_count');
|
||||
|
||||
// Configuration
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8000';
|
||||
const TEST_USERNAME = __ENV.TEST_USERNAME || 'admin';
|
||||
const TEST_PASSWORD = __ENV.TEST_PASSWORD || 'change-me';
|
||||
|
||||
// Load test stages
|
||||
export const options = {
|
||||
scenarios: {
|
||||
// Scenario 1: Smoke test (1 VU for 30s)
|
||||
smoke: {
|
||||
executor: 'constant-vus',
|
||||
vus: 1,
|
||||
duration: '30s',
|
||||
tags: { scenario: 'smoke' },
|
||||
exec: 'smokeTest',
|
||||
},
|
||||
|
||||
// Scenario 2: Load test (ramp up to 50 VUs)
|
||||
load: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 0,
|
||||
stages: [
|
||||
{ duration: '1m', target: 10 }, // Ramp up to 10 VUs
|
||||
{ duration: '3m', target: 10 }, // Stay at 10 VUs
|
||||
{ duration: '1m', target: 50 }, // Ramp up to 50 VUs
|
||||
{ duration: '3m', target: 50 }, // Stay at 50 VUs
|
||||
{ duration: '1m', target: 0 }, // Ramp down to 0
|
||||
],
|
||||
tags: { scenario: 'load' },
|
||||
exec: 'loadTest',
|
||||
startTime: '30s', // Start after smoke test
|
||||
},
|
||||
|
||||
// Scenario 3: Stress test (push to limits)
|
||||
stress: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 0,
|
||||
stages: [
|
||||
{ duration: '2m', target: 100 }, // Ramp up to 100 VUs
|
||||
{ duration: '5m', target: 100 }, // Stay at 100 VUs
|
||||
{ duration: '2m', target: 200 }, // Push to 200 VUs
|
||||
{ duration: '5m', target: 200 }, // Stay at 200 VUs
|
||||
{ duration: '2m', target: 0 }, // Ramp down
|
||||
],
|
||||
tags: { scenario: 'stress' },
|
||||
exec: 'stressTest',
|
||||
startTime: '10m', // Start after load test
|
||||
},
|
||||
|
||||
// Scenario 4: Spike test (sudden traffic spike)
|
||||
spike: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 0,
|
||||
stages: [
|
||||
{ duration: '10s', target: 10 }, // Normal load
|
||||
{ duration: '10s', target: 200 }, // Sudden spike
|
||||
{ duration: '1m', target: 200 }, // Hold spike
|
||||
{ duration: '10s', target: 10 }, // Back to normal
|
||||
{ duration: '30s', target: 10 }, // Recovery period
|
||||
],
|
||||
tags: { scenario: 'spike' },
|
||||
exec: 'spikeTest',
|
||||
startTime: '26m', // Start after stress test
|
||||
},
|
||||
},
|
||||
|
||||
thresholds: {
|
||||
// Overall thresholds
|
||||
'http_req_duration': ['p(50)<100', 'p(95)<500', 'p(99)<1000'], // Latency targets
|
||||
'http_req_failed': ['rate<0.05'], // Error rate < 5%
|
||||
|
||||
// Authentication thresholds
|
||||
'auth_success_rate': ['rate>0.95'],
|
||||
|
||||
// Gateway-specific thresholds
|
||||
'rest_gateway_latency': ['p(50)<150', 'p(95)<600', 'p(99)<1200'],
|
||||
'graphql_gateway_latency': ['p(50)<200', 'p(95)<800', 'p(99)<1500'],
|
||||
'soap_gateway_latency': ['p(50)<250', 'p(95)<1000', 'p(99)<2000'],
|
||||
|
||||
// Error threshold
|
||||
'error_count': ['count<100'],
|
||||
},
|
||||
};
|
||||
|
||||
// Helper: Login and get auth token
|
||||
function login() {
|
||||
const loginPayload = JSON.stringify({
|
||||
username: TEST_USERNAME,
|
||||
password: TEST_PASSWORD,
|
||||
});
|
||||
|
||||
const params = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const response = http.post(`${BASE_URL}/auth/authorization`, loginPayload, params);
|
||||
|
||||
const success = check(response, {
|
||||
'login status is 200': (r) => r.status === 200,
|
||||
'login has access_token_cookie': (r) => r.cookies.access_token_cookie !== undefined,
|
||||
});
|
||||
|
||||
authSuccessRate.add(success);
|
||||
|
||||
if (success) {
|
||||
return response.cookies.access_token_cookie[0].value;
|
||||
}
|
||||
|
||||
errorCount.add(1);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper: Make authenticated request
|
||||
function makeAuthRequest(method, url, token, body = null) {
|
||||
const params = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cookies: {
|
||||
'access_token_cookie': token,
|
||||
},
|
||||
};
|
||||
|
||||
if (body) {
|
||||
return http.request(method, url, JSON.stringify(body), params);
|
||||
}
|
||||
return http.request(method, url, null, params);
|
||||
}
|
||||
|
||||
// Smoke Test: Basic functionality
|
||||
export function smokeTest() {
|
||||
group('Smoke Test - Basic Functionality', () => {
|
||||
// Test 1: Health check
|
||||
group('Health Check', () => {
|
||||
const response = http.get(`${BASE_URL}/health`);
|
||||
check(response, {
|
||||
'health check status is 200': (r) => r.status === 200,
|
||||
});
|
||||
});
|
||||
|
||||
// Test 2: Login
|
||||
group('Login', () => {
|
||||
const token = login();
|
||||
check(token, {
|
||||
'login successful': (t) => t !== null,
|
||||
});
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
});
|
||||
}
|
||||
|
||||
// Load Test: Realistic workload
|
||||
export function loadTest() {
|
||||
const token = login();
|
||||
if (!token) {
|
||||
errorCount.add(1);
|
||||
return;
|
||||
}
|
||||
|
||||
group('Load Test - Realistic Workload', () => {
|
||||
// Test 1: List APIs
|
||||
group('List APIs', () => {
|
||||
const response = makeAuthRequest('GET', `${BASE_URL}/api/apis`, token);
|
||||
check(response, {
|
||||
'list apis status is 200': (r) => r.status === 200,
|
||||
'list apis returns array': (r) => {
|
||||
try {
|
||||
const body = JSON.parse(r.body);
|
||||
return Array.isArray(body);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Test 2: REST Gateway (simulate API call)
|
||||
group('REST Gateway', () => {
|
||||
const start = Date.now();
|
||||
const response = http.get(`${BASE_URL}/myapi/v1/users`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const duration = Date.now() - start;
|
||||
|
||||
restGatewayLatency.add(duration);
|
||||
|
||||
check(response, {
|
||||
'rest gateway responds': (r) => r.status !== 0,
|
||||
}) || errorCount.add(1);
|
||||
});
|
||||
|
||||
// Test 3: List users
|
||||
group('List Users', () => {
|
||||
const response = makeAuthRequest('GET', `${BASE_URL}/user/users`, token);
|
||||
check(response, {
|
||||
'list users status is 200': (r) => r.status === 200,
|
||||
});
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
});
|
||||
}
|
||||
|
||||
// Stress Test: Push system to limits
|
||||
export function stressTest() {
|
||||
const token = login();
|
||||
if (!token) {
|
||||
errorCount.add(1);
|
||||
return;
|
||||
}
|
||||
|
||||
group('Stress Test - High Load', () => {
|
||||
// Rapid-fire requests
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const response = makeAuthRequest('GET', `${BASE_URL}/api/apis`, token);
|
||||
check(response, {
|
||||
'stress test request succeeds': (r) => r.status === 200,
|
||||
}) || errorCount.add(1);
|
||||
}
|
||||
|
||||
sleep(0.5);
|
||||
});
|
||||
}
|
||||
|
||||
// Spike Test: Sudden traffic increase
|
||||
export function spikeTest() {
|
||||
const token = login();
|
||||
if (!token) {
|
||||
errorCount.add(1);
|
||||
return;
|
||||
}
|
||||
|
||||
group('Spike Test - Traffic Burst', () => {
|
||||
// Burst of concurrent requests
|
||||
const responses = http.batch([
|
||||
['GET', `${BASE_URL}/api/apis`, null, { cookies: { 'access_token_cookie': token } }],
|
||||
['GET', `${BASE_URL}/user/users`, null, { cookies: { 'access_token_cookie': token } }],
|
||||
['GET', `${BASE_URL}/role/roles`, null, { cookies: { 'access_token_cookie': token } }],
|
||||
['GET', `${BASE_URL}/group/groups`, null, { cookies: { 'access_token_cookie': token } }],
|
||||
]);
|
||||
|
||||
responses.forEach((response) => {
|
||||
check(response, {
|
||||
'spike test request succeeds': (r) => r.status === 200,
|
||||
}) || errorCount.add(1);
|
||||
});
|
||||
|
||||
sleep(0.1);
|
||||
});
|
||||
}
|
||||
|
||||
// Summary handler
|
||||
export function handleSummary(data) {
|
||||
return {
|
||||
'stdout': textSummary(data, { indent: ' ', enableColors: true }),
|
||||
'load-tests/k6-summary.json': JSON.stringify(data, null, 2),
|
||||
};
|
||||
}
|
||||
|
||||
// Text summary helper
|
||||
function textSummary(data, options) {
|
||||
const indent = options?.indent || '';
|
||||
const enableColors = options?.enableColors || false;
|
||||
|
||||
const lines = [
|
||||
'',
|
||||
`${indent}===============================================`,
|
||||
`${indent} Doorman API Gateway - Load Test Results`,
|
||||
`${indent}===============================================`,
|
||||
'',
|
||||
`${indent}Scenarios:`,
|
||||
`${indent} ✓ Smoke Test (1 VU, 30s)`,
|
||||
`${indent} ✓ Load Test (0→10→50 VUs, 9m)`,
|
||||
`${indent} ✓ Stress Test (0→100→200 VUs, 16m)`,
|
||||
`${indent} ✓ Spike Test (10→200→10 VUs, 2m)`,
|
||||
'',
|
||||
`${indent}Key Metrics:`,
|
||||
`${indent} HTTP Requests: ${data.metrics.http_reqs?.values?.count || 0}`,
|
||||
`${indent} Failed Requests: ${data.metrics.http_req_failed?.values?.rate ? (data.metrics.http_req_failed.values.rate * 100).toFixed(2) : 0}%`,
|
||||
`${indent} Auth Success Rate: ${data.metrics.auth_success_rate?.values?.rate ? (data.metrics.auth_success_rate.values.rate * 100).toFixed(2) : 0}%`,
|
||||
'',
|
||||
`${indent}Latency (p50/p95/p99):`,
|
||||
`${indent} Overall: ${data.metrics.http_req_duration?.values?.['p(50)']?.toFixed(2) || 0}ms / ${data.metrics.http_req_duration?.values?.['p(95)']?.toFixed(2) || 0}ms / ${data.metrics.http_req_duration?.values?.['p(99)']?.toFixed(2) || 0}ms`,
|
||||
`${indent} REST: ${data.metrics.rest_gateway_latency?.values?.['p(50)']?.toFixed(2) || 0}ms / ${data.metrics.rest_gateway_latency?.values?.['p(95)']?.toFixed(2) || 0}ms / ${data.metrics.rest_gateway_latency?.values?.['p(99)']?.toFixed(2) || 0}ms`,
|
||||
`${indent} GraphQL: ${data.metrics.graphql_gateway_latency?.values?.['p(50)']?.toFixed(2) || 0}ms / ${data.metrics.graphql_gateway_latency?.values?.['p(95)']?.toFixed(2) || 0}ms / ${data.metrics.graphql_gateway_latency?.values?.['p(99)']?.toFixed(2) || 0}ms`,
|
||||
`${indent} SOAP: ${data.metrics.soap_gateway_latency?.values?.['p(50)']?.toFixed(2) || 0}ms / ${data.metrics.soap_gateway_latency?.values?.['p(95)']?.toFixed(2) || 0}ms / ${data.metrics.soap_gateway_latency?.values?.['p(99)']?.toFixed(2) || 0}ms`,
|
||||
'',
|
||||
`${indent}===============================================`,
|
||||
'',
|
||||
];
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// k6 load test for /api/rest/* and /platform/* with thresholds and JUnit output
|
||||
// Usage:
|
||||
// k6 run k6/load.test.js \
|
||||
// -e BASE_URL=http://localhost:5001 \
|
||||
// k6 run load-tests/k6/load.test.js \
|
||||
// -e BASE_URL=http://localhost:3001 \
|
||||
// -e RPS=50 \
|
||||
// -e DURATION=1m \
|
||||
// -e REST_PATHS='["/api/rest/health"]' \
|
||||
@@ -19,7 +19,7 @@ import http from 'k6/http'
|
||||
import { check, sleep, group } from 'k6'
|
||||
import { Trend, Rate, Counter } from 'k6/metrics'
|
||||
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:5001'
|
||||
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001'
|
||||
const DURATION = __ENV.DURATION || '1m'
|
||||
const RPS = Number(__ENV.RPS || 20)
|
||||
const REST_PATHS = (function () {
|
||||
@@ -134,6 +134,7 @@ export function handleSummary (data) {
|
||||
return {
|
||||
'junit.xml': junitXml,
|
||||
'summary.json': JSON.stringify(data, null, 2),
|
||||
'load-tests/k6-summary.json': JSON.stringify(data, null, 2),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +143,6 @@ function escapeXml (s) {
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/\"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ from urllib.parse import urljoin
|
||||
import requests
|
||||
|
||||
def base_url() -> str:
|
||||
return os.getenv('BASE_URL', 'http://localhost:5001').rstrip('/') + '/'
|
||||
return os.getenv('BASE_URL', 'http://localhost:3001').rstrip('/') + '/'
|
||||
|
||||
def _csrf(sess: requests.Session) -> str | None:
|
||||
for c in sess.cookies:
|
||||
@@ -116,7 +116,7 @@ def do_rotate_admin(sess: requests.Session, args):
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description='Doorman admin CLI')
|
||||
p.add_argument('--base-url', default=os.getenv('BASE_URL'), help='Override base URL (default env BASE_URL or http://localhost:5001)')
|
||||
p.add_argument('--base-url', default=os.getenv('BASE_URL'), help='Override base URL (default env BASE_URL or http://localhost:3001)')
|
||||
p.add_argument('--email', default=os.getenv('DOORMAN_ADMIN_EMAIL', 'admin@doorman.dev'))
|
||||
p.add_argument('--password', default=os.getenv('DOORMAN_ADMIN_PASSWORD'))
|
||||
p.add_argument('-y', '--yes', action='store_true', help='Assume yes for safety prompts')
|
||||
|
||||
19
scripts/cleanup_repo.sh
Executable file
19
scripts/cleanup_repo.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "[cleanup] Removing caches and runtime artifacts..."
|
||||
find . -type d -name "__pycache__" -prune -exec rm -rf {} + || true
|
||||
find . -type d -name ".pytest_cache" -prune -exec rm -rf {} + || true
|
||||
find . -type f -name "*.py[co]" -delete || true
|
||||
find . -type f -name ".DS_Store" -delete || true
|
||||
rm -rf backend-services/platform-logs/*.log backend-services/doorman.pid doorman.pid uvicorn.pid || true
|
||||
rm -rf web-client/.next || true
|
||||
rm -f pytest_backend_verbose.log || true
|
||||
|
||||
if [[ "${1-}" == "--deep" ]]; then
|
||||
echo "[cleanup] Removing generated dev artifacts..."
|
||||
rm -rf generated backend-services/generated || true
|
||||
fi
|
||||
|
||||
echo "[cleanup] Done."
|
||||
|
||||
@@ -7,10 +7,10 @@ set -euo pipefail
|
||||
# - Combines .coverage files and prints a report
|
||||
#
|
||||
# Env:
|
||||
# BASE_URL (default http://localhost:5001)
|
||||
# BASE_URL (default http://localhost:3001)
|
||||
# DOORMAN_ADMIN_EMAIL, DOORMAN_ADMIN_PASSWORD (required for live tests)
|
||||
|
||||
BASE_URL="${BASE_URL:-http://localhost:5001}"
|
||||
BASE_URL="${BASE_URL:-http://localhost:3001}"
|
||||
EMAIL="${DOORMAN_ADMIN_EMAIL:-}"
|
||||
PASSWORD="${DOORMAN_ADMIN_PASSWORD:-}"
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ set -euo pipefail
|
||||
|
||||
# One-time helper to force-clear stored allow_localhost_bypass=false via REST API.
|
||||
# Env vars:
|
||||
# - BASE_URL (default http://localhost:5001)
|
||||
# - BASE_URL (default http://localhost:3001)
|
||||
# - DOORMAN_ADMIN_EMAIL (required, no default)
|
||||
# - DOORMAN_ADMIN_PASSWORD (required, no default)
|
||||
|
||||
BASE_URL="${BASE_URL:-http://localhost:5001}"
|
||||
BASE_URL="${BASE_URL:-http://localhost:3001}"
|
||||
EMAIL="${DOORMAN_ADMIN_EMAIL:-}"
|
||||
PASSWORD="${DOORMAN_ADMIN_PASSWORD:-}"
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ set -euo pipefail
|
||||
# - Optionally smokes REST/SOAP/GraphQL gateway if SMOKE_* upstream URLs are provided
|
||||
#
|
||||
# Env:
|
||||
# BASE_URL (default http://localhost:5001)
|
||||
# BASE_URL (default http://localhost:3001)
|
||||
# DOORMAN_ADMIN_EMAIL, DOORMAN_ADMIN_PASSWORD (required)
|
||||
# SMOKE_REST_UPSTREAM (optional, e.g., http://httpbin.org)
|
||||
# SMOKE_SOAP_UPSTREAM (optional)
|
||||
# SMOKE_GQL_UPSTREAM (optional)
|
||||
|
||||
BASE_URL="${BASE_URL:-http://localhost:5001}"
|
||||
BASE_URL="${BASE_URL:-http://localhost:3001}"
|
||||
EMAIL="${DOORMAN_ADMIN_EMAIL:-}"
|
||||
PASSWORD="${DOORMAN_ADMIN_PASSWORD:-}"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Runs k6 load-tests/k6-load-test.js, captures CPU/loop-lag stats while running,
|
||||
# Runs k6 load-tests/k6/load.test.js, captures CPU/loop-lag stats while running,
|
||||
# and compares results against a baseline summary via scripts/compare_perf.py.
|
||||
|
||||
BASE_URL=${BASE_URL:-http://localhost:8000}
|
||||
@@ -36,7 +36,7 @@ cleanup() {
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
echo "Running k6 load test..."
|
||||
K6_CMD=(k6 run load-tests/k6-load-test.js --env BASE_URL="${BASE_URL}")
|
||||
K6_CMD=(k6 run load-tests/k6/load.test.js --env BASE_URL="${BASE_URL}")
|
||||
"${K6_CMD[@]}"
|
||||
|
||||
echo
|
||||
@@ -52,4 +52,3 @@ fi
|
||||
echo
|
||||
echo "Comparing current vs baseline..."
|
||||
python3 scripts/compare_perf.py "${CURRENT_JSON}" "${BASELINE_JSON}"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${BASE_URL:-http://localhost:5001}"
|
||||
BASE_URL="${BASE_URL:-http://localhost:3001}"
|
||||
EMAIL="${DOORMAN_ADMIN_EMAIL:-}"
|
||||
PASSWORD="${DOORMAN_ADMIN_PASSWORD:-}"
|
||||
|
||||
|
||||
8
scripts/test-shims/README.md
Normal file
8
scripts/test-shims/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
These are optional developer shims that used to live at the repo root.
|
||||
|
||||
- `test_ip_filter_platform.py` and `test_platform_expanded.py` allow running select backend tests via their shim paths.
|
||||
- `conftest.py` mirrors backend test fixtures for the shims.
|
||||
- `sitecustomize.py` mirrors logging redaction behavior for ad-hoc runs; primary copies exist under `backend-services/` and `backend-services/tests/`.
|
||||
|
||||
They are not required for normal test execution. Use `make unit` or run `pytest` from `backend-services/`.
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
"""
|
||||
Root-level pytest configuration shim.
|
||||
Pytest configuration shim for developer test shims.
|
||||
|
||||
Allows running tests from backend-services/tests using root-level nodeids
|
||||
like `pytest -v test_ip_filter_platform.py::test_...` by dynamically
|
||||
loading the backend-services test fixtures into this module's namespace.
|
||||
Executes backend-services/tests/conftest.py and exposes its fixtures
|
||||
in this module's namespace for the shim tests in this folder.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import runpy
|
||||
|
||||
|
||||
def _path(*parts: str) -> str:
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), *parts))
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', *parts))
|
||||
|
||||
|
||||
_BS_DIR = _path('backend-services')
|
||||
if _BS_DIR not in sys.path:
|
||||
sys.path.insert(0, _BS_DIR)
|
||||
|
||||
def _load_backend_services_conftest():
|
||||
"""Execute backend-services/tests/conftest.py and copy its fixtures here.
|
||||
|
||||
This makes its fixtures visible to pytest when collecting root-level tests.
|
||||
"""
|
||||
def _load_backend_services_conftest():
|
||||
bs_conftest = _path('backend-services', 'tests', 'conftest.py')
|
||||
if not os.path.exists(bs_conftest):
|
||||
return
|
||||
@@ -32,5 +30,6 @@ def _load_backend_services_conftest():
|
||||
continue
|
||||
g.setdefault(k, v)
|
||||
|
||||
|
||||
_load_backend_services_conftest()
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
"""
|
||||
Ensure the 'doorman.gateway' and 'doorman.logging' loggers have a redaction filter
|
||||
attached, even when the application module isn't imported (e.g., unit tests that
|
||||
only import logging). Python automatically imports sitecustomize at interpreter
|
||||
startup when it's present on sys.path.
|
||||
Optional dev sitecustomize to mirror logging redaction during ad-hoc runs.
|
||||
|
||||
Primary redaction hooks live under:
|
||||
- backend-services/sitecustomize.py
|
||||
- backend-services/tests/sitecustomize.py
|
||||
|
||||
Keeping this here avoids cluttering the repo root.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
class _RedactFilter(logging.Filter):
|
||||
PATTERNS = [
|
||||
re.compile(r'(?i)(authorization\s*[:=]\s*)([^;\r\n]+)'),
|
||||
@@ -31,6 +35,7 @@ class _RedactFilter(logging.Filter):
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def _ensure_logger(name: str):
|
||||
logger = logging.getLogger(name)
|
||||
for h in logger.handlers:
|
||||
@@ -41,6 +46,7 @@ def _ensure_logger(name: str):
|
||||
h.addFilter(_RedactFilter())
|
||||
logger.addHandler(h)
|
||||
|
||||
|
||||
try:
|
||||
_ensure_logger('doorman.gateway')
|
||||
_ensure_logger('doorman.logging')
|
||||
@@ -1,22 +1,24 @@
|
||||
"""
|
||||
Root-level test shim for backend-services/tests/test_ip_filter_platform.py
|
||||
Shim for backend-services/tests/test_ip_filter_platform.py
|
||||
|
||||
Enables running:
|
||||
pytest -v test_ip_filter_platform.py::test_...
|
||||
from the repository root.
|
||||
Allows running from repo root:
|
||||
pytest -v scripts/test-shims/test_ip_filter_platform.py::test_...
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import runpy
|
||||
|
||||
|
||||
def _path(*parts: str) -> str:
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), *parts))
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', *parts))
|
||||
|
||||
|
||||
_BS_DIR = _path('backend-services')
|
||||
if _BS_DIR not in sys.path:
|
||||
sys.path.insert(0, _BS_DIR)
|
||||
|
||||
|
||||
def _load_tests():
|
||||
src = _path('backend-services', 'tests', 'test_ip_filter_platform.py')
|
||||
if not os.path.exists(src):
|
||||
@@ -28,5 +30,6 @@ def _load_tests():
|
||||
continue
|
||||
g.setdefault(k, v)
|
||||
|
||||
|
||||
_load_tests()
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
"""
|
||||
Root-level test shim for backend-services/tests/test_platform_expanded.py
|
||||
Shim for backend-services/tests/test_platform_expanded.py
|
||||
|
||||
Enables running:
|
||||
pytest -v test_platform_expanded.py::test_security_and_memory_dump_restore
|
||||
from the repository root.
|
||||
Allows running from repo root:
|
||||
pytest -v scripts/test-shims/test_platform_expanded.py::test_security_and_memory_dump_restore
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import runpy
|
||||
|
||||
|
||||
def _path(*parts: str) -> str:
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), *parts))
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', *parts))
|
||||
|
||||
|
||||
_BS_DIR = _path('backend-services')
|
||||
if _BS_DIR not in sys.path:
|
||||
sys.path.insert(0, _BS_DIR)
|
||||
|
||||
|
||||
def _load_tests():
|
||||
src = _path('backend-services', 'tests', 'test_platform_expanded.py')
|
||||
if not os.path.exists(src):
|
||||
@@ -28,5 +30,6 @@ def _load_tests():
|
||||
continue
|
||||
g.setdefault(k, v)
|
||||
|
||||
|
||||
_load_tests()
|
||||
|
||||
@@ -25,7 +25,7 @@ docker compose up --build
|
||||
```
|
||||
|
||||
**Services will be available at:**
|
||||
- Backend API: `http://localhost:5001`
|
||||
- Backend API: `http://localhost:3001`
|
||||
- Web UI: `http://localhost:3000`
|
||||
|
||||
### 2. Configure Environment Variables
|
||||
@@ -110,7 +110,7 @@ cd web-client
|
||||
cp .env.local.example .env.local
|
||||
|
||||
# Edit .env.local and set:
|
||||
# NEXT_PUBLIC_SERVER_URL=http://localhost:5001
|
||||
# NEXT_PUBLIC_SERVER_URL=http://localhost:3001
|
||||
|
||||
npm ci
|
||||
npm run dev # Development mode
|
||||
@@ -131,11 +131,11 @@ export DOORMAN_ADMIN_PASSWORD="YourStrongPassword123!"
|
||||
curl -s -c /tmp/doorman.cookies \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"email\":\"$DOORMAN_ADMIN_EMAIL\",\"password\":\"$DOORMAN_ADMIN_PASSWORD\"}" \
|
||||
http://localhost:5001/platform/authorization
|
||||
http://localhost:3001/platform/authorization
|
||||
|
||||
# Check authentication status
|
||||
curl -s -b /tmp/doorman.cookies \
|
||||
http://localhost:5001/platform/authorization/status
|
||||
http://localhost:3001/platform/authorization/status
|
||||
```
|
||||
|
||||
### Via Web UI
|
||||
@@ -153,7 +153,7 @@ Let's publish a simple REST API backed by httpbin for testing.
|
||||
```bash
|
||||
curl -s -b /tmp/doorman.cookies \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X POST http://localhost:5001/platform/credit \
|
||||
-X POST http://localhost:3001/platform/credit \
|
||||
-d '{
|
||||
"api_credit_group": "demo-api",
|
||||
"api_key": "demo-secret-key-123",
|
||||
@@ -175,7 +175,7 @@ curl -s -b /tmp/doorman.cookies \
|
||||
```bash
|
||||
curl -s -b /tmp/doorman.cookies \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X POST http://localhost:5001/platform/api \
|
||||
-X POST http://localhost:3001/platform/api \
|
||||
-d '{
|
||||
"api_name": "demo",
|
||||
"api_version": "v1",
|
||||
@@ -197,7 +197,7 @@ curl -s -b /tmp/doorman.cookies \
|
||||
# GET endpoint
|
||||
curl -s -b /tmp/doorman.cookies \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X POST http://localhost:5001/platform/endpoint \
|
||||
-X POST http://localhost:3001/platform/endpoint \
|
||||
-d '{
|
||||
"api_name": "demo",
|
||||
"api_version": "v1",
|
||||
@@ -209,7 +209,7 @@ curl -s -b /tmp/doorman.cookies \
|
||||
# POST endpoint
|
||||
curl -s -b /tmp/doorman.cookies \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X POST http://localhost:5001/platform/endpoint \
|
||||
-X POST http://localhost:3001/platform/endpoint \
|
||||
-d '{
|
||||
"api_name": "demo",
|
||||
"api_version": "v1",
|
||||
@@ -224,7 +224,7 @@ curl -s -b /tmp/doorman.cookies \
|
||||
```bash
|
||||
curl -s -b /tmp/doorman.cookies \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X POST http://localhost:5001/platform/subscription/subscribe \
|
||||
-X POST http://localhost:3001/platform/subscription/subscribe \
|
||||
-d '{
|
||||
"username": "admin",
|
||||
"api_name": "demo",
|
||||
@@ -237,13 +237,13 @@ curl -s -b /tmp/doorman.cookies \
|
||||
```bash
|
||||
# GET request
|
||||
curl -s -b /tmp/doorman.cookies \
|
||||
"http://localhost:5001/api/rest/demo/v1/get?test=123"
|
||||
"http://localhost:3001/api/rest/demo/v1/get?test=123"
|
||||
|
||||
# POST request
|
||||
curl -s -b /tmp/doorman.cookies \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"message": "Hello Doorman!"}' \
|
||||
http://localhost:5001/api/rest/demo/v1/post
|
||||
http://localhost:3001/api/rest/demo/v1/post
|
||||
```
|
||||
|
||||
Doorman will automatically inject the `x-api-key` header to the upstream service!
|
||||
|
||||
@@ -557,18 +557,18 @@ ALLOW_CREDENTIALS=True
|
||||
Doorman includes comprehensive security test coverage:
|
||||
|
||||
**Test suites:**
|
||||
- `tests/test_auth_csrf_https.py` - CSRF validation
|
||||
- `tests/test_production_https_guard.py` - HTTPS enforcement
|
||||
- `tests/test_ip_policy_allow_deny_cidr.py` - IP filtering
|
||||
- `tests/test_security.py` - General security features
|
||||
- `tests/test_request_id_and_logging_redaction.py` - Audit trail
|
||||
- `backend-services/tests/test_auth_csrf_https.py` - CSRF validation
|
||||
- `backend-services/tests/test_production_https_guard.py` - HTTPS enforcement
|
||||
- `backend-services/tests/test_ip_policy_allow_deny_cidr.py` - IP filtering
|
||||
- `backend-services/tests/test_security.py` - General security features
|
||||
- `backend-services/tests/test_request_id_and_logging_redaction.py` - Audit trail
|
||||
|
||||
**Run security tests:**
|
||||
```bash
|
||||
cd backend-services
|
||||
pytest tests/test_auth_csrf_https.py -v
|
||||
pytest tests/test_production_https_guard.py -v
|
||||
pytest tests/test_ip_policy_allow_deny_cidr.py -v
|
||||
pytest backend-services/tests/test_auth_csrf_https.py -v
|
||||
pytest backend-services/tests/test_production_https_guard.py -v
|
||||
pytest backend-services/tests/test_ip_policy_allow_deny_cidr.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -17,7 +17,7 @@ Throughout this guide, the **platform API** lives under `/platform/*` and the **
|
||||
## Conventions
|
||||
|
||||
```bash
|
||||
BASE=http://localhost:5001 # Backend URL
|
||||
BASE=http://localhost:3001 # Backend URL
|
||||
COOKIE=/tmp/doorman.cookies # Cookie jar path
|
||||
```
|
||||
|
||||
|
||||
@@ -836,11 +836,11 @@ Import and tune thresholds to match your SLOs.
|
||||
|
||||
## Load Testing with k6
|
||||
|
||||
Use `k6/load.test.js` to validate performance and SLOs in CI:
|
||||
Use `load-tests/k6/load.test.js` to validate performance and SLOs in CI:
|
||||
|
||||
```bash
|
||||
k6 run k6/load.test.js \
|
||||
-e BASE_URL=http://localhost:5001 \
|
||||
k6 run load-tests/k6/load.test.js \
|
||||
-e BASE_URL=http://localhost:3001 \
|
||||
-e RPS=50 \
|
||||
-e DURATION=2m \
|
||||
-e REST_PATHS='["/api/rest/health"]' \
|
||||
|
||||
@@ -429,7 +429,7 @@ Command‑line helper for common maintenance actions with safety prompts.
|
||||
|
||||
```bash
|
||||
# Environment (development defaults shown)
|
||||
export BASE_URL=http://localhost:5001
|
||||
export BASE_URL=http://localhost:3001
|
||||
export DOORMAN_ADMIN_EMAIL=admin@doorman.dev
|
||||
export DOORMAN_ADMIN_PASSWORD=your-admin-password
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ Load Tests (k6 preferred, Locust optional)
|
||||
Run against a running Doorman instance. k6 script writes a JSON summary; helper scripts compare against a baseline to detect regressions.
|
||||
|
||||
- k6 quick run (BASE_URL defaults to http://localhost:8000):
|
||||
- k6 run load-tests/k6-load-test.js --env BASE_URL=http://localhost:8000
|
||||
- k6 run load-tests/k6/load.test.js --env BASE_URL=http://localhost:8000
|
||||
|
||||
- End-to-end perf check with regression gating:
|
||||
- BASE_URL=http://localhost:8000 bash scripts/run_perf_check.sh
|
||||
@@ -82,4 +82,3 @@ Troubleshooting
|
||||
- Auth: Admin password must match the one you started Doorman with.
|
||||
- k6 missing: Install from https://k6.io/docs/get-started/installation/
|
||||
- Baseline not found: Create one per the Load Tests section above.
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ def test_jwt_algorithm_enforcement():
|
||||
print()
|
||||
print("4. Related files:")
|
||||
print(" - utils/auth_util.py:94-99 (main auth)")
|
||||
print(" - tests/test_redis_token_revocation_ha.py:48-52 (test code)")
|
||||
print(" - backend-services/tests/test_redis_token_revocation_ha.py:48-52 (test code)")
|
||||
print()
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -25,7 +25,7 @@ def test_load_testing_implementation():
|
||||
|
||||
locations = [
|
||||
{
|
||||
'file': 'load-tests/k6-load-test.js',
|
||||
'file': 'load-tests/k6/load.test.js',
|
||||
'type': 'k6 Load Test',
|
||||
'scenarios': 4,
|
||||
'description': 'JavaScript-based load testing with k6'
|
||||
@@ -253,16 +253,16 @@ def test_load_testing_implementation():
|
||||
print("Running Tests:")
|
||||
print()
|
||||
print(" k6 - Basic Run:")
|
||||
print(" k6 run load-tests/k6-load-test.js")
|
||||
print(" k6 run load-tests/k6/load.test.js")
|
||||
print()
|
||||
print(" k6 - Custom Configuration:")
|
||||
print(" k6 run --env BASE_URL=https://api.example.com \\")
|
||||
print(" --env TEST_USERNAME=admin \\")
|
||||
print(" --env TEST_PASSWORD=secret \\")
|
||||
print(" load-tests/k6-load-test.js")
|
||||
print(" load-tests/k6/load.test.js")
|
||||
print()
|
||||
print(" k6 - Generate JSON Results:")
|
||||
print(" k6 run --out json=results.json load-tests/k6-load-test.js")
|
||||
print(" k6 run --out json=results.json load-tests/k6/load.test.js")
|
||||
print()
|
||||
print(" Locust - Web UI Mode:")
|
||||
print(" locust -f load-tests/locust-load-test.py \\")
|
||||
@@ -37,7 +37,7 @@
|
||||
<pre>
|
||||
# Example curl
|
||||
curl -H "Authorization: Bearer ..." \
|
||||
http://localhost:5001/api/rest/users/v1/list
|
||||
http://localhost:3001/api/rest/users/v1/list
|
||||
</pre>
|
||||
|
||||
<h3 id="servers">Servers</h3>
|
||||
@@ -94,14 +94,14 @@ curl -H "Authorization: Bearer ..." \
|
||||
# REST call through Doorman
|
||||
curl -H "Authorization: Bearer <token>" \
|
||||
-H "client-key: demo-client" \
|
||||
http://localhost:5001/api/rest/users/v1/items/42
|
||||
http://localhost:3001/api/rest/users/v1/items/42
|
||||
|
||||
# GraphQL
|
||||
curl -H "Authorization: Bearer <token>" \
|
||||
-H "X-API-Version: v1" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"{ hello }"}' \
|
||||
http://localhost:5001/api/graphql/example
|
||||
http://localhost:3001/api/graphql/example
|
||||
</pre>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user