Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/sdk/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.test.ts'],
// #126: no test may open a real outbound connection.
setupFilesAfterEnv: ['<rootDir>/jest.no-network.js'],
};
44 changes: 44 additions & 0 deletions packages/sdk/jest.no-network.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Issue #126: fail any jest test that opens a real outbound connection.
//
// Loopback stays allowed so a test can run a local stub server. Everything
// else is refused at the socket and recorded, and the test that caused it
// fails in afterEach. The record matters: code under test often catches the
// connection error (x402Serve turns it into a 503), so refusing alone would
// pass silently.
const net = require('net');

const LOOPBACK = new Set(['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1']);
const violations = [];

function target(args) {
const [a, b] = args;
if (Array.isArray(a)) return target(a); // internal normalized form
if (a && typeof a === 'object') {
if (a.path) return null; // unix socket / named pipe: not network
return { host: a.host ?? 'localhost', port: a.port };
}
if (typeof a === 'string' && Number.isNaN(Number(a))) return null; // ipc path
return { host: typeof b === 'string' ? b : 'localhost', port: a };
}

if (!net.Socket.prototype.__niriumNoNetwork) {
const connect = net.Socket.prototype.connect;
net.Socket.prototype.connect = function (...args) {
const t = target(args);
if (t && !LOOPBACK.has(String(t.host).replace(/^\[|\]$/g, ''))) {
const err = new Error(`outbound network blocked in tests: ${t.host}:${t.port} (see jest.no-network.js)`);
violations.push(err.message);
process.nextTick(() => this.destroy(err));
return this;
}
return connect.apply(this, args);
};
net.Socket.prototype.__niriumNoNetwork = true;
}

afterEach(() => {
if (violations.length) {
const found = violations.splice(0);
throw new Error(`test opened ${found.length} outbound connection(s):\n ${found.join('\n ')}`);
}
});
32 changes: 24 additions & 8 deletions packages/sdk/src/x402serve-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,23 @@ describe('x402Serve guard integration (config.guard)', () => {
// x402Serve ever reaches preflight()/build() — so none of this needs the
// @x402/express/@x402/core mocks above. The allowed-path/store behavior
// itself is covered directly in x402-guard.test.ts.
//
// #126: calls that get past the guard reach preflight(), which fetches the
// facilitator's /supported. Stub that fetch so these tests never touch the
// network, and answer 503 so the path after the guard is always the same
// (x402Serve's own 503), instead of depending on a third-party service.
let facilitatorFetch: jest.SpyInstance;
beforeEach(() => {
facilitatorFetch = jest.spyOn(globalThis, 'fetch').mockImplementation(async () => new Response(null, { status: 503 }));
});
afterEach(() => facilitatorFetch.mockRestore());

function expectStubbedPreflight(res: any) {
expect(facilitatorFetch).toHaveBeenCalledWith('https://channels.openzeppelin.com/x402/testnet/supported', expect.anything());
expect(res.statusCode).toBe(503);
expect(res.body.error).toBe('x402 payments unavailable');
expect(res.body.detail).toBe('facilitator returned 503');
}

function fakeStore(overrides: Partial<{
claimOnce: (key: string, ttlMs: number) => Promise<boolean>;
Expand Down Expand Up @@ -138,9 +155,8 @@ describe('x402Serve guard integration (config.guard)', () => {

const first = mockReqRes({ 'payment-signature': 'proof-1' });
await handler(first.req, first.res, () => {});
// First call proceeds past the guard (into preflight, which will fail
// in this test env with no real facilitator — that's fine, it's not
// what this test is checking).
// First call proceeds past the guard, into the stubbed preflight.
expectStubbedPreflight(first.res);

const second = mockReqRes({ 'payment-signature': 'proof-1' });
await handler(second.req, second.res, () => {});
Expand Down Expand Up @@ -178,10 +194,10 @@ describe('x402Serve guard integration (config.guard)', () => {
await handler(first.req, first.res, () => {});
const second = mockReqRes({ 'payment-signature': 'proof-4' }); // same "proof" reused
await handler(second.req, second.res, () => {});
// Neither call was rejected by a guard that doesn't exist - whatever
// status they got came from downstream (preflight failing in this test
// env), never 409/429, and never our guard's own error bodies.
expect(second.res.body?.error).not.toBe('payment_replayed');
expect(second.res.statusCode).not.toBe(409);
// Neither call was rejected by a guard that doesn't exist: both reached
// the stubbed preflight and got its 503, never 409/429.
expectStubbedPreflight(first.res);
expectStubbedPreflight(second.res);
expect(facilitatorFetch).toHaveBeenCalledTimes(2);
});
});
Loading