MasterController
Testing
Boot the real app in-process and drive it with a client that remembers cookies.
master.createTestClient() is the WebApplicationFactory / TestServer + HttpClient pairing: it runs setupServer → startMVC(app) → start → listen(127.0.0.1:0) once and returns a TestClient. Requests go through the entire pipeline — middleware, auth, binding, caching, controllers — on an ephemeral port with no network exposure. Works with node --test out of the box.
A first test#
test/account.test.js
// test/account.test.js
import { test, after } from 'node:test';
import assert from 'node:assert/strict';
import master from 'mastercontroller';
let client;
after(() => client.close());
test('login then read profile', async () => {
client = await master.createTestClient({
root: new URL('..', import.meta.url).pathname, // the app root (config/, app/)
app: 'app',
environment: 'test',
configure: (m) => { m.auth.addCookie('cookie', { secret: process.env.TEST_COOKIE_SECRET }); },
});
const login = await client.post('/login', { body: { email: 'a@b.co', password: 'pw' } }); // Set-Cookie captured
assert.equal(login.status, 200);
const me = await client.get('/me'); // Cookie sent automatically
assert.equal(me.json().email, 'a@b.co');
const admin = client.withBearer(token); // separate identity, same jar
const denied = await admin.get('/admin');
assert.equal(denied.status, 403);
});Run it with node --test. createTestClient is idempotent on an already-started app, so several test files can share one boot; call close() when you are done.
Client API#
javascript
const client = await master.createTestClient({ root, app: 'app', environment: 'test', configure });
// requests — JSON bodies, query objects, extra headers
const res = await client.get('/items', { query: { page: 2 }, headers: { 'x-api-version': '2.0' } });
await client.post('/items', { body: { name: 'A1' } });
await client.put('/items/1', { body: { name: 'A2' } });
await client.patch('/items/1', { body: { name: 'A3' } });
await client.delete('/items/1');
await client.head('/items');
// responses
res.status; // 200
res.ok; // true
res.headers; // fetch Headers — res.headers.get('location') / res.header('location')
res.text; // raw body (already read)
res.json(); // parsed lazily; null if not JSON
// identities and state
const asAdmin = client.withBearer(token); // new client, same cookie jar
const withTenant = client.withHeaders({ 'x-tenant': 'acme' });
client.withCookie('.Culture', 'c=de|uic=de');
client.cookies; client.clearCookies();
client.baseUrl; // http://127.0.0.1:<ephemeral> for raw fetch()
await client.close();- Options:
root(setsmaster.root),app('app'by default;falseskips controller discovery),environment('test'),configure(master, server)— runs aftersetupServer()and beforestartMVC(), the place for schemes, services, minimal routes. - Bodies: objects are JSON-encoded with
content-type: application/json; pass aBufferorURLSearchParamsfor anything else.accept: application/jsonis sent by default (json: falseto omit). - Cookies:
Set-Cookieis captured into a jar shared by everywith*()client; expired cookies are dropped. - Redirects are not followed (
redirect: 'manual'), so you can assert onLocation.
Minimal-API hosts#
app: false boots without app/ — handy for unit-style tests of minimal APIs and middleware.
javascript
// Pure minimal-API host — no controller discovery
const client = await master.createTestClient({
app: false,
configure: (m) => {
m.map.get('/ping', () => ({ pong: true }), { allowAnonymous: true });
},
});
assert.deepEqual((await client.get('/ping')).json(), { pong: true });Asserting on logs#
javascript
import master, { memoryProvider } from 'mastercontroller';
const mem = memoryProvider();
const client = await master.createTestClient({ root, configure: (m) => m.logging.addProvider(mem) });
await client.get('/orders/1');
assert.ok(mem.entries.some((e) => e.category === 'Request' && e.scope?.path === '/orders/1')); Request-scoped services in tests
Because each request gets its own DI scope, a test that registers
m.addScoped('db', AppContext) in configure exercises exactly the production lifetime — see Dependency Injection. Point the test environment at a throwaway database in config/appsettings.test.json. Named exports
import { TestClient, createTestClient } from 'mastercontroller' — createTestClient(master, opts) takes the instance explicitly, which is useful with new MasterControl() when you want isolated apps per test file.