Testing Arcjet
Unlike network-based security solutions, Arcjet can run locally, in staging and in production. This makes it easy to test your security rules before deploying them.
You can also automate testing against your Arcjet-projected routes using standard testing libraries.
Testing using Newman
Newman is an open source command-line collection runner for Postman. It allows you to define requests in Postman format and run them from the command line without requiring Postman itself.
You can find our full example of how to test Arcjet with Express and Newman on GitHub. The key files are:
index.js
is a simple Express server that uses Arcjet to protect several routes. This is the application being tested.tests/api.test.js
is the test runner which loads the test definitions, executes them with Newman, and then asserts the expected results.- The
tests/bots.json
,tests/high-rate-limit.json
, andtests/low-rate-limit.json
files define the test requests in Postman Collection format.
The example
README
explains how to start the test application and then execute the tests using
Newman.
To adapt these to your own application you would modify the tests/*.json
files
to point to your application endpoints, and then run them using the Newman CLI.
This allows you to automate testing and run multiple iterations e.g. to test a
rate limit.
Testing triggering rules
Arcjet runs the same in production as locally. This ensures that the behavior you see in development is the same as in production. However, there are ways to trigger the different rules.
- Shield: Send 5 requests with the special header
x-arcjet-suspicious: true
to trigger the shield rule on the next request. - Rate limiting: Make more requests than the rate limit allows.
- Bot protection: Bot detection uses multiple heuristics so the quickest way
to get a bot detection
DENY
response is to make a request that is always considered a bot. With a deny rule set toCURL
you should see aDENY
response if you make a request using thecurl
command. See Identifying Bots for more information. - Email validation: Use an email address that has invalid syntax or does not match any other rules you have configured e.g. has no MX records or if from a disposable email service.
Sampling
Arcjet is designed to run on every request. When testing your rules, you can use
DRY_RUN
mode to see how your rules would behave without actually enforcing
them. The result will be logged, but all requests will be allowed. This is useful
for testing your rules without affecting your users.
You can also write a simple sampling function to set the Arcjet rules using
withRule
based on a desired sample rate. This allows you to test your rules on
a subset of your traffic.
This Next.js middleware example shows how to trigger Arcjet Shield and bot detection rules in live mode on 10% of your traffic.
import arcjet, { detectBot, shield } from "@arcjet/next";import { NextRequest, NextResponse } from "next/server";
export const config = { // matcher tells Next.js which routes to run the middleware on. This runs // the middleware on all routes except for static assets. matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],};
const sampleRate = 0.1; // 10% of requests
const aj = arcjet({ key: process.env.ARCJET_KEY!, // You could include one or more base rules to apply to all requests rules: [],});
function shouldSampleRequest(sampleRate: number) { // sampleRate should be between 0 and 1, e.g., 0.1 for 10%, 0.5 for 50% return Math.random() < sampleRate;}
// Shield and bot rules will be configured with live mode if the request is// sampled, otherwise only Shield will be configured with dry run modefunction sampleSecurity() { if (shouldSampleRequest(sampleRate)) { console.log("Rule is LIVE"); return aj .withRule( shield( { mode: "LIVE" }, // will block requests if triggered ), ) .withRule( detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ); } else { console.log("Rule is DRY_RUN"); return aj.withRule( shield({ mode: "DRY_RUN", // Only logs the result }), ); }}
export default async function middleware(request: NextRequest) { const decision = await sampleSecurity().protect(request);
if (decision.isDenied()) { if (decision.reason.isBot()) { return NextResponse.json({ error: "You are a bot" }, { status: 403 }); } else if (decision.reason.isShield()) { return NextResponse.json({ error: "Shields up!" }, { status: 403 }); } else { return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); } } else { return NextResponse.next(); }}
import arcjet, { detectBot, shield } from "@arcjet/next";import { NextResponse } from "next/server";
export const config = { // matcher tells Next.js which routes to run the middleware on. This runs // the middleware on all routes except for static assets. matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],};
const sampleRate = 0.1; // 10% of requests
const aj = arcjet({ key: process.env.ARCJET_KEY, // You could include one or more base rules to apply to all requests rules: [],});
function shouldSampleRequest(sampleRate) { // sampleRate should be between 0 and 1, e.g., 0.1 for 10%, 0.5 for 50% return Math.random() < sampleRate;}
// Shield and bot rules will be configured with live mode if the request is// sampled, otherwise only Shield will be configured with dry run modefunction sampleSecurity() { if (shouldSampleRequest(sampleRate)) { console.log("Rule is LIVE"); return aj .withRule( shield( { mode: "LIVE" }, // will block requests if triggered ), ) .withRule( detectBot({ mode: "LIVE", allow: [], // "allow none" will block all detected bots }), ); } else { console.log("Rule is DRY_RUN"); return aj.withRule( shield({ mode: "DRY_RUN", // Only logs the result }), ); }}
export default async function middleware(request) { const decision = await sampleSecurity().protect(request);
if (decision.isDenied()) { if (decision.reason.isBot()) { return NextResponse.json({ error: "You are a bot" }, { status: 403 }); } else if (decision.reason.isShield()) { return NextResponse.json({ error: "Shields up!" }, { status: 403 }); } else { return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); } } else { return NextResponse.next(); }}