mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-23 17:18:32 +02:00
139 lines
7.0 KiB
JavaScript
139 lines
7.0 KiB
JavaScript
const { createAdapter, paymentRequest } = require('../html/js/network/smirkPaymentAdapter');
|
|
|
|
const NOW = 1800000000000;
|
|
function invoice(extra = {}) {
|
|
return {
|
|
paymentId: 31, ownerUserId: 7, asset: 'wow', network: 'mainnet',
|
|
amount: '123456789012', humanAmount: '1.23456789012',
|
|
address: 'Wo' + '1'.repeat(95), expiresAt: new Date(NOW + 60000).toISOString(),
|
|
paymentType: 'credits_package', memo: 'Buy 10 credits', package: { credits: 10 }, ...extra
|
|
};
|
|
}
|
|
function setup(extra = {}) {
|
|
const values = new Map();
|
|
const storage = {
|
|
getItem: key => values.get(key) || null,
|
|
setItem: (key, value) => values.set(key, value),
|
|
removeItem: key => values.delete(key)
|
|
};
|
|
let tail = Promise.resolve();
|
|
const withLock = (_name, work) => {
|
|
const result = tail.then(work);
|
|
tail = result.catch(() => {});
|
|
return result;
|
|
};
|
|
const options = { storage, withLock, crypto: require('crypto').webcrypto, now: () => NOW, ...extra };
|
|
return { adapter: createAdapter(options), options, values };
|
|
}
|
|
|
|
describe('Smirk WOW invoice contract', () => {
|
|
test('uses exact server decimal and memo without inventing wallet parameters', () => {
|
|
expect(paymentRequest(invoice(), NOW)).toEqual({
|
|
asset: 'wow', amount: '1.23456789012', address: invoice().address, memo: 'Buy 10 credits'
|
|
});
|
|
});
|
|
|
|
test('accepts a WOW RPC subaddress as well as a standard address', () => {
|
|
const address = 'WW3UyJFX6GPKdM21APyVYjGhfX3ebyHvPPgtFC3wHxkYTCGg8k2UM9DLTCzcgoaLHSKnvJKRtCPgJ1sxvgYiFCoK19vLk83Jn';
|
|
expect(paymentRequest(invoice({ address }), NOW).address).toBe(address);
|
|
});
|
|
|
|
test('restored invoices keep the persisted memo without package display metadata', () => {
|
|
expect(paymentRequest(invoice({ package: undefined, restored: true }), NOW).memo).toBe('Buy 10 credits');
|
|
});
|
|
|
|
test.each([
|
|
{ humanAmount: undefined, amountFormatted: undefined },
|
|
{ humanAmount: '0' }, { humanAmount: '-1' }, { humanAmount: '1e-3' },
|
|
{ humanAmount: '1.000000000001' }, { humanAmount: 1 },
|
|
{ humanAmount: '1.23' }, { amount: '1.2' },
|
|
{ asset: 'xmr' }, { asset: undefined }, { network: 'stagenet' }, { network: undefined },
|
|
{ paymentId: undefined }, { ownerUserId: undefined }, { address: 'not-a-wow-address' }, { memo: undefined },
|
|
{ expiresAt: undefined }, { expiresAt: new Date(NOW).toISOString() }
|
|
])('refuses invalid or mismatched invoice before wallet invocation: %j', async extra => {
|
|
const { adapter } = setup();
|
|
const wallet = { requestPayment: jest.fn() };
|
|
await expect(adapter.pay(invoice(extra), wallet)).rejects.toThrow();
|
|
expect(wallet.requestPayment).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Smirk submission evidence and repeat protection', () => {
|
|
test.each([
|
|
[{ success: true, txid: 'a'.repeat(64) }, 'submitted'],
|
|
[{ success: true }, 'unknown'],
|
|
[{ success: false }, 'failed'],
|
|
[{ success: false, txid: 'a'.repeat(64) }, 'unknown'],
|
|
[undefined, 'unknown'], [true, 'unknown'], [{ txid: 'a'.repeat(64) }, 'unknown'],
|
|
[{ success: true, txid: 'malformed' }, 'unknown']
|
|
])('classifies wallet result %j as %s without confirming credits', async (result, state) => {
|
|
const { adapter } = setup();
|
|
const wallet = { requestPayment: jest.fn().mockResolvedValue(result) };
|
|
expect(await adapter.pay(invoice(), wallet)).toMatchObject({ state });
|
|
expect(wallet.requestPayment).toHaveBeenCalledTimes(1);
|
|
expect(await adapter.pay(invoice(), wallet)).toMatchObject({ state, repeated: true });
|
|
expect(wallet.requestPayment).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test.each([['USER_REJECTED', 'cancelled'], ['NOT_AUTHORIZED', 'unavailable'], ['TIMEOUT', 'unknown']])(
|
|
'classifies %s and never automatically retries', async (code, state) => {
|
|
const { adapter } = setup();
|
|
const wallet = { requestPayment: jest.fn().mockRejectedValue(Object.assign(new Error('fixture'), { code })) };
|
|
expect(await adapter.pay(invoice(), wallet)).toMatchObject({ state });
|
|
await adapter.pay(invoice(), wallet);
|
|
expect(wallet.requestPayment).toHaveBeenCalledTimes(1);
|
|
}
|
|
);
|
|
|
|
test('coordinates concurrent events, two tabs and a page reload for the same invoice', async () => {
|
|
const { adapter, options } = setup();
|
|
const secondTab = createAdapter(options);
|
|
const wallet = { requestPayment: jest.fn().mockResolvedValue({ success: true }) };
|
|
await Promise.all([adapter.pay(invoice(), wallet), secondTab.pay(invoice(), wallet)]);
|
|
const reloaded = createAdapter(options);
|
|
expect(await reloaded.pay(invoice({ reused: true }), wallet)).toMatchObject({ state: 'unknown', repeated: true });
|
|
expect(wallet.requestPayment).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('records intent before invoking a wallet and retains unknown after interruption', async () => {
|
|
const { adapter, options } = setup();
|
|
const wallet = { requestPayment: jest.fn(async () => {
|
|
expect(createAdapter(options).status(invoice()).state).toBe('unknown');
|
|
throw new Error('Transport closed after possible submission');
|
|
}) };
|
|
expect(await adapter.pay(invoice(), wallet)).toMatchObject({ state: 'unknown' });
|
|
expect(createAdapter(options).status(invoice())).toMatchObject({ state: 'unknown' });
|
|
});
|
|
|
|
test.each([
|
|
{ ownerUserId: 8 }, { memo: 'different purchase' },
|
|
{ humanAmount: '2', amount: '200000000000' },
|
|
{ expiresAt: new Date(NOW + 120000).toISOString() }
|
|
])('refuses changed immutable invoice fields after an attempt: %j', async extra => {
|
|
const { adapter } = setup();
|
|
const wallet = { requestPayment: jest.fn().mockResolvedValue({ success: true }) };
|
|
await adapter.pay(invoice(), wallet);
|
|
await expect(adapter.pay(invoice(extra), wallet)).rejects.toThrow(/changed/i);
|
|
expect(wallet.requestPayment).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('rechecks session admission inside the cross-tab lock before writing intent', async () => {
|
|
const { adapter } = setup();
|
|
const wallet = { requestPayment: jest.fn() };
|
|
await expect(adapter.pay(invoice(), wallet, () => { throw new Error('Account changed'); })).rejects.toThrow('Account changed');
|
|
expect(wallet.requestPayment).not.toHaveBeenCalled();
|
|
expect(adapter.status(invoice())).toBeNull();
|
|
});
|
|
|
|
test.each([
|
|
{ withLock: null },
|
|
{ storage: null },
|
|
{ storage: { getItem() { throw new Error('blocked'); }, setItem() {} } }
|
|
])('refuses unsafe repeat protection before requesting funds', async options => {
|
|
const { adapter } = setup(options);
|
|
const wallet = { requestPayment: jest.fn() };
|
|
await expect(adapter.pay(invoice(), wallet)).rejects.toThrow();
|
|
expect(wallet.requestPayment).not.toHaveBeenCalled();
|
|
});
|
|
});
|