|
10 | 10 | const request = require("supertest"); |
11 | 11 | const app = require("../../app"); |
12 | 12 | const { cacheClear } = require("../../services/transactionService"); |
| 13 | +const stellarRouter = require("../../routes/stellar"); |
| 14 | +const stellarConfig = require("../../config/stellar"); |
13 | 15 |
|
14 | 16 | // ── Mock the Horizon server ─────────────────────────────────────────────────── |
15 | 17 |
|
@@ -100,6 +102,8 @@ function mockHorizonSuccess(txs, ops) { |
100 | 102 | beforeEach(() => { |
101 | 103 | cacheClear(); |
102 | 104 | jest.clearAllMocks(); |
| 105 | + stellarRouter.resetFundRateLimiter(); |
| 106 | + stellarConfig.NETWORK = "testnet"; |
103 | 107 | }); |
104 | 108 |
|
105 | 109 | // ── Tests ───────────────────────────────────────────────────────────────────── |
@@ -431,3 +435,105 @@ describe("GET /api/v1/stellar/account/:publicKey/transactions", () => { |
431 | 435 | expect(res.body.meta.hasMore).toBe(false); |
432 | 436 | }); |
433 | 437 | }); |
| 438 | + |
| 439 | +describe("POST /api/v1/stellar/fund", () => { |
| 440 | + const originalFetch = global.fetch; |
| 441 | + |
| 442 | + afterEach(() => { |
| 443 | + global.fetch = originalFetch; |
| 444 | + }); |
| 445 | + |
| 446 | + it("funds a valid Testnet account", async () => { |
| 447 | + global.fetch = jest.fn().mockResolvedValue({ |
| 448 | + ok: true, |
| 449 | + json: jest.fn().mockResolvedValue({ hash: "friendbot-tx-hash" }), |
| 450 | + }); |
| 451 | + |
| 452 | + const res = await request(app) |
| 453 | + .post("/api/v1/stellar/fund") |
| 454 | + .send({ publicKey: VALID_KEY }) |
| 455 | + .expect(200); |
| 456 | + |
| 457 | + expect(res.body).toEqual({ |
| 458 | + publicKey: VALID_KEY, |
| 459 | + funded: true, |
| 460 | + hash: "friendbot-tx-hash", |
| 461 | + }); |
| 462 | + expect(global.fetch).toHaveBeenCalledWith( |
| 463 | + expect.stringContaining(`addr=${VALID_KEY}`) |
| 464 | + ); |
| 465 | + }); |
| 466 | + |
| 467 | + it("returns 400 when publicKey is missing", async () => { |
| 468 | + const res = await request(app).post("/api/v1/stellar/fund").send({}).expect(400); |
| 469 | + expect(res.body.error).toMatch(/publicKey is required/i); |
| 470 | + }); |
| 471 | + |
| 472 | + it("returns 400 for a malformed publicKey", async () => { |
| 473 | + const res = await request(app) |
| 474 | + .post("/api/v1/stellar/fund") |
| 475 | + .send({ publicKey: "not-a-real-key" }) |
| 476 | + .expect(400); |
| 477 | + expect(res.body.error).toMatch(/valid Stellar public key/i); |
| 478 | + }); |
| 479 | + |
| 480 | + it("returns 403 when the network is configured for Mainnet", async () => { |
| 481 | + stellarConfig.NETWORK = "mainnet"; |
| 482 | + global.fetch = jest.fn(); |
| 483 | + |
| 484 | + const res = await request(app) |
| 485 | + .post("/api/v1/stellar/fund") |
| 486 | + .send({ publicKey: VALID_KEY }) |
| 487 | + .expect(403); |
| 488 | + |
| 489 | + expect(res.body).toEqual({ |
| 490 | + error: "Friendbot is only available on Stellar Testnet.", |
| 491 | + }); |
| 492 | + expect(global.fetch).not.toHaveBeenCalled(); |
| 493 | + }); |
| 494 | + |
| 495 | + it("returns a structured 500 error when Friendbot fails", async () => { |
| 496 | + global.fetch = jest.fn().mockResolvedValue({ |
| 497 | + ok: false, |
| 498 | + status: 400, |
| 499 | + json: jest.fn().mockResolvedValue({ detail: "createAccountAlreadyExist" }), |
| 500 | + }); |
| 501 | + |
| 502 | + const res = await request(app) |
| 503 | + .post("/api/v1/stellar/fund") |
| 504 | + .send({ publicKey: VALID_KEY }) |
| 505 | + .expect(500); |
| 506 | + |
| 507 | + expect(res.body).toHaveProperty("error"); |
| 508 | + }); |
| 509 | + |
| 510 | + it("returns a structured 500 error when Friendbot is unreachable", async () => { |
| 511 | + global.fetch = jest.fn().mockRejectedValue(new Error("network down")); |
| 512 | + |
| 513 | + const res = await request(app) |
| 514 | + .post("/api/v1/stellar/fund") |
| 515 | + .send({ publicKey: VALID_KEY }) |
| 516 | + .expect(500); |
| 517 | + |
| 518 | + expect(res.body).toHaveProperty("error"); |
| 519 | + }); |
| 520 | + |
| 521 | + it("returns 429 after the per-IP limit (1 per hour) is exceeded", async () => { |
| 522 | + global.fetch = jest.fn().mockResolvedValue({ |
| 523 | + ok: true, |
| 524 | + json: jest.fn().mockResolvedValue({ hash: "friendbot-tx-hash" }), |
| 525 | + }); |
| 526 | + |
| 527 | + await request(app) |
| 528 | + .post("/api/v1/stellar/fund") |
| 529 | + .send({ publicKey: VALID_KEY }) |
| 530 | + .expect(200); |
| 531 | + |
| 532 | + const res = await request(app) |
| 533 | + .post("/api/v1/stellar/fund") |
| 534 | + .send({ publicKey: VALID_KEY }) |
| 535 | + .expect(429); |
| 536 | + |
| 537 | + expect(res.body).toHaveProperty("error"); |
| 538 | + }); |
| 539 | +}); |
0 commit comments