07-Security-Testing / 07.01.API-Testing07.01.API Testing
07.01. API Testing
Overview
The bnc-cpt-api project uses pytest with FastAPI TestClient for unit tests and httpx for post-deploy integration tests. Tests run both locally inside the Docker container and in GitHub Actions CI.
| Metric |
Value |
| Total tests |
90 (83 collected, 7 post-deploy skipped locally) |
| Unit tests |
76 |
| Integration tests (post-deploy) |
7 |
| Test files |
9 (+ conftest.py, utils.py) |
| Fixtures |
10 |
| Markers |
6 (unit, integration, slow, payment, auth, tesla) |
How to Run Tests
Local (via shell action — recommended)
# Run all API tests in parallel (auto-detect CPU cores)
cd /opt/bnc/bnc-cpt/bnc-cpt-utl
./run -a do_test_api_parallel
# Specify number of workers
WORKERS=4 ./run -a do_test_api_parallel
Local (manual, inside Docker container)
sudo docker exec con-bnc-cpt-api bash -c "
cd /opt/bnc/bnc-cpt/bnc-cpt-api/src/python/cpt-api && \
source .venv/bin/activate && \
python -m pytest tests/ -v --tb=short
"
# Run a specific test file or class
sudo docker exec con-bnc-cpt-api bash -c "
cd /opt/bnc/bnc-cpt/bnc-cpt-api/src/python/cpt-api && \
source .venv/bin/activate && \
python -m pytest tests/test_payment.py::TestStripeWebhook -v --tb=short
"
CI (GitHub Actions)
Tests run automatically on every push to master in the build-and-test job:
- All team repos are cloned
- Docker container
con-bnc-cpt-api is built via make do-setup-api-no-cache
pytest tests/ -v --tb=short runs inside the container
- On failure, tests re-run with
--tb=long for detailed output
Post-deploy integration tests run in the post-deploy-test job after deployment to inf and dev environments.
Shell Actions for Testing
| Shell Action |
Repo |
Description |
do_test_api_parallel |
bnc-cpt-utl |
Run API pytest in parallel using pytest-xdist inside con-bnc-cpt-api |
do_run_wui_tests |
bnc-cpt-utl |
Run WUI Puppeteer tests inside con-bnc-cpt-the-bot (headless) |
do_run_wui_tests_local |
bnc-cpt-utl |
Run WUI Puppeteer tests natively on host (supports headed mode) |
do_ci_health_check |
bnc-cpt-utl |
HTTP health check with retries — used in CI deploy and post-deploy jobs |
Test Configuration
File: src/python/cpt-api/pytest.ini
[pytest]
testpaths = tests
asyncio_mode = auto
addopts = -v --tb=short --strict-markers
markers =
unit: Unit tests
integration: Integration tests
slow: Slow running tests
post_deploy: Post-deploy smoke tests against live API
Environment variables set in conftest.py:
| Variable |
Test Value |
Purpose |
TESTING |
1 |
Signals test mode |
ENV |
test |
Environment identifier |
JWT_SECRET_KEY |
test_secret_key_for_testing_only |
JWT signing key |
ADMIN_USERNAME |
admin |
Admin login for auth tests |
ADMIN_PASSWORD |
admin |
Admin password for auth tests |
STRIPE_SECRET_KEY |
sk_test_fake_for_tests |
Stripe API key (fake) |
TESLA_CLIENT_ID |
"" (empty) |
Forces Tesla mock/unconfigured mode |
TESLA_CLIENT_SECRET |
"" (empty) |
Forces Tesla mock/unconfigured mode |
Fixtures (conftest.py)
| Fixture |
Scope |
Description |
app |
session |
FastAPI application instance |
client |
function |
Synchronous TestClient for endpoint tests |
async_client |
function |
Async httpx.AsyncClient via ASGITransport |
mock_stripe |
function |
Mocks Stripe Checkout Session + PaymentIntent, preserves stripe.error |
mock_paypal |
function |
Mocks PayPal provider |
auth_headers |
function |
Logs in as admin, returns Authorization: Bearer <token> header |
test_payment_data |
session |
Sample payment amounts for basic/pro/bulk plans |
test_user_data |
session |
Sample user email, password, name |
reset_tesla_fleet_service |
function (autouse) |
Clears TeslaFleetService._states and ._reports between tests |
reset_rate_limiter |
function (autouse) |
Resets slowapi rate limiter between tests |
Test Files — What Is Tested
| Test |
Endpoint |
What it verifies |
test_root_endpoint |
GET / |
Returns 200 with expected response body |
test_health_check |
GET /health |
Returns 200 with health status |
test_readiness_check |
GET /ready |
Returns 200 if readiness endpoint exists |
test_openapi_schema_available |
GET /openapi.json |
OpenAPI schema is accessible and valid JSON |
test_docs_endpoint |
GET /docs |
Swagger UI returns 200 |
test_redoc_endpoint |
GET /redoc |
ReDoc returns 200 |
test_api_endpoints.py — General API Behaviour (5 tests)
| Test |
What it verifies |
test_list_endpoint_returns_list |
List endpoints return JSON arrays |
test_invalid_endpoint_returns_404 |
Non-existent routes return 404 |
test_method_not_allowed |
Wrong HTTP methods return 405 |
test_async_root_endpoint |
Root endpoint works via async client |
test_concurrent_requests |
API handles concurrent async requests without errors |
test_auth.py — Authentication (8 tests)
| Test |
Endpoint |
What it verifies |
test_login_with_valid_credentials |
POST /api/v1/auth/login/json |
Valid admin creds return 200 + token |
test_login_with_invalid_credentials |
POST /api/v1/auth/login/json |
Invalid creds return 401 |
test_login_missing_username |
POST /api/v1/auth/login/json |
Missing username returns 422 |
test_login_missing_password |
POST /api/v1/auth/login/json |
Missing password returns 422 |
test_login_empty_credentials |
POST /api/v1/auth/login/json |
Empty creds handled |
test_protected_endpoint_without_token |
GET /api/v1/payment/receipt/* |
No token returns 401 |
test_protected_endpoint_with_invalid_token |
GET /api/v1/payment/receipt/* |
Invalid token returns 401 |
test_protected_endpoint_malformed_header |
GET /api/v1/payment/receipt/* |
Malformed Authorization header rejected |
test_payment.py — Payment Processing (26 tests)
TestPaymentCreateIntent (5 tests)
| Test |
Endpoint |
What it verifies |
test_create_intent_with_valid_stripe_request |
POST /api/v1/payment/create-intent |
Valid Stripe request accepted (200) or graceful fail (400/500) |
test_create_intent_with_valid_paypal_request |
POST /api/v1/payment/create-intent |
PayPal request accepted or returns expected error |
test_create_intent_missing_amount |
POST /api/v1/payment/create-intent |
Missing amount field returns 422 |
test_create_intent_invalid_provider |
POST /api/v1/payment/create-intent |
Invalid provider name returns 400/422 |
test_create_intent_invalid_plan |
POST /api/v1/payment/create-intent |
Invalid plan name returns 400/422 |
TestPaymentVerify (5 tests)
| Test |
Endpoint |
What it verifies |
test_verify_payment_missing_payment_id |
POST /api/v1/payment/verify |
Missing payment_id returns 422 |
test_verify_payment_invalid_id_format |
POST /api/v1/payment/verify |
Invalid format returns 400/404/422 |
test_verify_stripe_payment_id_format |
POST /api/v1/payment/verify |
Stripe pi_* format handled correctly |
test_verify_stripe_checkout_session_returns_succeeded |
POST /api/v1/payment/verify |
cs_* session ID verifies as succeeded (mocked Stripe) |
test_verify_stripe_payment_intent_returns_succeeded |
POST /api/v1/payment/verify |
pi_* intent ID verifies as succeeded (mocked Stripe) |
TestPaymentReceipt (4 tests)
| Test |
Endpoint |
What it verifies |
test_receipt_nonexistent_id |
GET /api/v1/payment/receipt/*/pdf |
Nonexistent ID returns 401/404 |
test_receipt_endpoint_requires_auth |
GET /api/v1/payment/receipt/*/pdf |
Endpoint requires authentication |
test_receipt_endpoint_exists |
GET /api/v1/payment/receipt/*/pdf |
Endpoint is registered (not 405) |
test_receipt_with_auth_nonexistent_id |
GET /api/v1/payment/receipt/*/pdf |
Authenticated request with bad ID returns 404 |
TestPaymentPlans (3 parameterised tests)
| Test |
What it verifies |
test_valid_plan_amounts[basic-1900] |
Basic plan (1900 cents / 19.00 EUR) accepted |
test_valid_plan_amounts[pro-2900] |
Pro plan (2900 cents / 29.00 EUR) accepted |
test_valid_plan_amounts[bulk-14900] |
Bulk plan (14900 cents / 149.00 EUR) accepted |
TestStripeWebhook (10 tests)
| Test |
What it verifies |
test_missing_signature_header_returns_422 |
FastAPI requires stripe-signature header |
test_webhook_secret_not_configured |
Returns 500 when STRIPE_WEBHOOK_SECRET is unset |
test_invalid_signature_returns_400 |
SignatureVerificationError returns 400 "Invalid signature" |
test_invalid_payload_returns_400 |
Malformed body (ValueError) returns 400 "Invalid payload" |
test_succeeded_event_updates_status |
payment_intent.succeeded sets payment status to succeeded |
test_failed_event_updates_status |
payment_intent.payment_failed sets payment status to failed |
test_canceled_event_updates_status |
payment_intent.canceled sets payment status to canceled |
test_succeeded_event_unknown_payment_id_still_ok |
Unknown payment_id in event doesn't crash (returns 200) |
test_unhandled_event_type_returns_success |
Unrecognised event types acknowledged (200) but not processed |
test_construct_event_receives_raw_body_and_secret |
construct_event() called with correct payload, sig, and secret |
test_tesla.py — Tesla OAuth & Fleet API (30 tests)
TestTeslaOAuthInitiate (4 tests)
| Test |
Endpoint |
What it verifies |
test_oauth_initiate_missing_required_fields |
POST /api/v1/tesla/oauth/initiate |
Missing paymentId+plan returns 422 |
test_oauth_initiate_missing_plan |
POST /api/v1/tesla/oauth/initiate |
Missing plan returns 422 |
test_oauth_initiate_missing_payment_id |
POST /api/v1/tesla/oauth/initiate |
Missing paymentId returns 422 |
test_oauth_initiate_unconfigured_returns_503 |
POST /api/v1/tesla/oauth/initiate |
Returns 503 when Tesla credentials empty |
TestTeslaOAuthCallback (3 tests)
| Test |
Endpoint |
What it verifies |
test_oauth_callback_without_code_redirects_error |
GET /api/v1/tesla/oauth/callback |
No code param redirects with ?oauth=error |
test_oauth_callback_with_invalid_state_redirects_error |
GET /api/v1/tesla/oauth/callback |
Invalid state redirects with error |
test_oauth_callback_with_error_param_redirects_error |
GET /api/v1/tesla/oauth/callback |
Tesla error param forwarded as redirect error |
TestTeslaStoredReport (3 tests)
| Test |
Endpoint |
What it verifies |
test_get_report_nonexistent_session_returns_404 |
GET /api/v1/tesla/report/{id} |
Nonexistent session returns 404 |
test_get_report_returns_stored_data |
GET /api/v1/tesla/report/{id} |
Valid session returns stored report data |
test_get_report_is_one_time_retrieval |
GET /api/v1/tesla/report/{id} |
Report deleted after first retrieval |
TestTeslaVehicleReportPdf (1 test)
| Test |
Endpoint |
What it verifies |
test_vehicle_report_pdf_endpoint |
POST /api/v1/tesla/report/pdf |
Returns PDF content type |
TestTeslaFleetServiceUnconfigured (3 tests)
| Test |
What it verifies |
test_is_not_configured_in_test_env |
is_configured() returns False when credentials empty |
test_initiate_oauth_raises_503 |
initiate_oauth() raises TeslaAPIError |
test_fetch_and_store_report_raises_503 |
fetch_and_store_report() raises TeslaAPIError |
TestTeslaFleetServiceConfigured (14 tests)
| Test |
What it verifies |
test_is_configured_with_credentials |
is_configured() returns True with valid credentials |
test_initiate_oauth_returns_auth_url |
OAuth URL contains client_id and redirect_uri |
test_initiate_oauth_stores_state |
State stored with paymentId, plan, and created_at |
test_validate_state_consumes_state |
State returned and removed from _states dict |
test_validate_state_rejects_invalid |
Unknown state raises TeslaOAuthError |
test_validate_state_rejects_expired |
Expired state (>15 min) raises TeslaOAuthError |
test_get_stored_report_returns_and_deletes |
Report returned and deleted from _reports |
test_get_stored_report_raises_on_missing |
Missing session raises TeslaAPIError |
test_get_stored_report_raises_on_expired |
Expired report raises TeslaAPIError |
test_fetch_and_store_report_success |
Full mocked flow: token exchange, vehicle fetch, report storage |
test_fetch_and_store_report_token_exchange_failure |
Failed token exchange raises TeslaOAuthError |
test_fetch_and_store_report_no_vehicles |
No vehicles raises TeslaAPIError |
test_cleanup_expired_removes_old_entries |
_cleanup_expired() removes entries older than 15 min |
TestTeslaHelpers (2 tests)
| Test |
What it verifies |
test_car_type_to_model |
Maps Tesla car_type strings (e.g. "models") to display names |
test_vin_to_year |
Extracts model year from VIN position 10 character |
test_report.py — PDF Report Generation (3 tests)
| Test |
What it verifies |
test_report_endpoint_method |
Report endpoint accepts correct HTTP method |
test_pdf_content_type |
PDF endpoints return application/pdf content type |
test_receipts_storage_path_in_config |
RECEIPTS_DIR is configured in settings |
| Test |
What it verifies |
test_invalid_json_body |
Invalid JSON body returns proper error response |
test_missing_required_fields |
Missing required fields return 422 with detail |
test_invalid_field_types |
Wrong types return 422 validation error |
test_404_response_format |
404 responses contain detail field |
test_error_response_is_json |
All error responses are JSON-formatted |
test_post_deploy.py — Post-Deploy Smoke Tests (7 tests)
These run only in CI after deployment to inf/dev environments. They test the live deployed API via API_URL env var. Skipped locally when API_URL is not set.
| Test |
Endpoint |
What it verifies |
test_health_endpoint |
GET /health |
Live API returns 200 with healthy status |
test_root_endpoint |
GET / |
Live API returns version info |
test_swagger_ui |
GET /docs |
Swagger UI accessible on live API |
test_openapi_schema |
GET /openapi.json |
OpenAPI schema accessible on live API |
test_create_intent_rejects_invalid |
POST /api/v1/payment/create-intent |
Live API rejects invalid payment body |
test_verify_rejects_invalid |
POST /api/v1/payment/verify |
Live API rejects invalid verify body |
test_login_rejects_bad_credentials |
POST /api/v1/auth/login/json |
Live API rejects bad credentials |
test_tesla_live.py — Manual Tesla Integration Test
Not a pytest file. Standalone script for manual testing of real Tesla Fleet API calls with actual OAuth credentials. Used during development, not in CI.
CI Pipeline Test Integration
The ci-cd: api-build-deploy GitHub Actions workflow has 4 jobs that involve testing:
| Job |
What it does |
Tests executed |
| build-and-test |
Builds Docker container, runs full pytest suite |
pytest tests/ -v --tb=short (83 tests) |
| deploy |
Deploys to Cloud Run, runs health check |
do_ci_health_check against Cloud Run URL |
| post-deploy-test |
Runs smoke tests against live deployed API |
pytest tests/test_post_deploy.py -v -m integration (7 tests) |
CI test flow
push to master
|
v
build-and-test ─── pytest 83 unit tests inside Docker
|
v
deploy (inf, dev) ── health check via Cloud Run service URL
|
v
post-deploy-test (inf, dev) ── 7 integration tests + 3 curl smoke tests (health, docs, payment)
Known Gaps
These are existing functionalities with zero or minimal test coverage, prioritised by risk:
| Gap |
Risk |
Notes |
PayPal webhook (POST /webhook/paypal) |
High |
Endpoint has event handling code but zero tests |
PayPal capture (POST /capture/paypal/{order_id}) |
High |
Full capture flow, zero tests |
Payment provider routing (get_provider_by_payment_id) |
Medium |
Prefix detection logic (pi_ / cs_ / PAYID-), untested |
Auth service functions (verify_password, verify_token) |
Medium |
Only tested indirectly through endpoints |
| PDF VAT calculations |
Medium |
Real arithmetic, only tested via endpoint returning bytes |
| Rate limiting (429 responses) |
Low |
Configured via slowapi but never tested |