Build extensions / Workflow
Testing
orb test runs every JSON file in tests/ against your built hooks, without OrbTorrent. Each test is a download, the network’s answers, and what the hook should return.
npm test✓ Holds a download with a screensaver in it tests/holds-a-screensaver.json✓ Ignores a screensaver the user isn't downloading tests/ignores-a-skipped-screensaver.json✓ Lets a film without one start tests/lets-a-film-start.json
3 passedTo run some tests only, name them: npx orb test tests/holds-a-screensaver.json.
A test
Section titled “A test”{ "name": "Fails a program that 12 engines flag", "hook": "scan", "preferences": { "apiKey": "test-key" }, "download": { "name": "Tool 2.0", "files": [{ "path": "Tool 2.0/setup.exe", "size": 5000000, "sha256": "aaaa…" }] }, "mocks": { "fetch": [ { "url": "https://www.virustotal.com/api/v3/files/*", "json": { "data": { "attributes": { "last_analysis_stats": { "malicious": 12, "suspicious": 0, "undetected": 58, "harmless": 0 } } } } } ] }, "expect": { "status": "failed", "summaryIncludes": "12 of 70 engines" }}Test files lists every field. The ones you’ll use most:
download: itsnameandfiles, each with apath, asize, and"wanted": falsefor a file the user skipped. Once it’s finished,"isNew": falsemarks a file a hook saw on an earlier run.preferences: the settings. Any you leave out take their defaults.expect: what the result should be.
What to test
Section titled “What to test”- Each thing it catches, with the summary you expect.
- What must pass. Releases that look like what you catch, but aren’t, matter most: a false alarm that holds a download is worse than a missed warning. The Fake Download Check tests real releases that must pass, as well as each fake it holds.
- What it skips, such as files the user didn’t want, and when a setting is empty.
- Failure: a refused API key, a rate limit, a server error. Before a download starts, each should be
skipped.
Mock the network
Section titled “Mock the network”Requests are answered from mocks.fetch, never the real network:
{ "mocks": { "fetch": [ { "url": "https://www.virustotal.com/api/v3/files/aaaa…", "status": 429, "times": 1, "json": { "error": { "code": "QuotaExceededError" } } }, { "url": "https://www.virustotal.com/api/v3/files/aaaa…", "json": { "data": { "attributes": { "last_analysis_stats": { "malicious": 0, "suspicious": 0, "undetected": 60, "harmless": 10 } } } } } ] }}- A request takes the first mock that matches its
method(GETunless you say) andurl. *in aurlmatches any text, such ashttps://www.virustotal.com/api/v3/files/*.timesanswers that many requests, then the next matching mock takes over: here, a rate limit, then success.jsonanswers with JSON, and setscontent-type: application/json.textanswers with text.statusis 200 unless you say, andheadersadds headers.- A request with no mock fails the test, and says which: “The test has no mock for GET https://…. Add one to “mocks”: { “fetch”: […] }.”
The rules OrbTorrent applies run in tests too: a host missing from permissions.network, or a plain http:// address, is refused with the same error.
"fetched" in expect checks that a request was made, and "not": { "fetched": […] } that one wasn’t.
Files and storage
Section titled “Files and storage”files.sha256returns each file’ssha256from the test. A file without one fails the test, saying so.storagesets what the extension’s storage holds before the run, to test that it uses what it kept:
{ "storage": { "lookups": { "aaaa…": { "at": 9999999999999, "stats": { "malicious": 0, "suspicious": 0, "total": 70 } } } }, "expect": { "status": "passed", "not": { "fetched": ["https://www.virustotal.com/api/v3/files/aaaa…"] } }}Reading files follows OrbTorrent’s rules too: without "files": "contents", in a beforeStart hook, or for a file that isn’t wanted, it’s refused.
files.savefollows OrbTorrent’s rules and messages, and keeps what it adds instead of writing it.expectchecks it withsavedandsavedIncludes, anddownload.alsoOnDisklists files already there, which it won’t replace. Read and add files has an example.- Bytes: a mock with
base64answers with bytes, such as an image, forresponse.bytes().
Files a result names
Section titled “Files a result names”expect.files checks what the result says about each file, by path, with * matching any text:
{ "expect": { "status": "failed", "files": { "Tool 2.0/setup.exe": "failed", "*.txt": "none" } } }"none" means the result says nothing about it. A result is checked as OrbTorrent checks it: it can only name the download’s files, and it counts as no better than its worst file.
Reports on the checks
Section titled “Reports on the checks”An afterChecks hook gets other extensions’ results from download.checks, and "held": true makes the download one a check is holding:
{ "hook": "notify", "download": { "name": "Tool 2.0", "files": [{ "path": "Tool 2.0/setup.exe" }], "checks": [ { "extension": { "name": "virustotal", "title": "VirusTotal Scan" }, "status": "failed", "summary": "12 of 70 engines flag setup.exe", "files": [{ "path": "Tool 2.0/setup.exe", "status": "failed" }] } ] }, "mocks": { "fetch": [{ "method": "POST", "url": "https://ntfy.sh/*" }] }, "expect": { "status": "passed", "fetched": ["https://ntfy.sh/*"] }}Timers run on a virtual clock. setTimeout and sleep fire as soon as nothing else is pending, and Date.now() moves on by the time waited. A hook that waits 15 seconds between requests for a rate limit is tested in moments, and a test of a minute’s back-off takes no time at all.
Each test can run for 10 seconds of real time, unless it sets timeout in milliseconds.
Test verify
Section titled “Test verify”To test the settings check, set "verify": true and the preferences to check, and expect ok and part of the message:
{ "name": "Verify refuses a topic with spaces", "verify": true, "preferences": { "topic": "my downloads" }, "expect": { "ok": false, "messageIncludes": "letters, digits" }}Expectations
Section titled “Expectations”| Key | Passes when |
|---|---|
status |
The result’s status matches. |
summaryIncludes |
The summary includes the text, ignoring case. |
summaryMatches |
The summary matches the regular expression. |
detailsIncludes |
The details include the text. |
fetched |
Each URL was requested. * matches any text. |
files |
Each file the result names has this status, by path, or "none". |
throws |
The hook threw an error including the text. |
ok and messageIncludes |
For verify: whether it accepted the settings, and its message. |
not |
The expectation inside it fails. |
A test with no expect passes if the hook returns without throwing.
How tests run
Section titled “How tests run”orb test builds the extension, then runs each hook in a Node.js vm context set up like OrbTorrent’s extension host: no Node.js APIs, and every request, fingerprint and storage call answered by the test. The rules and error messages are OrbTorrent’s own.
Node’s JavaScript engine isn’t JavaScriptCore, which OrbTorrent uses, so rare differences are possible. To be sure, run a hook in OrbTorrent itself on a real download with orb run.
For scripts and agents
Section titled “For scripts and agents”npx orb test --json prints the results as one JSON object, and exits with 1 if any test failed:
{ "ok": false, "passed": 2, "failed": 1, "tests": [ { "file": "tests/lets-a-film-start.json", "name": "Lets a film without one start", "passed": false, "failures": ["expected status “failed”, got “passed” (No screensavers)"], "result": { "status": "passed", "summary": "No screensavers" }, "logs": [], "fetched": [] } ]}In continuous integration
Section titled “In continuous integration”Tests, the build and the linter all run without OrbTorrent or a Mac, so they suit any CI. On GitHub Actions:
name: Teston: [push, pull_request]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npx orb test - run: npx orb lint - run: npx orb build