this repo has no description
at main 242 lines 8.7 kB view raw
1/** 2 * Integration tests for POST /xrpc/cash.attoshi.submitTransaction 3 */ 4 5import { describe, test, expect, beforeAll } from "bun:test"; 6import { AttoshiClient } from "../helpers/api.js"; 7import { waitForServer, BASE_URL } from "../setup.js"; 8 9describe("POST /xrpc/cash.attoshi.submitTransaction", () => { 10 const client = new AttoshiClient(); 11 12 beforeAll(async () => { 13 await waitForServer(); 14 }); 15 16 describe("request validation", () => { 17 test("returns 400 for empty body", async () => { 18 const res = await fetch( 19 `${BASE_URL}/xrpc/cash.attoshi.submitTransaction`, 20 { 21 method: "POST", 22 headers: { "Content-Type": "application/json" }, 23 body: "{}", 24 } 25 ); 26 expect(res.status).toBe(400); 27 }); 28 29 test("returns error for invalid JSON", async () => { 30 const res = await fetch( 31 `${BASE_URL}/xrpc/cash.attoshi.submitTransaction`, 32 { 33 method: "POST", 34 headers: { "Content-Type": "application/json" }, 35 body: "invalid json", 36 } 37 ); 38 // Server may return 400 or 500 depending on JSON parse error handling 39 expect([400, 500]).toContain(res.status); 40 }); 41 42 test("returns NO_INPUTS for transaction without inputs", async () => { 43 const { status, data } = await client.submitTransaction([], [ 44 { owner: "did:plc:bob1234567890", amount: 50 }, 45 ]); 46 expect(status).toBe(400); 47 expect(data.error).toBe("NO_INPUTS"); 48 }); 49 50 test("returns NO_OUTPUTS for transaction without outputs", async () => { 51 const { status, data } = await client.submitTransaction( 52 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }], 53 [] 54 ); 55 expect(status).toBe(400); 56 expect(data.error).toBe("NO_OUTPUTS"); 57 }); 58 }); 59 60 describe("output validation", () => { 61 test("returns INVALID_OUTPUT_OWNER for non-DID owner", async () => { 62 const { status, data } = await client.submitTransaction( 63 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }], 64 [{ owner: "not-a-did", amount: 50 }] 65 ); 66 expect(status).toBe(400); 67 expect(data.error).toBe("INVALID_OUTPUT_OWNER"); 68 }); 69 70 test("returns INVALID_OUTPUT_OWNER for empty owner", async () => { 71 const { status, data } = await client.submitTransaction( 72 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }], 73 [{ owner: "", amount: 50 }] 74 ); 75 expect(status).toBe(400); 76 expect(data.error).toBe("INVALID_OUTPUT_OWNER"); 77 }); 78 79 test("returns INVALID_OUTPUT_AMOUNT for zero amount", async () => { 80 const { status, data } = await client.submitTransaction( 81 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }], 82 [{ owner: "did:plc:bob1234567890", amount: 0 }] 83 ); 84 expect(status).toBe(400); 85 expect(data.error).toBe("INVALID_OUTPUT_AMOUNT"); 86 }); 87 88 test("returns INVALID_OUTPUT_AMOUNT for negative amount", async () => { 89 const { status, data } = await client.submitTransaction( 90 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }], 91 [{ owner: "did:plc:bob1234567890", amount: -100 }] 92 ); 93 expect(status).toBe(400); 94 expect(data.error).toBe("INVALID_OUTPUT_AMOUNT"); 95 }); 96 97 test("returns INVALID_OUTPUT_AMOUNT for non-integer amount", async () => { 98 const { status, data } = await client.submitTransaction( 99 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZXNpZw==" }], 100 [{ owner: "did:plc:bob1234567890", amount: 50.5 }] 101 ); 102 expect(status).toBe(400); 103 expect(data.error).toBe("INVALID_OUTPUT_AMOUNT"); 104 }); 105 }); 106 107 describe("UTXO validation", () => { 108 test("returns UTXO_NOT_FOUND for non-existent input", async () => { 109 const { status, data } = await client.submitTransaction( 110 [{ txId: "nonexistent123", index: 0, sig: "ZmFrZXNpZw==" }], 111 [{ owner: "did:plc:bob1234567890", amount: 50 }] 112 ); 113 expect(status).toBe(400); 114 expect(data.error).toBe("UTXO_NOT_FOUND"); 115 }); 116 117 test("returns UTXO_NOT_FOUND for non-existent output index", async () => { 118 // Get a real UTXO 119 const configRes = await client.getConfig(); 120 const treasuryDid = configRes.data.config.treasury; 121 const utxosRes = await client.getUtxos(treasuryDid, 1); 122 123 if (utxosRes.data.utxos.length > 0) { 124 const utxo = utxosRes.data.utxos[0]; 125 // Use valid txId but invalid index 126 const { status, data } = await client.submitTransaction( 127 [{ txId: utxo.txId, index: 999, sig: "ZmFrZXNpZw==" }], 128 [{ owner: "did:plc:bob1234567890", amount: 50 }] 129 ); 130 expect(status).toBe(400); 131 expect(data.error).toBe("UTXO_NOT_FOUND"); 132 } 133 }); 134 }); 135 136 describe("signature validation", () => { 137 test("returns INVALID_SIGNATURE for bad signature", async () => { 138 // Get a real UTXO 139 const configRes = await client.getConfig(); 140 const treasuryDid = configRes.data.config.treasury; 141 const utxosRes = await client.getUtxos(treasuryDid, 1); 142 143 if (utxosRes.data.utxos.length > 0) { 144 const utxo = utxosRes.data.utxos[0]; 145 const { status, data } = await client.submitTransaction( 146 [{ txId: utxo.txId, index: utxo.index, sig: "aW52YWxpZHNpZ25hdHVyZQ==" }], 147 [{ owner: "did:plc:bob1234567890", amount: utxo.amount }] 148 ); 149 expect(status).toBe(400); 150 expect(data.error).toBe("INVALID_SIGNATURE"); 151 } 152 }); 153 154 test("returns INVALID_SIGNATURE for malformed base64", async () => { 155 const configRes = await client.getConfig(); 156 const treasuryDid = configRes.data.config.treasury; 157 const utxosRes = await client.getUtxos(treasuryDid, 1); 158 159 if (utxosRes.data.utxos.length > 0) { 160 const utxo = utxosRes.data.utxos[0]; 161 const { status, data } = await client.submitTransaction( 162 [{ txId: utxo.txId, index: utxo.index, sig: "not-valid-base64!!!" }], 163 [{ owner: "did:plc:bob1234567890", amount: utxo.amount }] 164 ); 165 expect(status).toBe(400); 166 } 167 }); 168 }); 169 170 describe("multiple inputs/outputs", () => { 171 test("returns DUPLICATE_INPUT for same UTXO referenced twice", async () => { 172 const { status, data } = await client.submitTransaction( 173 [ 174 { txId: "same-tx-id123", index: 0, sig: "ZmFrZQ==" }, 175 { txId: "same-tx-id123", index: 0, sig: "ZmFrZQ==" }, 176 ], 177 [{ owner: "did:plc:bob1234567890", amount: 100 }] 178 ); 179 expect(status).toBe(400); 180 expect(data.error).toBe("DUPLICATE_INPUT"); 181 }); 182 183 test("validates all inputs exist", async () => { 184 const { status, data } = await client.submitTransaction( 185 [ 186 { txId: "nonexistent1", index: 0, sig: "ZmFrZQ==" }, 187 { txId: "nonexistent2", index: 0, sig: "ZmFrZQ==" }, 188 ], 189 [{ owner: "did:plc:bob1234567890", amount: 100 }] 190 ); 191 expect(status).toBe(400); 192 expect(data.error).toBe("UTXO_NOT_FOUND"); 193 }); 194 195 test("validates all output owners", async () => { 196 const { status, data } = await client.submitTransaction( 197 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZQ==" }], 198 [ 199 { owner: "did:plc:valid1234567", amount: 50 }, 200 { owner: "invalid-owner", amount: 50 }, 201 ] 202 ); 203 expect(status).toBe(400); 204 expect(data.error).toBe("INVALID_OUTPUT_OWNER"); 205 }); 206 207 test("validates all output amounts", async () => { 208 const { status, data } = await client.submitTransaction( 209 [{ txId: "fake-tx-id123", index: 0, sig: "ZmFrZQ==" }], 210 [ 211 { owner: "did:plc:valid1234567", amount: 50 }, 212 { owner: "did:plc:valid2345678", amount: -10 }, 213 ] 214 ); 215 expect(status).toBe(400); 216 expect(data.error).toBe("INVALID_OUTPUT_AMOUNT"); 217 }); 218 }); 219 220 describe("response structure (on success)", () => { 221 // Note: Testing successful submission requires valid signatures 222 // which would need access to private keys 223 // These tests document the expected response structure 224 225 test("successful response would have txId", async () => { 226 // This is documentation - actual success requires valid signature 227 // Successful response should have: 228 // { txId: string, uri: string, commit: { cid: string, rev: string } } 229 expect(true).toBe(true); 230 }); 231 232 test("successful response would have uri", async () => { 233 // URI format: at://{entity-did}/cash.attoshi.tx/{txId} 234 expect(true).toBe(true); 235 }); 236 237 test("successful response would have commit info", async () => { 238 // commit: { cid: string, rev: string } 239 expect(true).toBe(true); 240 }); 241 }); 242});