diff --git a/BOUNDARY_TESTS_SUMMARY.md b/BOUNDARY_TESTS_SUMMARY.md new file mode 100644 index 0000000..1d0b129 --- /dev/null +++ b/BOUNDARY_TESTS_SUMMARY.md @@ -0,0 +1,68 @@ +# Boundary Tests Implementation Summary + +## Overview +Added comprehensive boundary tests for the POST /api/streams route validation rules, specifically for `durationSeconds` and `totalAmount` parameters. + +## Tests Added + +### Duration Boundary Tests +Located in: `backend/src/index.test.ts` (lines ~618-664) + +1. **Test: durationSeconds = 59 (below minimum)** + - Sends request with `durationSeconds: 59` + - Expects: `400` status code + - Expects: Error message "durationSeconds must be at least 60 seconds" + - Validates the lower boundary is enforced + +2. **Test: durationSeconds = 60 (minimum boundary)** + - Sends request with `durationSeconds: 60` + - Expects: `201` status code (success) + - Validates the exact minimum boundary is accepted + +### Amount Precision Boundary Tests +Located in: `backend/src/index.test.ts` (lines ~666-744) + +1. **Test: totalAmount = 0.0000001 (1 stroop - minimum valid)** + - Sends request with `totalAmount: 0.0000001` (7 decimal places) + - Expects: `201` status code (success) + - Validates that the smallest Stellar amount (1 stroop) is accepted + +2. **Test: totalAmount = 0** + - Sends request with `totalAmount: 0` + - Expects: `400` status code + - Expects: Error message "Amount must be greater than zero" + - Validates zero amounts are rejected + +3. **Test: totalAmount with 8 decimal places** + - Sends request with `totalAmount: 100.12345678` (8 decimal places) + - Expects: `400` status code + - Expects: Error message "Amount cannot have more than 7 decimal places" + - Validates precision limit is enforced + +4. **Test: totalAmount with exactly 7 decimal places** + - Sends request with `totalAmount: 100.1234567` (7 decimal places) + - Expects: `201` status code (success) + - Validates the maximum precision boundary is accepted + +## Validation Schema Reference +The validation logic tested here is implemented in: +- `backend/src/validation/schemas.ts` + - `durationSecondsSchema`: Enforces minimum of 60 seconds + - `totalAmountSchema`: Enforces positive values and maximum 7 decimal places + +## Acceptance Criteria ✅ +- ✅ Duration boundary (59 vs 60) is tested explicitly +- ✅ Stroop-level minimum amount (0.0000001) is accepted +- ✅ More than 7 decimal places is rejected with clear message "Amount cannot have more than 7 decimal places" +- ✅ Zero amount is rejected with clear message "Amount must be greater than zero" +- ✅ All tests follow the existing test pattern with proper assertions + +## Test Structure +All boundary tests are organized under the existing `describe("POST /api/streams")` block with two sub-describe blocks: +1. `describe("Duration boundary tests")` +2. `describe("Amount precision boundary tests")` + +This organization makes it easy to find and maintain these specific boundary test cases. + +## Additional Fix +Fixed a syntax error in `backend/src/index.ts` (lines 1040-1090) where duplicate code was causing compilation failures. Removed the duplicate query parsing and filtering logic in the recipients route handler. diff --git a/backend/package-lock.json b/backend/package-lock.json index 94709c6..5a9fc64 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -728,6 +728,7 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, + "license": "MIT" "license": "MIT", "peer": true }, @@ -1509,6 +1510,7 @@ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", "dev": true, + "license": "MIT" "license": "MIT", "peer": true }, @@ -1600,6 +1602,7 @@ "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2241,6 +2244,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2278,6 +2282,7 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3077,6 +3082,7 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, + "license": "MIT" "license": "MIT", "peer": true }, @@ -3528,6 +3534,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, + "license": "MIT" "license": "MIT", "peer": true }, @@ -3553,6 +3560,7 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, + "license": "MIT" "license": "MIT", "peer": true }, @@ -3691,6 +3699,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -3759,6 +3768,7 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, + "license": "MIT" "license": "MIT", "peer": true }, @@ -3767,6 +3777,7 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, + "license": "MIT" "license": "MIT", "peer": true }, @@ -3892,6 +3903,7 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", "dev": true, + "license": "ISC" "license": "ISC", "peer": true }, @@ -4709,6 +4721,7 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, + "license": "MIT" "license": "MIT", "peer": true }, @@ -4724,6 +4737,7 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, + "license": "MIT" "license": "MIT", "peer": true }, @@ -5470,6 +5484,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", @@ -6797,6 +6812,7 @@ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7050,6 +7066,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7285,6 +7302,7 @@ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", diff --git a/backend/src/index.test.ts b/backend/src/index.test.ts index d0737aa..7c9fada 100644 --- a/backend/src/index.test.ts +++ b/backend/src/index.test.ts @@ -612,6 +612,134 @@ it("returns 400 when durationSeconds is below the 60-second minimum", async () = ]), ); }); + + // ══════════════════════════════════════════════════════════════════════════ + // Boundary Tests for Duration and Amount Validation + // ══════════════════════════════════════════════════════════════════════════ + + describe("Duration boundary tests", () => { + it("returns 400 when durationSeconds is 59 (below minimum)", async () => { + const response = await request(app) + .post("/api/streams") + .set("Authorization", "Bearer mock_token") + .send({ + sender: SENDER_A, + recipient: RECIPIENT_1, + assetCode: "USDC", + totalAmount: 100, + durationSeconds: 59, + }); + + expect(response.status).toBe(400); + expect(response.body.code).toBe("VALIDATION_ERROR"); + expect(response.body.error).toContain("durationSeconds must be at least 60 seconds"); + expect(response.body.details).toEqual( + expect.arrayContaining([ + expect.objectContaining({ field: "durationSeconds" }), + ]), + ); + }); + + it("returns 201 when durationSeconds is exactly 60 (minimum boundary)", async () => { + const response = await request(app) + .post("/api/streams") + .set("Authorization", "Bearer mock_token") + .send({ + sender: SENDER_A, + recipient: RECIPIENT_1, + assetCode: "USDC", + totalAmount: 100, + durationSeconds: 60, + }); + + expect(response.status).toBe(201); + expect(response.body.data).toMatchObject({ + durationSeconds: 60, + }); + }); + }); + + describe("Amount precision boundary tests", () => { + it("returns 201 when totalAmount is 0.0000001 (1 stroop - minimum valid)", async () => { + const response = await request(app) + .post("/api/streams") + .set("Authorization", "Bearer mock_token") + .send({ + sender: SENDER_A, + recipient: RECIPIENT_1, + assetCode: "USDC", + totalAmount: 0.0000001, + durationSeconds: 120, + }); + + expect(response.status).toBe(201); + expect(response.body.data).toMatchObject({ + totalAmount: 0.0000001, + }); + }); + + it("returns 400 when totalAmount is 0", async () => { + const response = await request(app) + .post("/api/streams") + .set("Authorization", "Bearer mock_token") + .send({ + sender: SENDER_A, + recipient: RECIPIENT_1, + assetCode: "USDC", + totalAmount: 0, + durationSeconds: 120, + }); + + expect(response.status).toBe(400); + expect(response.body.code).toBe("VALIDATION_ERROR"); + expect(response.body.error).toContain("Amount must be greater than zero"); + expect(response.body.details).toEqual( + expect.arrayContaining([ + expect.objectContaining({ field: "totalAmount" }), + ]), + ); + }); + + it("returns 400 when totalAmount has more than 7 decimal places", async () => { + const response = await request(app) + .post("/api/streams") + .set("Authorization", "Bearer mock_token") + .send({ + sender: SENDER_A, + recipient: RECIPIENT_1, + assetCode: "USDC", + totalAmount: 100.12345678, // 8 decimal places + durationSeconds: 120, + }); + + expect(response.status).toBe(400); + expect(response.body.code).toBe("VALIDATION_ERROR"); + expect(response.body.error).toContain("Amount cannot have more than 7 decimal places"); + expect(response.body.details).toEqual( + expect.arrayContaining([ + expect.objectContaining({ field: "totalAmount" }), + ]), + ); + }); + + it("returns 201 when totalAmount has exactly 7 decimal places", async () => { + const response = await request(app) + .post("/api/streams") + .set("Authorization", "Bearer mock_token") + .send({ + sender: SENDER_A, + recipient: RECIPIENT_1, + assetCode: "USDC", + totalAmount: 100.1234567, // exactly 7 decimal places + durationSeconds: 120, + }); + + expect(response.status).toBe(201); + expect(response.body.data).toMatchObject({ + totalAmount: 100.1234567, + }); + }); + }); }); // ---------------------------------------------------------------------------