morphit/apps/indexer/test/handlers/orderComplete.test.ts

167 lines
6.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import handler from '$indexer/handlers/orderComplete';
import { makeCtx } from '../testutils/context';
import { makeMockClient } from '../testutils/mockClient';
const PERMLINK = 'sell-btc-eur-2026-04';
describe('orderComplete handler (morphit_order_complete_v1)', () => {
it('flips an owner\'s live order to completed', async () => {
// UPDATE ... WHERE account=signer AND status='live' matches one row.
const mock = makeMockClient([
{ match: "UPDATE orders SET status = 'completed'", rowCount: 1 }
]);
const r = await handler(
makeCtx({ signer: 'alice', payload: { permlink: PERMLINK } }),
mock.client
);
expect(r).toEqual({ ok: true });
expect(mock.queries).toHaveLength(1);
// Owner-only guard: signer is the WHERE account param.
expect(mock.queries[0]!.params).toContain('alice');
expect(mock.queries[0]!.params).toContain(PERMLINK);
});
it('rejects a NON-owner (griefing guard) with target_not_found', async () => {
// mallory doesn't own the order: the account=signer UPDATE matches
// nothing, and the probe (also scoped to mallory) finds nothing.
const mock = makeMockClient([
{ match: 'UPDATE orders', rowCount: 0 },
{ match: 'SELECT status FROM orders', rows: [], rowCount: 0 }
]);
const r = await handler(
makeCtx({ signer: 'mallory', payload: { permlink: PERMLINK } }),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'target_not_found' });
});
it('reports target_already_cancelled when the order is not live', async () => {
const mock = makeMockClient([
{ match: 'UPDATE orders', rowCount: 0 },
{ match: 'SELECT status FROM orders', rows: [{ status: 'cancelled' }], rowCount: 1 }
]);
const r = await handler(
makeCtx({ signer: 'alice', payload: { permlink: PERMLINK } }),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'target_already_cancelled' });
});
it('is idempotent-safe: a second complete on an already-completed order is a no-op reject', async () => {
const mock = makeMockClient([
{ match: 'UPDATE orders', rowCount: 0 },
{ match: 'SELECT status FROM orders', rows: [{ status: 'completed' }], rowCount: 1 }
]);
const r = await handler(
makeCtx({ signer: 'alice', payload: { permlink: PERMLINK } }),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'target_already_completed' });
});
// ── v1.5.5 (t155): the optional `counterparty` field ──────────────
//
// v1.5.5 credits BOTH sides of a completed trade, which means the owner now
// NAMES the other party on-chain. That is a public claim about someone else
// made by a stranger, so every path here is anti-collusion surface.
it('stores a PROVEN counterparty so both sides get trade credit', async () => {
const mock = makeMockClient([
// hasVerifiedChat: 2 each way, 15-min span, pair not flagged.
{
match: 'FROM chat_messages',
rows: [{ from_a: '2', from_b: '2', span_seconds: '900', has_recip_flag: false }]
},
{ match: "UPDATE orders SET status = 'completed'", rowCount: 1 }
]);
const r = await handler(
makeCtx({ signer: 'alice', payload: { permlink: PERMLINK, counterparty: 'bob' } }),
mock.client
);
expect(r).toEqual({ ok: true });
const update = mock.queries.find((q) => q.text.includes("status = 'completed'"))!;
expect(update.params).toContain('bob');
});
it('rejects naming YOURSELF as the counterparty', async () => {
// Otherwise one op mints two trade credits for one account.
const mock = makeMockClient([]);
const r = await handler(
makeCtx({ signer: 'alice', payload: { permlink: PERMLINK, counterparty: 'alice' } }),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'counterparty_is_self' });
expect(mock.queries).toHaveLength(0); // rejected before touching the DB
});
it('rejects a malformed counterparty name without touching the DB', async () => {
const mock = makeMockClient([]);
const r = await handler(
makeCtx({ signer: 'alice', payload: { permlink: PERMLINK, counterparty: 'Not A Name!' } }),
mock.client
);
expect(r).toEqual({ ok: false, reason: 'counterparty_invalid' });
expect(mock.queries).toHaveLength(0);
});
it('drops an UNPROVEN counterparty but still completes the order', async () => {
// THE anti-collusion rule: naming someone must clear the same
// provable-conversation bar a review requires, or anyone could mint fake
// trade credit for any account — and attach an unconsented public claim
// to a stranger's name. But the OWNER's own completion is legitimate
// regardless, so the op must still succeed with counterparty NULL rather
// than reject: refusing would leave a settled order stuck "Live" in the
// orderbook, which is the very bug v1.5.5 fixes.
const mock = makeMockClient([
// Never chatted → gate denies.
{
match: 'FROM chat_messages',
rows: [{ from_a: '0', from_b: '0', span_seconds: null, has_recip_flag: false }]
},
{ match: "UPDATE orders SET status = 'completed'", rowCount: 1 }
]);
const r = await handler(
makeCtx({ signer: 'alice', payload: { permlink: PERMLINK, counterparty: 'stranger' } }),
mock.client
);
expect(r).toEqual({ ok: true });
const update = mock.queries.find((q) => q.text.includes("status = 'completed'"))!;
expect(update.params).not.toContain('stranger');
expect(update.params).toContain(null);
});
it('omitting counterparty stores NULL (older clients stay valid)', async () => {
// Back-compat: the field is OPTIONAL. A pre-v1.5.5 client broadcasts
// {permlink} alone and must keep working — the op schema is append-only.
const mock = makeMockClient([
{ match: "UPDATE orders SET status = 'completed'", rowCount: 1 }
]);
const r = await handler(
makeCtx({ signer: 'alice', payload: { permlink: PERMLINK } }),
mock.client
);
expect(r).toEqual({ ok: true });
// No gate query — nothing to prove.
expect(mock.queries).toHaveLength(1);
expect(mock.queries[0]!.params).toContain(null);
});
it('rejects a non-object payload', async () => {
const mock = makeMockClient([]);
const r = await handler(makeCtx({ signer: 'alice', payload: [] }), mock.client);
expect(r).toEqual({ ok: false, reason: 'payload_not_object' });
expect(mock.queries).toHaveLength(0);
});
it('rejects an invalid permlink without touching the DB', async () => {
const mock = makeMockClient([]);
const r = await handler(
makeCtx({ signer: 'alice', payload: { permlink: 123 } }),
mock.client
);
expect(r.ok).toBe(false);
expect(mock.queries).toHaveLength(0);
});
});