> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/development/testing.md).

# Testing

MyIP uses the **Node.js built-in test runner**. There is no Jest, no Vitest, no test framework dependency at all.

```bash
pnpm test     # node --test tests/*.test.js
```

Every spec lives in `tests/`, flat, named `<subject>.test.js`. Composable specs are prefixed: `tests/composable-status-tone.test.js`, `tests/composable-refresh-orchestrator.test.js`.

To run one file while iterating:

```bash
node --test tests/guards.test.js
```

## Spec shape

`node:test` for structure, `node:assert/strict` for assertions. No custom helpers beyond what a file needs:

{% code title="tests/composable-status-tone.test.js" %}

```js
// Tests for ipFieldTone — the unified "status string → tone" mapping that
// WebRtcTest / DnsLeaksTest / RuleTest / ConnectivityTest all use.

import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import { ipFieldTone } from '../frontend/composables/use-status-tone.js';

describe('ipFieldTone()', () => {
  it('returns "wait" when value equals the wait label', () => {
    assert.equal(ipFieldTone('Waiting', { waitLabels: 'Waiting', errorLabels: 'Error' }), 'wait');
  });
});
```

{% endcode %}

Like every other file in the project, a spec opens with a header comment stating what it covers.

## What gets a spec

{% hint style="success" %}
**Any non-visual logic exercisable without a network call ships with a spec — in the same change.**
{% endhint %}

In practice:

* **Pure functions** — validators, formatters, parsers. `tests/valid-ip.test.js`, `tests/bgp-prefix.test.js`, `tests/mtr-parse.test.js`.
* **Transforms** — anything that reshapes upstream data. `tests/transform-ip-data.test.js`, `tests/service-status-transform.test.js`.
* **Composables with mockable inputs** — logic you can drive by passing values in. `tests/composable-achievement-engine.test.js`, `tests/composable-info-mask.test.js`.
* **Middleware** — `tests/guards.test.js` covers every guard in `common/guards.js` with `(req, res, next)` stubs.
* **Static data files** — shape and integrity. `tests/changelog.test.js`, `tests/achievements.test.js`, `tests/sections.test.js`, `tests/ip-databases.test.js`.
* **API handlers** — smoke coverage only, see below.

When behavior shifts, update the affected specs **in the same change**. Don't defer.

### Bridge specs

When a helper lives in `common/` and is re-exported through `frontend/utils/`, the spec imports **both paths** and asserts they agree. `tests/valid-ip.test.js` does this, which is what stops a bridge from quietly regrowing a duplicate implementation.

## What does not get a spec

| Out of scope       | Why                                                      |
| ------------------ | -------------------------------------------------------- |
| Vue rendering      | The Node runner mounts nothing — no DOM, no components.  |
| Real network calls | No spec may hit a live upstream.                         |
| Browser APIs       | WebRTC, `navigator`, canvas fingerprinting, and friends. |

{% hint style="warning" %}
**Visual changes can't be self-tested.** If your change is visual, say so explicitly when you hand it off and let a human look at it in `pnpm dev`. A green `pnpm check` proves nothing about how the UI looks.
{% endhint %}

## Smoke tests for API handlers

Every handler under `api/` has smoke coverage in `tests/api-handlers.test.js`. The philosophy is narrow and strict:

{% hint style="danger" %}
**Never hit a real upstream.** Assert only on branches that return **before** the first `fetchUpstream` call.
{% endhint %}

That leaves three kinds of assertion:

* **Method gating** — `POST` to a GET-only handler returns `405`.
* **Parameter branches** — missing or malformed input returns `400`.
* **"API key missing" early returns** — the handler bails before calling out.

The file provides two stubs every handler test reuses:

{% code title="tests/api-handlers.test.js" %}

```js
function createRequest(options = {}) {
    const method = options.method || 'GET';
    const query = options.query || {};
    const referer = Object.hasOwn(options, 'referer') ? options.referer : 'http://localhost/';
    const headers = {};
    if (referer !== undefined) headers.referer = referer;
    return { method, headers, query, body: options.body };
}

function createResponse() {
    return {
        statusCode: 200,
        body: undefined,
        status(code) { this.statusCode = code; return this; },
        json(payload) { this.body = payload; return this; },
        send(payload) { this.body = payload; return this; },
    };
}
```

{% endcode %}

A complete handler block looks like this:

{% code title="tests/api-handlers.test.js" %}

```js
describe('mac-checker handler', () => {
    it('rejects missing ?mac', async () => {
        const res = createResponse();
        await macCheckerHandler(createRequest(), res);
        assert.equal(res.statusCode, 400);
        assert.deepEqual(res.body, { error: 'No MAC address provided' });
    });

    it('rejects invalid MAC format', async () => {
        const res = createResponse();
        await macCheckerHandler(createRequest({ query: { mac: 'not-a-mac' } }), res);
        assert.equal(res.statusCode, 400);
        assert.deepEqual(res.body, { error: 'Invalid MAC address' });
    });
});
```

{% endcode %}

### Environment variables

Tests that flip an environment variable register its name in the `ENV_KEYS` array at the top of the file. `beforeEach` backs those keys up and `afterEach` restores them, so no spec leaks state into the next.

### Don't duplicate the middleware

Referer checks and parameter validation are enforced by middleware, not by handlers. They are covered once, in `tests/guards.test.js`. A handler spec should not re-assert "rejects a bad domain" — the handler never sees one.

The convention is to note that in a comment above the `describe` block, the way the OONI handler's block does:

```js
// Domain presence/shape is enforced by requireValidDomain middleware
// (tests/guards.test.js); the handler's only pre-fetch branch is the
// defensive method gate.
```

### Defensive method gates stay

Some handlers keep a `req.method !== 'GET'` check even though the route already restricts the method. Those gates exist because the smoke tests assert on them directly. Leave them in place.

## Related specs worth knowing

| Spec                                                            | Covers                                                                                |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `tests/guards.test.js`                                          | Every middleware in `common/guards.js`                                                |
| `tests/fetch-with-timeout.test.js`                              | Upstream timeout and abort behavior                                                   |
| `tests/changelog.test.js`                                       | Changelog shape and four-locale coverage — see [i18n](/developer/development/i18n.md) |
| `tests/report-schema.test.js` / `tests/report-builders.test.js` | The shareable diagnostic report pipeline                                              |

## `pnpm check` must be green

```bash
pnpm check     # pnpm test && pnpm build
```

Tests plus a real production build. Run it before every handoff — a PR, a commit, a review request.

CI runs the same two steps (`pnpm test`, then `pnpm run build`) on every push and pull request against `main` and `dev`, on Node 24, installing with `pnpm install --frozen-lockfile`. A green local `check` usually means a green CI run.

## Next

* [Coding Conventions](/developer/development/coding-conventions.md) — the rules the specs are checking against.
* [Adding a New Tool](/developer/development/adding-a-new-tool.md) — where tests fit in a full feature.
* [Backend](/developer/architecture/backend.md) — handler and middleware design.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/development/testing.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
