Files
wownerogue/test/walletRpcAuth.test.js
jwinterm fad2a695bd Wownerogue
A browser-based roguelike synchronized with crypto block timing. A run is a race
against the next block: reach the exit before the block lands, or the house wins.
The same codebase runs the solo dungeon, the social Tavern, and operator-selected
PvP and race rulesets, with separate free and paid leaderboards.

Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md
for the design, and docs/DEPLOY.md to operate an instance.
2026-07-27 08:23:01 -04:00

117 lines
4.8 KiB
JavaScript

const WalletRPCService = require('../src/payments/walletRPCService');
describe('WalletRPCService HTTP authentication', () => {
const originalUser = process.env.WALLET_RPC_USER;
const originalPassword = process.env.WALLET_RPC_PASSWORD;
beforeEach(() => {
delete process.env.WALLET_RPC_USER;
delete process.env.WALLET_RPC_PASSWORD;
});
afterAll(() => {
if (originalUser === undefined) delete process.env.WALLET_RPC_USER;
else process.env.WALLET_RPC_USER = originalUser;
if (originalPassword === undefined) delete process.env.WALLET_RPC_PASSWORD;
else process.env.WALLET_RPC_PASSWORD = originalPassword;
});
test('answers a wallet RPC Digest challenge with the configured credential', async () => {
process.env.WALLET_RPC_USER = 'rpc-user';
process.env.WALLET_RPC_PASSWORD = 'rpc-password';
const unauthorized = new Error('unauthorized');
unauthorized.response = {
status: 401,
headers: {
'www-authenticate': 'Digest realm="monero-rpc", nonce="abc123", qop="auth", opaque="opaque1", algorithm=MD5'
}
};
const http = { post: jest.fn()
.mockRejectedValueOnce(unauthorized)
.mockResolvedValueOnce({ data: { result: { height: 1 } } }) };
const service = new WalletRPCService({ CONSOLE_LOGGING: false }, { http });
await service.rpcCall('get_height');
expect(http.post).toHaveBeenNthCalledWith(2,
expect.stringContaining('/json_rpc'),
expect.objectContaining({ method: 'get_height' }),
expect.objectContaining({
headers: expect.objectContaining({
Authorization: expect.stringMatching(/^Digest .*username="rpc-user".*realm="monero-rpc".*nonce="abc123".*qop=auth/)
})
})
);
});
test('omits authentication for a loopback no-login service', async () => {
const http = { post: jest.fn().mockResolvedValue({ data: { result: { height: 1 } } }) };
const service = new WalletRPCService({ CONSOLE_LOGGING: false }, { http });
await service.rpcCall('get_height');
expect(http.post).toHaveBeenCalledTimes(1);
expect(http.post.mock.calls[0][2].headers).not.toHaveProperty('Authorization');
});
test('production refuses a wallet that accepts the unauthenticated probe', async () => {
const http = { post: jest.fn().mockResolvedValue({ data: { result: { version: 1 } } }) };
const service = new WalletRPCService({ CONSOLE_LOGGING: false }, {
http,
rpcUser: 'rpc-user',
rpcPassword: 'rpc-password',
authRequired: true
});
await expect(service.ensureAuthentication({ force: true }))
.rejects.toMatchObject({ code: 'WALLET_RPC_AUTH_NOT_ENFORCED', statusCode: 503 });
expect(service.getHealthStatus()).toMatchObject({
healthy: false,
authentication: { required: true, verified: false, reason: 'not_enforced' }
});
});
test('production proves both the unauthenticated challenge and an authenticated response', async () => {
const challenge = () => {
const error = new Error('unauthorized');
error.response = {
status: 401,
headers: {
'www-authenticate': 'Digest realm="monero-rpc", nonce="abc123", qop="auth", algorithm=MD5'
}
};
return error;
};
const http = { post: jest.fn(async (_url, _body, config) => {
if (!config?.headers?.Authorization) throw challenge();
return { data: { result: { version: 1 } } };
}) };
const service = new WalletRPCService({ CONSOLE_LOGGING: false }, {
http,
rpcUser: 'rpc-user',
rpcPassword: 'rpc-password',
authRequired: true
});
await expect(service.ensureAuthentication({ force: true })).resolves.toMatchObject({
verified: true,
reason: null
});
expect(http.post).toHaveBeenCalledTimes(2);
expect(http.post.mock.calls[1][2].headers.Authorization).toMatch(/^Digest /);
expect(JSON.stringify(service.getHealthStatus())).not.toMatch(/rpc-user|rpc-password/);
});
test('production refuses missing wallet credentials without contacting RPC', async () => {
const http = { post: jest.fn() };
const service = new WalletRPCService({ CONSOLE_LOGGING: false }, {
http,
authRequired: true
});
await expect(service.ensureAuthentication({ force: true }))
.rejects.toMatchObject({ code: 'WALLET_RPC_AUTH_UNVERIFIED', statusCode: 503 });
expect(http.post).not.toHaveBeenCalled();
});
});