07-Security-Testing / 07.01.API-Testing

07.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

# 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:

  1. All team repos are cloned
  2. Docker container con-bnc-cpt-api is built via make do-setup-api-no-cache
  3. pytest tests/ -v --tb=short runs inside the container
  4. 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_health.py — Health & Metadata Endpoints (6 tests)

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_validation.py — Input Validation & Error Responses (5 tests)

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