@odatnurd/cf-aegis
v0.1.5
Published
Simple Aegis Test Helpers
Readme
Cloudflare Aegis Test Wrapper
This package includes a set of helpers to facilitate testing projects with the Aegis test runner and a Cloudflare Worker via Miniflare.
To use these utilities, you must install the required peer dependencies into
your own project's devDependencies if you have not already done so.
pnpm add -D @axel669/aegis miniflare json5 smol-tomlThe @odatnurd/cf-aegis module exports the following functions:
export async function aegisSetup(ctx, inputConfig, workerMocks = {}) {}An async function to be invoked from the setup hook in your Aegis config file.
This uses the provided configuration to create a new Miniflare instance.
inputConfig can be one of:
- A wrangler configuration file (in
TOMLorJSONCformat) - An object with the structure of a parsed Wrangler configuration file
Using the configuration, a Miniflare worker is configured, and the passed in
ctx object has the following fields injected into it:
worker: the actual Miniflare worker instanceenv: an object that contains all of the configured bindings, as they would be presented to you in a workerisServerListening: a boolean that indicates if the configuration contained a port, in which case a development service will be startedserverBaseUrl: ifisServerListeningistrue, this key will be added and gives you the full base URL for all routes in the worker, including the configured port.fetch: ifisServerListeningistrue, this is a wrapped function on thefetch()method that allows fetching from URL fragments without needing to know the port (e.gctx.fetch('/api/thing'))
The configuration currently supports:
D1databasesR2storageDurable ObjectsKVQueuesStatic Assets(see below)Service Bindings
When using service bindings, all services will, by default, be mocked to
include a fetch handler that generates an error response saying the service
is not mocked. This facilitates testing in cases where the bound services are
not needed.
Optionally, the workerMocks object can be populated; the keys are the names
of the service bindings, and the values are objects that contain either a
script or scriptPath field to specify the body of the service.
⚠️ Warning Currently, if a worker is configured to contain both static assets and a worker, the static assets will override all worker resources, such that any request to the worker that is not a static asset will return a
404.This bug will be addressed in a follow-up version.
export async function aegisTeardown(ctx) {}An async function to be called from the teardown hook in your Aegis config
that aligns with the setup hook. It safely disposes of the Miniflare instance
created by aegisSetup and removes from ctx all of the values added to it by
aegisSetup.
export function initializeCustomChecks() {}A function that registers several custom checks with Aegis to simplify testing. This augments the internal tests that are already available in Aegis.
.isArray($): Checks if a value is an array..isNotArray($): Checks if a value is not an array..isObject($): Checks if a value is a plain object..isNotObject($): Checks if a value is not a plain object..keyCount($, count): Checks if an object has an exact number of keys..isFunction($): A shortcut to check if a value is an instance ofFunction..isNotFunction($): A shortcut to check if a value is not an instance ofFunction..deepEquals($, expected): Does a deep equality check (e.g. on an array or an object) to ensure that they are completely equal; can also be used on other values as well..notDeepEquals($, expected): Does a deep equality check as above, but with a check for inequality instead.
The @odatnurd/cf-aegis/config module exports the following function:
export function loadWranglerConfig(filename) {}Given the path to a Wrangler configuration file (in either TOML or JSONC
format), load the configuration and return the object back.
This can be useful in cases where you want tests to be configured via the same Wrangler config that you use to develop and deploy your application, but you need to amend it prior to testing in some fashion.
Configuration Examples
You can import the helper functions into your aegis.config.js file to easily
set up a test environment. The aegisSetup() function accepts either an object
that is a structured Wrangler config object or the name of a wrangler config
file, and will configure Miniflare appropriately.
Example aegis.config.js:
This example configures a worker to include a binding for a database and an R2
bucket; they appear in the ctx as ctx.env.DB and ctx.env.BUCKET
respectively.
import { initializeCustomChecks, aegisSetup, aegisTeardown } from '@odatnurd/cf-aegis';
initializeCustomChecks();
export const config = {
files: [
"test/**/*.test.js",
],
hooks: {
async setup(ctx) {
await aegisSetup(ctx, {
d1_databases: [{
binding: 'DB',
database_name: 'test-db',
database_id: 'test-db-id'
}],
r2_buckets: [{
binding: 'BUCKET',
bucket_name: 'test-bucket'
}]
});
},
async teardown(ctx) {
await aegisTeardown(ctx);
},
},
failAction: "afterSection",
}The format of the configuration object is that of a loaded Wrangler configuration file in either of the supported formats. Optionally, if you already have a config file to use, you can point the function at it and it will load the config for you.
wrangler.toml
name = "aegis-test"
main = "src/index.js"
compatibility_date = "2024-04-05"
[[d1_databases]]
binding = "DB"
database_name = "test-db"
database_id = "test-db-id"
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "test-bucket"or if you prefer to not use Toms Obnoxious Malformed Language, JSONC is
also supported:
{
"name": "aegis-test",
"main": "src/index.js",
"compatibility_date": "2024-04-05",
"d1_databases": [
{
"binding": "DB",
"database_name": "test-db",
"database_id": "test-db-id"
}
],
"r2_buckets": [
{
"binding": "BUCKET",
"bucket_name": "test-bucket"
}
]
}Then the example becomes:
import { initializeCustomChecks, aegisSetup, aegisTeardown } from '@odatnurd/cf-aegis';
initializeCustomChecks();
export const config = {
files: [
"test/**/*.test.js",
],
hooks: {
async setup(ctx) {
// You can use either 'wrangler.toml' or 'wrangler.jsonc' here
await aegisSetup(ctx, './wrangler.toml');
},
async teardown(ctx) {
await aegisTeardown(ctx);
},
},
failAction: "afterSection",
}Mocking Service Bindings
Should your configuration require that you bind to another worker, by default the binding will be set up such that all requests to that worker generate an error response telling you that it is not mocked; this is considered the most common use case, where you would be reusing an existing configuration but testing code paths that don't touch external services.
If you require such services to be implemented, you can tell aegisSetup() what
services to mock and how the mocks work. This can be done via an inline script
as in this example or by providing a scriptPath instead to point at a file,
should the mock be more complex.
import { initializeCustomChecks, aegisSetup, aegisTeardown } from '@odatnurd/cf-aegis';
initializeCustomChecks();
export const config = {
files: [
"test/**/*.test.js",
],
hooks: {
async setup(ctx) {
// Define the inline configuration for the main worker
const config = {
services: [{
// The name of the binding; e.g. ctx.env.LOG_SERVICE
binding: 'LOG_SERVICE',
service: 'log-service'
}]
};
const workerMocks = {
'log-service': {
// For a more complex mock, you can use scriptPath: './path/to/mock-worker.js'
script: `export default {
fetch(request) {
return new Response('Success', { status: 200 });
}
}`
}
};
await aegisSetup(ctx, config, workerMocks);
},
async teardown(ctx) {
await aegisTeardown(ctx);
},
},
failAction: "afterSection",
}