Test AWS CloudFront Functions locally and in CI without deploying CloudFront resources or configuring AWS credentials.
cff-test is an offline test runner for viewer request and viewer response functions using the CloudFront Functions JavaScript runtime 2.0.
- No AWS account or credentials required
- No CloudFront Function resource required
- Works in GitHub Actions and other CI environments
- Validates CloudFront Functions runtime compatibility
- Supports CloudFront KeyValueStore fixtures
QuickJS and the supported built-in modules are embedded in the executable, so Node.js is not required at runtime.
Warning
The capability restrictions in cff-test are intended for compatibility testing of trusted function code. They are not a security sandbox for safely executing untrusted JavaScript.
Even short CloudFront Functions directly affect viewer requests and viewer responses during content delivery. However, deploying every change to AWS for testing slows down feedback and requires both development environments and CI systems to have AWS access.
cff-test was created so that function code, input events, and expected results can be kept together in a repository and tested locally or in CI just like regular application code. It aims to detect compatibility violations and unintended behavior at the pull request stage, shortening the feedback loop before deployment to AWS.
It does not replace final verification on AWS. Instead, it provides a fast and reproducible test layer for repeatedly validating day-to-day changes.
cff-test |
AWS test-function |
Node.js unit tests | |
|---|---|---|---|
| AWS account and credentials required | No | Yes | No |
| CloudFront Function resource required | No | Yes | No |
| Works offline | Yes | No | Yes |
| Runtime compatibility checks | Yes | Runs on the native runtime | No |
| KeyValueStore testing | Local fixture | Associated KVS | Mock required |
| GitHub Actions support | Yes | Yes | Yes |
AWS test-function runs an existing CloudFront Function on AWS. It is useful for verification on the native runtime, but it requires a CloudFront Function resource, AWS credentials, and network access.
cff-test is intended for the earlier stage, before creating or updating AWS resources:
pull request -> cff-test -> merge -> update AWS function -> AWS test-function -> publish
Use cff-test for fast, repeatable local and CI feedback, then use AWS testing when you need to verify behavior on the native runtime.
- Diagnoses syntax, globals, modules, and members unavailable in CloudFront Functions runtime 2.0 before execution
- Validates viewer request and viewer response events and handler return values
- Prints function return values as formatted JSON
- Displays differences from expected values by JSON Pointer
- Supports
crypto,querystring, and thecloudfrontmodule backed by a local KVS fixture - Reproducibly freezes
Datewith--now-ms - Runs multiple functions and test cases from one suite JSON file
- Requires no AWS credentials, network connection, or Node.js
See the compatibility documentation for details about the supported scope.
GitHub Releases provides the following executables for each tag. For example, <TAG> in the filenames may be v0.1.0.
| Platform | Release asset |
|---|---|
| Linux x86 (32-bit) | cff-test-<TAG>-i686-unknown-linux-gnu |
| Linux amd64 | cff-test-<TAG>-x86_64-unknown-linux-gnu |
| Linux arm64 | cff-test-<TAG>-aarch64-unknown-linux-gnu |
| macOS Intel | cff-test-<TAG>-x86_64-apple-darwin |
| macOS Apple Silicon | cff-test-<TAG>-aarch64-apple-darwin |
Make the downloaded file executable if necessary and place it in a directory on your PATH. For example, on Linux amd64:
chmod +x cff-test-v0.1.0-x86_64-unknown-linux-gnu
mkdir -p "$HOME/.local/bin"
install -m 0755 cff-test-v0.1.0-x86_64-unknown-linux-gnu "$HOME/.local/bin/cff-test"Each release asset is a single executable, not an archive. The macOS binaries are not code-signed or notarized.
You need a Rust toolchain and the C/C++ build toolchain for your target platform. To use Rust 1.96.0, the same version used for release builds:
git clone https://github.com/kaminchu/cff-test.git
cd cff-test
rustup toolchain install 1.96.0 --profile minimal
cargo +1.96.0 build --locked --releaseThe generated executable is target/release/cff-test.
This example uses a viewer request function that rewrites a URI to demonstrate static checking, execution, and comparison against an expected value.
function.js:
function handler(event) {
event.request.uri = "/rewritten";
return event.request;
}event.json:
{
"version": "1.0",
"context": { "eventType": "viewer-request" },
"viewer": { "ip": "198.51.100.11" },
"request": {
"method": "GET",
"uri": "/original",
"querystring": {},
"headers": { "host": { "value": "example.com" } },
"cookies": {}
}
}expected.json:
{
"method": "GET",
"uri": "/rewritten",
"querystring": {},
"headers": { "host": { "value": "example.com" } },
"cookies": {}
}# Statically check compatibility with runtime 2.0
cff-test check function.js
# Run the function and print its return value to standard output
cff-test run function.js --event event.json
# Compare the return value with expected.json
cff-test test function.js --event event.json --expected expected.jsonOn success, check prints OK: function.js is compatible with cloudfront-js-2.0, and test prints PASS: function.js.
The setup action installs the appropriate release binary for the runner and adds cff-test to PATH. It supports Linux and macOS runners on x64 and arm64, as well as Linux x86. No AWS credentials are required.
.github/workflows/cloudfront-functions.yml:
name: Test CloudFront Functions
on:
push:
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup cff-test
uses: kaminchu/cff-test/setup@v1
- name: Test CloudFront Function
run: |
cff-test test \
cloudfront/function.js \
--event cloudfront/event.json \
--expected cloudfront/expected.json
- name: Test CloudFront Function suite
run: cff-test test --suite cloudfront/cff-test.jsonWhen the action is referenced by a major tag such as v1, it installs the newest release in that major version. To pin the CLI independently, set version to a full release tag:
- uses: kaminchu/cff-test/setup@v1
with:
version: v1.0.0cff-test test exits with code 0 on success and 1 when it detects a compatibility violation or a difference from the expected value, so the result can be used directly as the GitHub Actions job status.
The following example tests CloudFront Functions in GitLab CI using a release binary on a Linux amd64 runner. Replace vX.Y.Z with the release tag you want to use, and adjust the file paths under cloudfront/ to match your repository layout.
.gitlab-ci.yml:
variables:
CFF_TEST_VERSION: "vX.Y.Z"
cloudfront-functions:
image: ubuntu:24.04
before_script:
- apt-get update
- apt-get install --yes --no-install-recommends ca-certificates curl
- |
asset="cff-test-${CFF_TEST_VERSION}-x86_64-unknown-linux-gnu"
curl --fail --location --silent --show-error \
--output ./cff-test \
"https://github.com/kaminchu/cff-test/releases/download/${CFF_TEST_VERSION}/${asset}"
chmod +x ./cff-test
script:
- >-
./cff-test test
cloudfront/function.js
--event cloudfront/event.json
--expected cloudfront/expected.jsoncff-test test exits with code 0 on success and 1 when it detects a compatibility violation or a difference from the expected value, so the result can be used directly as the GitLab CI job status. When using a Linux arm64 runner or another platform, change the asset name to match the list of release assets.
cff-test check <FUNCTION>
cff-test run <FUNCTION> --event <EVENT> [--kvs <KVS>] [--now-ms <MILLISECONDS>]
cff-test test <FUNCTION> --event <EVENT> --expected <EXPECTED> [--kvs <KVS>] [--now-ms <MILLISECONDS>]
cff-test test --suite <SUITE>
cff-test <FUNCTION> --event <EVENT> --expected <EXPECTED> [--kvs <KVS>] [--now-ms <MILLISECONDS>]
| Command | Behavior |
|---|---|
check |
Statically checks the function code without executing an event. |
run |
Runs the function with an event and prints the returned JSON to standard output. |
test |
Compares the function's return value with the expected JSON. |
test --suite |
Runs the functions and cases defined in a suite JSON file. |
| Command omitted | Behaves the same as test. |
FUNCTION is a UTF-8 JavaScript file, while EVENT and EXPECTED are UTF-8 JSON files. Specify CloudFront Functions event version 1.0 in EVENT, and the request or response returned by the handler in EXPECTED.
Because console.log() output, diagnostics, and errors are written to standard error, the standard output from run can be piped to another command as JSON.
Use cff-test test --suite <SUITE> to run multiple functions and cases in declaration order:
{
"functions": [
{
"name": "rewrite",
"function": "functions/rewrite.js",
"cases": [
{
"name": "normal request",
"event": "events/request.json",
"expected": "expected/rewrite.json"
},
{
"name": "inline request",
"event": {
"version": "1.0",
"context": { "eventType": "viewer-request" },
"viewer": { "ip": "198.51.100.11" },
"request": {
"method": "GET",
"uri": "/original",
"querystring": {},
"headers": { "host": { "value": "example.com" } },
"cookies": {}
}
},
"expected": "expected/rewrite.json",
"skip": false
}
]
}
]
}String values in event, expected, and kvs are always paths to JSON files. Other JSON values are used inline, including null. Relative paths are resolved from the suite file's directory, regardless of the current working directory. kvs, now_ms, and skip apply to each case independently; skip: true validates the case inputs but does not execute the case.
For example, an inline KVS and a fixed time can be specified as follows:
{
"name": "KVS and time",
"event": "events/request.json",
"expected": "expected/kvs.json",
"kvs": {
"values": {
"setting": { "format": "string", "value": "enabled" }
},
"meta": {
"creationDateTime": "2024-01-01T00:00:00.000Z",
"lastUpdatedDateTime": "2024-01-02T00:00:00.000Z",
"keyCount": 1
}
},
"now_ms": 0
}| Code | Meaning |
|---|---|
0 |
The check, execution, or comparison succeeded |
1 |
Compatibility, event, execution, return value, or comparison error |
2 |
CLI usage, file I/O, or JSON syntax error |
When --now-ms is given a time in milliseconds since the Unix epoch, Date is frozen at that time for the duration of the invocation. Use this option to reproducibly test time-dependent functions.
cff-test test function.js --event event.json --expected expected.json --now-ms 0Pass --kvs a UTF-8 JSON file in the following format. The value of bytes must use standard Base64 encoding.
{
"values": {
"plain": { "format": "string", "value": "hello" },
"settings": { "format": "json", "value": { "enabled": true } },
"binary": { "format": "bytes", "value": "AQID" }
},
"meta": {
"creationDateTime": "2024-01-01T00:00:00.000Z",
"lastUpdatedDateTime": "2024-01-02T00:00:00.000Z",
"keyCount": 3
}
}Read the fixture from the function through the cloudfront module.
import cf from "cloudfront";
const kvs = cf.kvs();
async function handler(event) {
event.request.headers["x-feature"] = { value: await kvs.get("plain") };
return event.request;
}cff-test run function.js --event event.json --kvs kvs.jsonKVS fixtures are read-only. The get(), exists(), and meta() methods are supported.
cff-test targets viewer request and viewer response functions on runtime 2.0 using event version 1.0. Supported modules are crypto, querystring, and cloudfront backed by a local fixture. Function code must be no larger than 10 KiB in UTF-8 bytes.
QuickJS execution is subject to local safety limits: 64 MiB of memory, approximately one second of execution time, and a 512 KiB stack. However, cff-test does not reproduce AWS ComputeUtilization, the performance or internal behavior of the production engine, or identical error messages.
Runtime 1.0, Connection Functions, Lambda@Edge, cloudfront.cwt, networking, file system access, and npm or local file modules are outside the supported scope. See docs/compatibility.md for the complete list, including the built-in global allowlist, JavaScript syntax support, and known differences.
Run the full test suite locally:
cargo test --lockedTo build a release binary:
cargo build --locked --release