From bfd94a3ce1560d10a9466b0529c0fb19efb5d1d9 Mon Sep 17 00:00:00 2001 From: Ryan Miles Date: Fri, 9 Oct 2026 13:25:09 -0700 Subject: [PATCH 1/2] Enforce default row limit and validation for AccountingCatalogController (#1920) --- .../watersupply/AccountingCatalogController.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCatalogController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCatalogController.java index e7153487f..b61febbc8 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCatalogController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCatalogController.java @@ -83,6 +83,7 @@ public class AccountingCatalogController implements Handler { private static final String TAG = "Pump Accounting"; private static final String ROW_LIMIT = "row-limit"; private static final String ASCENDING = "ascending"; + private static final int DEFAULT_ROW_LIMIT = 500; private final MetricRegistry metrics; private Timer.Context markAndTime(String subject) { @@ -117,7 +118,7 @@ protected WaterSupplyAccountingDao getWaterSupplyAccountingDao(DSLContext dsl) { @OpenApiParam(name = ASCENDING, description = "Whether or not the entries should be returned " + "in ascending order. Defaults to TRUE.", type = Boolean.class), @OpenApiParam(name = ROW_LIMIT, description = "The maximum number of rows to return. " - + "Defaults to 0, which means no limit.", type = Integer.class) + + "Defaults to " + DEFAULT_ROW_LIMIT + ".", type = Integer.class) }, pathParams = { @OpenApiParam(name = OFFICE, description = "The office ID of the project the " @@ -165,7 +166,15 @@ public void handle(Context ctx) { .getOrDefault(true); final boolean endInclusive = ctx.queryParamAsClass(END_TIME_INCLUSIVE, Boolean.class).getOrDefault(true); final boolean ascending = ctx.queryParamAsClass(ASCENDING, Boolean.class).getOrDefault(true); - final int rowLimit = ctx.queryParamAsClass(ROW_LIMIT, Integer.class).getOrDefault(0); + final int rowLimit = ctx.queryParamAsClass(ROW_LIMIT, Integer.class).getOrDefault(DEFAULT_ROW_LIMIT); + + if (rowLimit == 0) { + CdaError error = new CdaError("Unable to retrieve accounting - " + ROW_LIMIT + " must be greater than 0."); + LOGGER.atSevere().log("Unable to retrieve accounting - " + ROW_LIMIT + " must be greater than 0."); + ctx.status(HttpServletResponse.SC_BAD_REQUEST).json(error); + return; + } + DSLContext dsl = getDslContext(ctx); String formatHeader = ctx.headerAsClass(Header.ACCEPT, String.class).getOrDefault(Formats.JSONV1); From b6c9723219ed1fc38a3fe477d8b062452c9004c7 Mon Sep 17 00:00:00 2001 From: Ryan Miles Date: Fri, 16 Oct 2026 16:01:56 -0700 Subject: [PATCH 2/2] Adding integration test for validating 0 row limit produces a bad request response. --- .../WaterSupplyAccountingControllerIT.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/WaterSupplyAccountingControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/WaterSupplyAccountingControllerIT.java index ff62380c5..01e13303e 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/WaterSupplyAccountingControllerIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/WaterSupplyAccountingControllerIT.java @@ -799,6 +799,77 @@ void testStoreNonExistentTransferType() throws Exception { .assertThat() .statusCode(is(HttpServletResponse.SC_BAD_REQUEST)) ; + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(START_TIME, "2005-04-05T00:00:00Z") + .queryParam(END_TIME, "2335-04-06T00:00:00Z") + .queryParam(START_INCLUSIVE, "true") + .queryParam(END_INCLUSIVE, "true") + .queryParam(ASCENDING, "true") + .queryParam(ROW_LIMIT, 100) + .queryParam(UNIT, "cfs") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/" + + contract.getWaterUser().getEntityName() + "/contracts/" + + contract.getContractId().getName() + "/accounting") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + } + + @Test + void testGet0RowLimit() throws Exception { + // 1 Store data + // 2 Retrieve accounting with 0 row limit + // 3 Verify failure from row limit + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + String json = JsonV1.buildObjectMapper().writeValueAsString(waterSupplyAccounting); + + // create pump accounting + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/" + + contract.getWaterUser().getEntityName() + "/contracts/" + + contract.getContractId().getName() + "/accounting") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)) + ; + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(START_TIME, "2005-04-05T00:00:00Z") + .queryParam(END_TIME, "2335-04-06T00:00:00Z") + .queryParam(START_INCLUSIVE, "true") + .queryParam(END_INCLUSIVE, "true") + .queryParam(ASCENDING, "true") + .queryParam(ROW_LIMIT, 0) + .queryParam(UNIT, "cfs") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/" + + contract.getWaterUser().getEntityName() + "/contracts/" + + contract.getContractId().getName() + "/accounting") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_BAD_REQUEST)); } private static Location buildTestLocation(String name, String locationKind) {