mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 14:21:25 +02:00
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.
117 lines
4.8 KiB
JavaScript
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();
|
|
});
|
|
});
|