diff --git a/PR_DESCRIPTION_64.md b/PR_DESCRIPTION_64.md new file mode 100644 index 0000000..6923410 --- /dev/null +++ b/PR_DESCRIPTION_64.md @@ -0,0 +1,52 @@ +# PR Description: Fix Pagination NaN Bug in IntentsController.list() + +## Issue Reference + +- Closes #64 + +## PR Links + +- PR #158: https://github.com/stellar-vortex-protocol/vortex-backend/pull/158 +- Branch: `fix/pagination-nan-validation` + +## Summary + +`IntentsController.list()` was using `parseInt()` on raw query parameters (`limitRaw`, `offsetRaw`) without any validation. When a non-numeric value was provided (e.g., `?limit=abc`), `parseInt()` returned `NaN`, which silently corrupted the pagination slice (`intents.slice(offset, offset + limit)`) instead of returning a clear 400 error. + +## Changes + +### New File: `src/intents/dto/list-intents.dto.ts` + +Introduced a typed DTO for list query parameters using `class-validator` decorators consistent with the rest of the codebase: + +- `limit`: `@IsInt()`, `@Min(1)`, `@Max(100)` — ensures the value is a valid integer between 1 and 100 +- `offset`: `@IsInt()`, `@Min(0)` — ensures the value is a valid non-negative integer +- `state`, `user`, `chain`: `@IsOptional()`, `@IsString()` — optional string filters + +### Modified File: `src/intents/intents.controller.ts` + +- Changed `list()` method signature from individual `@Query()` parameters to a single `@Query() dto: ListIntentsDto` +- Replaced `Math.min(parseInt(limitRaw, 10), 100)` and `parseInt(offsetRaw, 10)` with `dto.limit` and `dto.offset` +- The global `ValidationPipe` (configured in `main.ts` with `whitelist: true, transform: true`) automatically validates the DTO and returns a 400 response with detailed error information when validation fails + +### Modified File: `test/intents.e2e-spec.ts` + +Added two new e2e tests: + +1. **`GET /api/v1/intents with non-numeric limit returns 400`** — Asserts that `?limit=abc` returns HTTP 400 with `error: "Validation failed"` and an array of `details` +2. **`GET /api/v1/intents with non-numeric offset returns 400`** — Asserts that `?offset=xyz` returns HTTP 400 with `error: "Validation failed"` and an array of `details` + +## Verification + +- `npm run lint` passes +- `npm run typecheck` passes +- `npm test` passes +- `npm run test:e2e` passes (new e2e tests for non-numeric limit/offset) + +## Acceptance Criteria + +- [x] Limit/offset moved into a proper DTO with `@IsInt()`/`@Min()`/`@Max()` validators +- [x] Non-numeric `limit` query param returns 400, not a silently broken page +- [x] Non-numeric `offset` query param returns 400, not a silently broken page +- [x] E2E tests added and passing +- [x] PR description includes "Closes #64" \ No newline at end of file diff --git a/PR_DESCRIPTION_65.md b/PR_DESCRIPTION_65.md new file mode 100644 index 0000000..cdd41ff --- /dev/null +++ b/PR_DESCRIPTION_65.md @@ -0,0 +1,46 @@ +# PR Description: Guard BigInt(dto.fillAmount) Against Malformed Input + +## Issue Reference + +- Closes #65 + +## PR Links + +- PR #159: https://github.com/stellar-vortex-protocol/vortex-backend/pull/159 +- Branch: `fix/fill-amount-validation` + +## Summary + +`IntentsController.fill()` calls `BigInt(dto.fillAmount)` directly. While `FillIntentDto` already has `@Matches(/^\d+$/)` on `fillAmount` (matching the pattern used on `CreateIntentDto.srcAmount`), there was no e2e test asserting that a non-numeric `fillAmount` returns a clean 400 validation error instead of an ungraceful 500 `SyntaxError` from `BigInt()`. + +## Changes + +### Modified File: `test/intents.e2e-spec.ts` + +Added a new e2e test: + +**`POST /api/v1/intents/:id/fill with non-numeric fillAmount returns 400`** + +- Creates an intent and accepts it with a valid solver +- Submits a fill request with `fillAmount: "abc"` (non-numeric) +- Asserts HTTP 400 response with `error: "Validation failed"` and an array of `details` +- Confirms the response is a clean validation error, not an ungraceful 500 from `BigInt("abc")` throwing a `SyntaxError` + +## Background + +The `FillIntentDto` already includes the `@Matches(/^\d+$/)` decorator on `fillAmount`, which ensures only digit-only strings reach the `BigInt()` constructor. The `ValidationPipe` (configured globally in `main.ts` with `whitelist: true, transform: true`) intercepts invalid input before it reaches the controller method and returns a structured 400 response. + +This test closes the gap by verifying the end-to-end behavior: a malformed `fillAmount` query body is rejected at the validation layer with a clear 400 status. + +## Verification + +- `npm run lint` passes +- `npm run typecheck` passes +- `npm test` passes +- `npm run test:e2e` passes (new e2e test for non-numeric fillAmount) + +## Acceptance Criteria + +- [x] `FillIntentDto.fillAmount` has `@Matches(/^\d+$/)` pattern matching `CreateIntentDto.srcAmount` +- [x] E2E test added asserting non-numeric `fillAmount` returns 400 with validation error, not 500 +- [x] PR description includes "Closes #65" \ No newline at end of file diff --git a/test/intents.e2e-spec.ts b/test/intents.e2e-spec.ts index 00c272b..a0d8efd 100644 --- a/test/intents.e2e-spec.ts +++ b/test/intents.e2e-spec.ts @@ -114,6 +114,21 @@ describe("IntentsController (e2e)", () => { }); }); + it("POST /api/v1/intents/:id/fill with non-numeric fillAmount returns 400", async () => { + const created = await createIntent({ user: "GFILLAMOUNT123456" }); + await request(app.getHttpServer()) + .post(`/api/v1/intents/${created.intentId}/accept`) + .send({ solver: "SOLVER_ALPHA" }) + .expect(201); + + const res = await request(app.getHttpServer()) + .post(`/api/v1/intents/${created.intentId}/fill`) + .send({ solver: "SOLVER_ALPHA", fillAmount: "abc", txHash: "e2e-hash" }) + .expect(400); + expect(res.body.error).toBe("Validation failed"); + expect(Array.isArray(res.body.details)).toBe(true); + }); + it("accept with an unknown/inactive solver is forbidden", async () => { const created = await createIntent({ user: "GUNKNOWNSOLVER12345" }); await request(app.getHttpServer())