Testing an AIWAF-protected application
Source and package versions
One AIWAF repository.
Python: py/aiwaf/ · Node.js: js/ · Rust/WASM: rust/ · Java: java/.
Source and canonical README.
Versions shown describe the source checkout; package releases are independent.
Current Python/Node source targets are aiwaf 1.1.8 (Python) and
aiwaf 1.1.5 (npm), with Rust/Python 0.2.7 and
WASM 0.2.5 and Java 1.3.5. Sandbox guides retain their separately pinned builds.
Test both the application's successful responses and the protection decisions that can stop a request before its handler. Start with deterministic limits, then add the full middleware stack and deployment checks.
Choose the right test boundary
| Layer | What to verify | What it proves |
|---|---|---|
| Application tests | Validation, authentication, business rules, and successful response bodies. | The handler behaves correctly when reached. |
| Focused protection tests | An allowed request, a rejected request, and a scoped exemption for one enabled check. | The middleware is registered and enforces the intended policy. |
| Full application integration | Actual middleware order, route manifest, headers, storage, and known model fixtures. | The checks work together with your framework and application. |
| Deployment tests | Real proxy/client identity, repeated requests across workers, shared state, and health probes. | The deployed topology behaves as intended. |
Keep a successful request beside each rejection assertion. A suite where every request fails can hide a broken registration, missing route, or overly broad policy. The examples below enable only rate limiting so an unrelated header or model check cannot cause an accidental pass.
Isolate state before writing assertions
- Use a fresh application or test client lifecycle and dedicated data, log, and model locations. CSV files and Redis state do not roll back with a database transaction.
- Give parallel workers separate cache namespaces, databases, or directories. Keep one namespace shared only for tests explicitly checking cross-worker behavior.
- Use fixed route budgets and an isolated manifest path. A discovered route policy can override the global limit, so test the effective route policy.
- Keep the client identity stable while testing a bucket. Use a new identity when testing independent clients.
- Use a known JSON model fixture for anomaly decisions. Do not train on live traffic as part of a deterministic application test.
- For window expiry tests, control the clock at the component boundary or use a dedicated timed integration test. Avoid long sleeps in ordinary tests.
Django's test runner creates test databases, but caches are not automatically isolated or cleared between tests. Use a dedicated test cache before clearing it. See Django's testing overview.
Write and run Flask protection tests
Create an empty working directory, install aiwaf[flask], and save this complete file as test_flask_protection.py. It builds a new application and temporary CSV directory for every test, explicitly selects memory counters, and pins both route budgets.
python -m pip install "aiwaf[flask]"
test_flask_protection.py
"""Run with: python -m unittest discover -s examples/testing -p test_flask_protection.py -v"""
from pathlib import Path
import tempfile
import unittest
from flask import Flask
from aiwaf.flask import AIWAF, aiwaf_exempt, aiwaf_exempt_from
class ProtectionTests(unittest.TestCase):
def setUp(self):
# A new application and state directory for every test.
directory = tempfile.TemporaryDirectory(prefix=".ci-protection-validation-", dir=Path.cwd())
self.addCleanup(directory.cleanup)
app = Flask(__name__)
app.config.update(
TESTING=True,
AIWAF_USE_CSV=True,
AIWAF_DATA_DIR=directory.name,
AIWAF_RATE_CACHE_BACKEND="memory",
AIWAF_PATH_MANIFEST=str(Path(directory.name) / "paths.json"),
AIWAF_RATE_WINDOW=60,
AIWAF_RATE_MAX=3,
AIWAF_RATE_FLOOD=100,
AIWAF_PATH_RULES=[
{"PREFIX": path, "RATE_LIMIT": {"WINDOW": 60, "MAX": 3, "FLOOD": 100}}
for path in ("/api/message", "/api/preview")
],
)
@app.get("/api/message")
def message():
return {"message": "Protected"}
@app.get("/health")
@aiwaf_exempt
def health():
return {"ok": True}
@app.get("/api/preview")
@aiwaf_exempt_from("rate_limit")
def preview():
return {"message": "Preview"}
AIWAF(app, middlewares=["rate_limit"])
self.client = app.test_client()
def test_protected_route_throttles(self):
for _ in range(3):
response = self.client.get("/api/message")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {"message": "Protected"})
response = self.client.get("/api/message")
self.assertEqual(response.status_code, 429)
self.assertEqual(response.json, {"error": "too_many_requests"})
def test_health_stays_available_after_throttling(self):
for _ in range(4):
self.client.get("/api/message")
response = self.client.get("/health")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {"ok": True})
def test_selective_exemption_bypasses_rate_limit(self):
self.assertEqual(
[self.client.get("/api/preview").status_code for _ in range(5)],
[200] * 5,
)
if __name__ == "__main__":
unittest.main()
python -m unittest discover -s . -p test_flask_protection.py -v
Expect three passing tests. The first verifies three successful responses followed by HTTP 429 and the exact too_many_requests JSON body. The others verify health availability after the protected route is throttled and that a selective rate-limit exemption permits repeated requests.
This fixture exercises AIWAF behavior independently. In your own project, also test your actual app factory and live routes so a mistake in production registration cannot be hidden by a separate fixture.
Write and run Django protection tests
In a separate empty directory, install aiwaf[django] and save both files below. The fixture uses Django's test client, ORM-backed AIWAF stores, an in-memory SQLite test database, and a dedicated local-memory cache. The URL configuration is the test module itself.
python -m pip install "aiwaf[django]"
django_test_settings.py
"""Settings for this standalone example, separate from your application's settings."""
from pathlib import Path
SECRET_KEY = "local-documentation-test-key"
INSTALLED_APPS = ["aiwaf.django"]
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Only for this standalone fixture: create AIWAF tables without generated migrations.
MIGRATION_MODULES = {"aiwaf": None}
ROOT_URLCONF = "test_django_protection"
ALLOWED_HOSTS = ["testserver"]
MIDDLEWARE = ["aiwaf.django.middleware.RateLimitMiddleware"]
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "documentation-protection-tests",
}
}
AIWAF_USE_CSV = False
AIWAF_PATH_MANIFEST = str(Path(__file__).with_name("unused-test-manifest.json"))
AIWAF_RATE_WINDOW = 60
AIWAF_RATE_MAX = 3
AIWAF_RATE_FLOOD = 100
AIWAF_EXEMPT_PATHS = ["/health"]
AIWAF_SETTINGS = {
"PATH_RULES": [
{"PREFIX": "/api/message", "RATE_LIMIT": {"WINDOW": 60, "MAX": 3, "FLOOD": 100}},
{"PREFIX": "/api/preview", "DISABLE": ["rate_limit"]},
]
}
test_django_protection.py
"""Run from this directory using django_test_settings; see /docs/testing."""
from django.core.cache import cache
from django.http import JsonResponse
from django.test import TestCase
from django.urls import path
def message(request):
return JsonResponse({"message": "Protected"})
def health(request):
return JsonResponse({"ok": True})
def preview(request):
return JsonResponse({"message": "Preview"})
urlpatterns = [
path("api/message", message),
path("health", health),
path("api/preview", preview),
]
class ProtectionTests(TestCase):
def setUp(self):
# django_test_settings points only at a dedicated local-memory test cache.
cache.clear()
self.addCleanup(cache.clear)
def test_protected_route_throttles(self):
for _ in range(3):
response = self.client.get("/api/message")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"message": "Protected"})
response = self.client.get("/api/message")
self.assertEqual(response.status_code, 429)
self.assertEqual(response.json(), {"error": "too_many_requests"})
def test_health_stays_available_after_throttling(self):
for _ in range(4):
self.client.get("/api/message")
response = self.client.get("/health")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"ok": True})
def test_selective_exemption_bypasses_rate_limit(self):
self.assertEqual(
[self.client.get("/api/preview").status_code for _ in range(5)],
[200] * 5,
)
python -m django test test_django_protection --settings=django_test_settings -v 2
Expect three passing tests and creation/destruction of the test database. TestCase isolates ORM changes; setUp() and cleanup clear only the dedicated local-memory cache. The standalone fixture disables AIWAF migrations to create its tables directly. In an existing project, keep your real migrations, include AIWAF in INSTALLED_APPS, and run your application's tests with its dedicated test settings.
Adapt these assertions to the routes from the Django setup guide. Keep another suite using your real ROOT_URLCONF and complete MIDDLEWARE list: this focused fixture intentionally exercises only the rate limiter.
This fixture uses AIWAF_EXEMPT_PATHS for health and a PATH_RULES entry disabling only rate_limit for preview. In the current source, the rate limiter runs before Django resolves the view, while decorator detection reads request.resolver_match. A selective decorator alone did not bypass the limiter in this full test-client request. Use the explicit path policy for this case, and test decorators through your actual middleware stack.
Apply the same assertions to other frameworks
The status sequence and test structure above belong to these Python fixtures. For other runtimes, use that runtime's configuration names, response contract, and policy precedence.
- FastAPI: use its test client against the actual ASGI application. Keep the client in a context manager when startup/shutdown initializes resources. Give each test its own policy/state configuration; see the FastAPI setup guide.
- Node.js: create a fresh app and AIWAF middleware instance per test. Use the framework's injection facility or an HTTP client against an ephemeral local port. Verify a normal response, configured throttle/block response, and exemption. See Node.js operations.
- Java: test the engine decision independently, then exercise the registered Spring filter or Servlet filter through your framework test harness. Keep shared Redis integration tests separate from memory-backed tests. See Java operations.
Expand to the full protection stack
- Headers and parsing: pair valid browser/API requests with requests violating your configured requirements. Assert the rejection reason or body as well as status.
- Exemptions: verify that the intended check is bypassed and that another enabled check still runs. The rate-only examples prove the bypass, but do not prove other checks remain active.
- Flood and reputation: distinguish a soft throttle from a hard block. Verify persistent state and later requests separately using disposable storage.
- Models: cover a known normal sample, a known anomaly, and missing/invalid model handling for your configured runtime. Keep training tests separate from inference tests.
- Route discovery: generate the manifest from the test application's live route table, verify the protected route is present, and repeat the behavior assertions with the manifest loaded.
- Observation: check the expected log record and response status, including denied requests, without relying on a production log file.
For Redis or multiple workers, send a combined request sequence across workers using the same client identity and bucket. Assert the aggregate budget, expiry behavior, and configured backend-failure behavior. A single-process in-memory test cannot establish those guarantees.
Run the examples in continuous integration
From the AIWAF website checkout, the checked-in examples run with these commands. Install the relevant framework extras first.
python -m unittest discover -s examples/testing -p test_flask_protection.py -v
cd examples/testing
python -m django test test_django_protection --settings=django_test_settings -v 2
Run application and focused protection tests on each change. Run shared-backend and deployed-topology checks in a separate integration job with disposable services. Save failure output and effective policy configuration so failures can be reproduced locally.
Diagnose a failing protection test
| Failure | Check next |
|---|---|
| Expected 429, got 200 | Middleware registration, route/IP exemptions, effective manifest rules, changing client identity, and counters split across workers. |
| First request already rejected | Leaked cache/blocklist state, missing required headers, route mismatch, or another enabled check. |
| Passes alone, fails in the suite | Shared storage/cache keys, module-level app reuse, test order, and model/manifest files left by another test. |
| Expected JSON, got HTML or a redirect | Wrong URL, trailing-slash routing, framework exception handling, or a response produced by the application/proxy. |
Use the Redis-enabled multi-runtime sandbox for pinned GitHub source builds and cross-runtime checks. Follow the first-run tutorial, run SQL, TLS, outage and dependency checks, and review OWASP coverage and limits before interpreting results.
Continue with troubleshooting, configuration reference, and state ownership across workers.
Legacy native CSV and training regression checks
python aiwaf_test.py --python-only --coverage
python manage.py test tests.django
python scripts/check_native_csv_runtime.py
python scripts/check_csv_release_concurrency.py
Django regression tests belong in tests/django as Django test cases discovered by manage.py test. Core and Flask/FastAPI tests use their corresponding pytest suites. The release runner also checks canonical source-to-test layout, line/branch coverage, and newly uncovered callables. Passing a coverage percentage alone does not satisfy the callable baseline gate.
Test native/fallback parity with explicit adapter settings or native_override(False): an environment-only toggle can be overridden by already configured Django settings in a combined suite. Exercise invalid/quoted/gzip logs, duplicate keyword updates, expiry, cross-worker changes, model parity, and close/reopen after rotation. Use isolated temporary files and close writer handles before cleanup.
Native pipeline validation
Test each framework adapter with normal routes and SQL/PHP probes, bounded JSON/form inspection, decorator precedence, unknown routes, model activation, clock adjustments, opaque context ownership, single finalization and log output. Test CSV timing across engines/workers and memory isolation/restart behavior separately. Streaming ASGI logs finalize at response start.
Release validation includes tests/core/test_csv_pipeline.py, framework test_csv_pipeline.py suites and installed-wheel rust/tests/test_csv_pipeline_api.py. Compare full protected requests with a matched framework-only baseline.
Native Node regression checks
Native tests exercise real WASM (no mock fallback), parsed SQL/JSON escapes and limits, safe Unicode, header caps, timing/isolation, flood expiry, persisted blocks/learned keywords, write errors, model activation, streaming and engine-owned finalization. Transport tests cover Express, Fastify, Hapi, Koa, Nest, Next and Adonis; unknown paths must be tested alongside registered handlers.
Read the native lifecycle, configuration, deployment and measurements, and migration.
Verify the current Python and Node source
The source targets are Python 1.1.4 with Rust/Python 0.2.3 and Node 1.1.2 with WASM 0.2.2. Run the framework-specific suites with the same dependencies and configuration as the deployed application.
# From the AIWAF monorepo root, with test dependencies installed.
python -m pytest tests/core tests/flask tests/fast tests/test_cli.py tests/test_cli_init_detection.py
python manage.py test tests.django
# Reproduce the committed Node dependency tree.
npm ci --prefix js
npm audit --prefix js
# Build the CommonJS native fixture used by Jest.
wasm-pack build rust/crates/aiwaf_wasm --release --target nodejs --out-dir pkg-node-test
export AIWAF_NATIVE_TEST_MODULE="$PWD/rust/crates/aiwaf_wasm/pkg-node-test/aiwaf_wasm.js"
npm run test:coverage --prefix js
npm run test:wasm --prefix js
npm run test:native --prefix jsIn PowerShell, replace export with:
$env:AIWAF_NATIVE_TEST_MODULE = (Resolve-Path 'rust/crates/aiwaf_wasm/pkg-node-test/aiwaf_wasm.js').PathThe Node release workflow builds this fixture for Jest and checks the actual installed WASM engine separately. Framework test dependencies include patched Fastify 5 and NestJS 11; Jest 30 uses a cookie-module transform for Node 22 compatibility. Audit development dependencies as well as production dependencies.
For Python CSV upgrades, verify temporary and permanent blocks, expiry metadata, keywords and counts, IP/pattern exemptions, country lists, path exemptions, and UTF-8 data. Restart after removing a migrated block to verify that the old source is not replayed. Test malformed input and interrupted migration using an isolated directory. See migration semantics.
For native Node mode, verify initialization failure, bounded parsed bodies, unknown-route checks, actual model inference counters, finish/close finalization, persistence on reconstruction, and worker isolation. Native Node files and legacy CLI stores have different schemas. See the current validation results.
October 2026 release upgrade
The prepared source versions are Python 1.1.6, Rust/Python 0.2.5, Node.js 1.1.4, WASM 0.2.4, and Java 1.3.4. These are source targets; verify registry availability before installing. Python requires aiwaf-rust>=0.2.5, including the TrainingEvidence API. Node offline training requires the matching WASM receipt API.
- Back up policies, logs, models, and training evidence. Use the same absolute data and evidence directories for application workers and CLI trainers.
- Install the matching native companion before upgrading its host package: Rust before Python, WASM before Node. Preserve
training_evidence.jsonandnode_training_evidence.jsonacross expiry, unblocking, restarts, and log rotation. - Stop older file writers and restart all workers on the new code. Mixing old unlocked writers with new writers can still lose updates.
- In staging, train twice on identical logs, then verify handled evidence does not recreate a removed or expired block. Add genuinely fresh requests and confirm they must independently satisfy the enforcement threshold.
- Verify that listing and expiry checks preserve concurrently refreshed blocks and unrelated IPs. Check the raw expiry and backend location if entries disappear unexpectedly.
Java CSV and file storage
Java 1.3.4 reloads current storage under a shared JVM and OS sidecar lock for each operation. Reads filter expiry without rewriting disk; mutations preserve other instances' updates and report persistence failures. This adds file I/O. Java model training still does not add blocklist entries. Rate buckets and other per-engine memory state retain their existing isolation.
Lock recovery
Python training uses an OS file lock; Java CSV/file OS locks also release on process exit. Keep their stable sidecar lock files in place while workers run. Node uses lock directories for training and legacy CSV mutations; a crash can leave them behind. Confirm the owning process has stopped before removing a Node lock directory. Do not delete training receipts to clear a lock: doing so loses handled-evidence history.
Release checks
Local validation passed 247 Node tests across 67 suites with the coverage gate, 215 Java tests with 3 skips, and 38 Rust/WASM native tests. After the native dependency bump, 11 affected Python packaging tests passed through the appropriate Django and pytest runners. These checks do not replace full release CI, dependency audits, signing, or operating-system wheel builds. Build Linux/macOS Rust wheels using the release matrix; the local bundle contains a Windows x86-64 wheel.
For release maintainers: publish the WASM companion, refresh the Node lockfile from the registry to record its actual integrity, then run npm ci and the native integration checks. Do not substitute an older native dependency to satisfy a stale packaging test. See the changelog.
