/** * Integration tests for POST /xrpc/cash.attoshi.submitTransaction */ import { describe, test, expect, beforeAll } from "bun:test"; import { AttoshiClient } from "../helpers/api.js"; import { waitForServer, BASE_URL } from "../setup.js"; describe("POST /xrpc/cash.attoshi.submitTransaction", () => { const client = new AttoshiClient(); beforeAll(async () => { await waitForServer(); }); describe("request validation", () => { test("returns 400 for empty body", async () => { const res = await fetch( `${BASE_URL}/xrpc/cash.attoshi.submitTransaction`, { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}", } ); expect(res.status).toBe(400); }); test("returns error for invalid JSON", async () => { const res = await fetch( `${BASE_URL}/xrpc/cash.attoshi.submitTransaction`, { method: "POST", headers: { "Content-Type": "application/json" }, body: "invalid json", } ); // Server may return 400 or 500 depending on JSON parse error handling expect([400, 500]).toContain(res.status); }); test("returns NO_INPUTS for transaction without inputs", async () => { const { status, data } = await client.submitTransaction([], [ { owner: "did:plc:bob1234567890", amount: 50 }, ]); expect(status).toBe(400); expect(data.error).toBe("NO_INPUTS"); }); test("returns NO_OUTPUTS for transaction without outputs", async () => { const { status, data } = await client.submitTransaction( [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }], [] ); expect(status).toBe(400); expect(data.error).toBe("NO_OUTPUTS"); }); }); describe("output validation", () => { test("returns INVALID_OUTPUT_OWNER for non-DID owner", async () => { const { status, data } = await client.submitTransaction( [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }], [{ owner: "not-a-did", amount: 50 }] ); expect(status).toBe(400); expect(data.error).toBe("INVALID_OUTPUT_OWNER"); }); test("returns INVALID_OUTPUT_OWNER for empty owner", async () => { const { status, data } = await client.submitTransaction( [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }], [{ owner: "", amount: 50 }] ); expect(status).toBe(400); expect(data.error).toBe("INVALID_OUTPUT_OWNER"); }); test("returns INVALID_OUTPUT_AMOUNT for zero amount", async () => { const { status, data } = await client.submitTransaction( [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }], [{ owner: "did:plc:bob1234567890", amount: 0 }] ); expect(status).toBe(400); expect(data.error).toBe("INVALID_OUTPUT_AMOUNT"); }); test("returns INVALID_OUTPUT_AMOUNT for negative amount", async () => { const { status, data } = await client.submitTransaction( [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }], [{ owner: "did:plc:bob1234567890", amount: -100 }] ); expect(status).toBe(400); expect(data.error).toBe("INVALID_OUTPUT_AMOUNT"); }); test("returns INVALID_OUTPUT_AMOUNT for non-integer amount", async () => { const { status, data } = await client.submitTransaction( [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }], [{ owner: "did:plc:bob1234567890", amount: 50.5 }] ); expect(status).toBe(400); expect(data.error).toBe("INVALID_OUTPUT_AMOUNT"); }); }); describe("UTXO validation", () => { test("returns UTXO_NOT_FOUND for non-existent input", async () => { const { status, data } = await client.submitTransaction( [{ txId: "nonexistent123", index: 0, sig: "ZmFrZXNpZw==" }], [{ owner: "did:plc:bob1234567890", amount: 50 }] ); expect(status).toBe(400); expect(data.error).toBe("UTXO_NOT_FOUND"); }); test("returns UTXO_NOT_FOUND for non-existent output index", async () => { // Get a real UTXO const configRes = await client.getConfig(); const treasuryDid = configRes.data.config.treasury; const utxosRes = await client.getUtxos(treasuryDid, 1); if (utxosRes.data.utxos.length > 0) { const utxo = utxosRes.data.utxos[0]; // Use valid txId but invalid index const { status, data } = await client.submitTransaction( [{ txId: utxo.txId, index: 999, sig: "ZmFrZXNpZw==" }], [{ owner: "did:plc:bob1234567890", amount: 50 }] ); expect(status).toBe(400); expect(data.error).toBe("UTXO_NOT_FOUND"); } }); }); describe("signature validation", () => { test("returns INVALID_SIGNATURE for bad signature", async () => { // Get a real UTXO const configRes = await client.getConfig(); const treasuryDid = configRes.data.config.treasury; const utxosRes = await client.getUtxos(treasuryDid, 1); if (utxosRes.data.utxos.length > 0) { const utxo = utxosRes.data.utxos[0]; const { status, data } = await client.submitTransaction( [{ txId: utxo.txId, index: utxo.index, sig: "aW52YWxpZHNpZ25hdHVyZQ==" }], [{ owner: "did:plc:bob1234567890", amount: utxo.amount }] ); expect(status).toBe(400); expect(data.error).toBe("INVALID_SIGNATURE"); } }); test("returns INVALID_SIGNATURE for malformed base64", async () => { const configRes = await client.getConfig(); const treasuryDid = configRes.data.config.treasury; const utxosRes = await client.getUtxos(treasuryDid, 1); if (utxosRes.data.utxos.length > 0) { const utxo = utxosRes.data.utxos[0]; const { status, data } = await client.submitTransaction( [{ txId: utxo.txId, index: utxo.index, sig: "not-valid-base64!!!" }], [{ owner: "did:plc:bob1234567890", amount: utxo.amount }] ); expect(status).toBe(400); } }); }); describe("multiple inputs/outputs", () => { test("returns DUPLICATE_INPUT for same UTXO referenced twice", async () => { const { status, data } = await client.submitTransaction( [ { txId: "same-tx-id123", index: 0, sig: "ZmFrZQ==" }, { txId: "same-tx-id123", index: 0, sig: "ZmFrZQ==" }, ], [{ owner: "did:plc:bob1234567890", amount: 100 }] ); expect(status).toBe(400); expect(data.error).toBe("DUPLICATE_INPUT"); }); test("validates all inputs exist", async () => { const { status, data } = await client.submitTransaction( [ { txId: "nonexistent1", index: 0, sig: "ZmFrZQ==" }, { txId: "nonexistent2", index: 0, sig: "ZmFrZQ==" }, ], [{ owner: "did:plc:bob1234567890", amount: 100 }] ); expect(status).toBe(400); expect(data.error).toBe("UTXO_NOT_FOUND"); }); test("validates all output owners", async () => { const { status, data } = await client.submitTransaction( [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZQ==" }], [ { owner: "did:plc:valid1234567", amount: 50 }, { owner: "invalid-owner", amount: 50 }, ] ); expect(status).toBe(400); expect(data.error).toBe("INVALID_OUTPUT_OWNER"); }); test("validates all output amounts", async () => { const { status, data } = await client.submitTransaction( [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZQ==" }], [ { owner: "did:plc:valid1234567", amount: 50 }, { owner: "did:plc:valid2345678", amount: -10 }, ] ); expect(status).toBe(400); expect(data.error).toBe("INVALID_OUTPUT_AMOUNT"); }); }); describe("response structure (on success)", () => { // Note: Testing successful submission requires valid signatures // which would need access to private keys // These tests document the expected response structure test("successful response would have txId", async () => { // This is documentation - actual success requires valid signature // Successful response should have: // { txId: string, uri: string, commit: { cid: string, rev: string } } expect(true).toBe(true); }); test("successful response would have uri", async () => { // URI format: at://{entity-did}/cash.attoshi.tx/{txId} expect(true).toBe(true); }); test("successful response would have commit info", async () => { // commit: { cid: string, rev: string } expect(true).toBe(true); }); }); });