From 49be83226b89edaf1826828a0376b90660727f63 Mon Sep 17 00:00:00 2001 From: Adam Korynta Date: Tue, 11 Aug 2026 14:39:26 -0700 Subject: [PATCH 1/3] Add integration tests and CRUD operations for CWMS Pools adds create, rename, delete functionality for pools --- .../main/java/cwms/cda/api/Controllers.java | 1 + .../java/cwms/cda/api/PoolController.java | 134 +++++-- .../main/java/cwms/cda/data/dao/PoolDao.java | 53 ++- .../src/main/java/cwms/cda/data/dto/Pool.java | 7 + .../java/cwms/cda/api/PoolControllerIT.java | 353 ++++++++++++++++++ .../src/test/resources/cwms/cda/api/pool.json | 13 + .../resources/cwms/cda/api/project_pool.json | 22 ++ 7 files changed, 550 insertions(+), 33 deletions(-) create mode 100644 cwms-data-api/src/test/java/cwms/cda/api/PoolControllerIT.java create mode 100644 cwms-data-api/src/test/resources/cwms/cda/api/pool.json create mode 100644 cwms-data-api/src/test/resources/cwms/cda/api/project_pool.json diff --git a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java index cfbbed401c..b3f58a0a3d 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java @@ -87,6 +87,7 @@ public final class Controllers { public static final String BLOB_ID = "blob-id"; public static final String INCLUDE_VALUES = "include-values"; public static final String FAIL_IF_EXISTS = "fail-if-exists"; + public static final String CREATE_POOL_NAME = "create-pool-name"; public static final String IGNORE_NULLS = "ignore-nulls"; public static final String EFFECTIVE_DATE = "effective-date"; public static final String DATE = "date"; diff --git a/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java b/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java index c806b6b68c..cbc8e16d29 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java @@ -1,26 +1,7 @@ package cwms.cda.api; import static com.codahale.metrics.MetricRegistry.name; -import static cwms.cda.api.Controllers.ANY_MASK; -import static cwms.cda.api.Controllers.BOTTOM_MASK; -import static cwms.cda.api.Controllers.CURSOR; -import static cwms.cda.api.Controllers.GET_ALL; -import static cwms.cda.api.Controllers.GET_ONE; -import static cwms.cda.api.Controllers.ID_MASK; -import static cwms.cda.api.Controllers.INCLUDE_EXPLICIT; -import static cwms.cda.api.Controllers.INCLUDE_IMPLICIT; -import static cwms.cda.api.Controllers.NAME_MASK; -import static cwms.cda.api.Controllers.OFFICE; -import static cwms.cda.api.Controllers.PAGE; -import static cwms.cda.api.Controllers.PAGE_SIZE; -import static cwms.cda.api.Controllers.POOL_ID; -import static cwms.cda.api.Controllers.PROJECT_ID; -import static cwms.cda.api.Controllers.RESULTS; -import static cwms.cda.api.Controllers.SIZE; -import static cwms.cda.api.Controllers.STATUS_200; -import static cwms.cda.api.Controllers.STATUS_404; -import static cwms.cda.api.Controllers.STATUS_501; -import static cwms.cda.api.Controllers.TOP_MASK; +import static cwms.cda.api.Controllers.*; import static cwms.cda.api.Controllers.queryParamAsClass; import static cwms.cda.api.Controllers.requiredParam; import static cwms.cda.data.dao.JooqDao.getDslContext; @@ -31,24 +12,31 @@ import com.google.common.flogger.FluentLogger; import cwms.cda.api.errors.CdaError; import cwms.cda.api.errors.ExceptionTraceSupport; +import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dao.PoolDao; import cwms.cda.data.dto.Pool; import cwms.cda.data.dto.Pools; +import cwms.cda.data.dto.StatusResponse; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; import io.javalin.http.Context; +import io.javalin.plugin.openapi.annotations.HttpMethod; import io.javalin.plugin.openapi.annotations.OpenApi; import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; +import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; import java.io.IOException; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; -public class PoolController implements CrudHandler { +public final class PoolController implements CrudHandler { + + static final String TAG = "Pools"; + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final int defaultPageSize = 100; @@ -103,7 +91,7 @@ private Timer.Context markAndTime(String subject) { @OpenApiResponse(status = STATUS_501, description = "request format is not" + " implemented")}, description = "Returns Pools Data", - tags = {"Pools"}) + tags = {TAG}) @Override public void getAll(@NotNull Context ctx) { try (final Timer.Context timeContext = markAndTime(GET_ALL)) { @@ -191,7 +179,7 @@ public void getAll(@NotNull Context ctx) { + "inputs provided the Location Category was not found."), @OpenApiResponse(status = STATUS_501, description = "request format is not " + "implemented")}, - description = "Retrieves requested Pool", tags = {"Pools"}) + description = "Retrieves requested Pool", tags = {TAG}) @Override public void getOne(@NotNull Context ctx, @NotNull String poolId) { try (final Timer.Context timeContext = markAndTime(GET_ONE)) { @@ -246,21 +234,105 @@ public void getOne(@NotNull Context ctx, @NotNull String poolId) { } } - @OpenApi(ignore = true) + @OpenApi( + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(from = Pool.class, type = Formats.JSONV2) + }, + required = true), + queryParams = { + @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, + description = "Create will fail if provided ID already exists. Default: true"), + @OpenApiParam(name = CREATE_POOL_NAME, type = Boolean.class, + description = "Create will create the pool name if it doesn't already exists. Default: true") + }, + description = "Create CWMS Pool", + method = HttpMethod.POST, + tags = {TAG}, + responses = { + @OpenApiResponse(status = STATUS_204, description = "Pool successfully stored to CWMS.") + } + ) @Override public void create(@NotNull Context ctx) { - ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); + try (Timer.Context ignored = markAndTime(CREATE)) { + DSLContext dsl = getDslContext(ctx); + boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class) + .getOrDefault(true); + boolean createPoolName = ctx.queryParamAsClass(CREATE_POOL_NAME, Boolean.class) + .getOrDefault(true); + String formatHeader = ctx.req.getContentType(); + ContentType contentType = Formats.parseHeader(formatHeader, Pool.class); + Pool pool = Formats.parseContent(contentType, ctx.body(), Pool.class); + PoolDao dao = new PoolDao(dsl); + dao.createPool(pool, failIfExists, createPoolName); + StatusResponse re = new StatusResponse(pool.getPoolName().getOfficeId(), + "Pool: " + pool.getPoolName().getPoolName() + " successfully created.", + pool.getPoolName().getPoolName()); + ctx.status(HttpServletResponse.SC_CREATED).json(re); + } } - @OpenApi(ignore = true) + @OpenApi( + pathParams = { + @OpenApiParam(name = NAME, description = "Specifies the name of " + + "the pool to be renamed."), + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office of " + + "the pool to be renamed."), + @OpenApiParam(name = NAME, required = true, description = "Specifies the new pool name. ") + }, + description = "Rename CWMS Pool", + method = HttpMethod.PATCH, + tags = {TAG}, + responses = { + @OpenApiResponse(status = STATUS_204, description = "Pool successfully renamed in CWMS.") + } + ) @Override - public void update(@NotNull Context ctx, @NotNull String locationCode) { - ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); + public void update(@NotNull Context ctx, @NotNull String poolId) { + try (Timer.Context ignored = markAndTime(UPDATE)) { + String office = requiredParam(ctx, OFFICE); + String newName = requiredParam(ctx, NAME); + DSLContext dsl = getDslContext(ctx); + PoolDao dao = new PoolDao(dsl); + dao.renamePool(office, poolId, newName); + StatusResponse re = new StatusResponse(office, "Pool: " + poolId + " successfully renamed to: " + newName, newName); + ctx.status(HttpServletResponse.SC_OK).json(re); + } } - @OpenApi(ignore = true) + @OpenApi( + pathParams = { + @OpenApiParam(name = NAME, description = "Specifies the name of " + + "the pool to be deleted."), + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office of " + + "the pool to be deleted."), + @OpenApiParam(name = METHOD, description = "Specifies the delete method used. " + + "Defaults to \"DELETE_KEY\"", + type = JooqDao.DeleteMethod.class) + }, + description = "Delete CWMS Pool", + method = HttpMethod.DELETE, + tags = {TAG}, + responses = { + @OpenApiResponse(status = STATUS_200, description = "Pool successfully deleted from CWMS."), + @OpenApiResponse(status = STATUS_404, description = "Based on the combination of " + + "inputs provided the pool was not found.") + } + ) @Override - public void delete(@NotNull Context ctx, @NotNull String locationCode) { - ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); + public void delete(@NotNull Context ctx, @NotNull String poolId) { + try (Timer.Context ignored = markAndTime(DELETE)) { + DSLContext dsl = getDslContext(ctx); + PoolDao dao = new PoolDao(dsl); + String office = requiredParam(ctx, OFFICE); + String projectId = requiredParam(ctx, PROJECT_ID); + dao.deletePool(office, projectId, poolId); + ctx.status(HttpServletResponse.SC_NO_CONTENT); + } } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/PoolDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/PoolDao.java index 2f0f3d2e68..e248a87633 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/PoolDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/PoolDao.java @@ -2,21 +2,22 @@ import static java.util.stream.Collectors.toList; +import com.google.common.flogger.FluentLogger; +import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dto.Pool; import cwms.cda.data.dto.PoolNameType; import cwms.cda.data.dto.Pools; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import com.google.common.flogger.FluentLogger; import java.util.stream.Collectors; import java.util.stream.Stream; - import org.jetbrains.annotations.NotNull; import org.jooq.Condition; import org.jooq.DSLContext; import org.jooq.Record; import org.jooq.Result; +import org.jooq.exception.DataAccessException; import org.jooq.exception.TooManyRowsException; import org.jooq.impl.DSL; import usace.cwms.db.jooq.codegen.packages.CWMS_POOL_PACKAGE; @@ -217,4 +218,52 @@ public Pools retrievePools(String cursor, int pageSize, return builder.build(); } + public void deletePool(String office, String projectId, String poolId) { + connection(dsl, c -> CWMS_POOL_PACKAGE.call_DELETE_POOL( + getDslContext(c, office).configuration(), + projectId, poolId, office) + ); + } + + public void createPool(Pool pool, boolean failIfExists, boolean createPoolName) { + String projectId = pool.getProjectId(); + String office = pool.getPoolName().getOfficeId(); + String poolId = pool.getPoolName().getPoolName(); + String bottomLevelId = pool.getBottomLevelId(); + String topLevelId = pool.getTopLevelId(); + Number attribute = pool.getAttribute(); + String description = pool.getDescription(); + String clobText = pool.getClobText(); + connection(dsl, c -> CWMS_POOL_PACKAGE.call_STORE_POOL2( + getDslContext(c, office).configuration(), + projectId, + poolId, + bottomLevelId, + topLevelId, + attribute, + description, + clobText, + formatBool(failIfExists), + formatBool(createPoolName), + office) + ); + } + + public void renamePool(String office, String poolId, String newName) { + try { + connection(dsl, c -> CWMS_POOL_PACKAGE.call_RENAME_POOL( + getDslContext(c, office).configuration(), + poolId, + newName, + office) + ); + } catch (DataAccessException e) { + //Typo being address in: https://github.com/HydrologicEngineeringCenter/cwms-database/pull/215 + if(e.getMessage().contains("ITEM DOES NOT EXIST")) { + throw new NotFoundException(e); + } else { + throw e; + } + } + } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Pool.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Pool.java index 5636fe3a55..663371980c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Pool.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Pool.java @@ -1,11 +1,18 @@ package cwms.cda.data.dto; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; import cwms.cda.formatters.json.JsonV2; import java.util.regex.Matcher; import java.util.regex.Pattern; +@JsonDeserialize(builder = Pool.Builder.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) @FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) public class Pool extends CwmsDTOBase { diff --git a/cwms-data-api/src/test/java/cwms/cda/api/PoolControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/PoolControllerIT.java new file mode 100644 index 0000000000..2f0a25894d --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/PoolControllerIT.java @@ -0,0 +1,353 @@ +/* + * MIT License + * + * Copyright (c) 2024 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package cwms.cda.api; + +import static cwms.cda.api.Controllers.*; +import static cwms.cda.data.dao.DaoTest.getDslContext; +import static cwms.cda.security.ApiKeyIdentityProvider.AUTH_HEADER; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import cwms.cda.api.errors.NotFoundException; +import cwms.cda.data.dao.DeleteRule; +import cwms.cda.data.dao.LocationLevelsDaoImpl; +import cwms.cda.data.dao.LocationsDaoImpl; +import cwms.cda.data.dao.location.kind.LocationUtil; +import cwms.cda.data.dto.Location; +import cwms.cda.data.dto.Pool; +import cwms.cda.data.dto.locationlevel.ConstantLocationLevel; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import fixtures.CwmsDataApiSetupCallback; +import fixtures.TestAccounts; +import io.restassured.filter.log.LogDetail; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import javax.servlet.http.HttpServletResponse; +import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import org.apache.commons.io.IOUtils; +import org.jooq.DSLContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import usace.cwms.db.jooq.codegen.packages.CWMS_PROJECT_PACKAGE; +import usace.cwms.db.jooq.codegen.udt.records.PROJECT_OBJ_T; + +final class PoolControllerIT extends DataApiTestIT { + + private static final Location PROJECT_LOC; + private static final Pool POOL; + private static final String OFFICE_ID = "office-id"; + private static final String MESSAGE = "message"; + private static final String IDENTIFIER = "identifier"; + + static { + try (InputStream projectStream = PoolControllerIT.class.getResourceAsStream( + "/cwms/cda/api/project_pool.json"); + InputStream poolStream = PoolControllerIT.class.getResourceAsStream("/cwms/cda/api/pool.json")) { + String projectLocJson = IOUtils.toString(projectStream, StandardCharsets.UTF_8); + PROJECT_LOC = Formats.parseContent(new ContentType(Formats.JSONV2), projectLocJson, Location.class); + String poolJson = IOUtils.toString(poolStream, StandardCharsets.UTF_8); + POOL = Formats.parseContent(new ContentType(Formats.JSONV2), poolJson, Pool.class); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + @BeforeAll + static void setup() throws Exception { + CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); + databaseLink.connection(c -> { + DSLContext context = getDslContext(c, PROJECT_LOC.getOfficeId()); + PROJECT_OBJ_T projectObjT = buildProject(); + CWMS_PROJECT_PACKAGE.call_STORE_PROJECT(context.configuration(), projectObjT, "T"); + LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(context); + String locationId = PROJECT_LOC.getName(); + dao.storeLocationLevel(new ConstantLocationLevel.Builder(locationId + "." + POOL.getBottomLevelId(), + Instant.now().truncatedTo(ChronoUnit.HOURS)).withConstantValue(100.0) + .build()); + dao.storeLocationLevel(new ConstantLocationLevel.Builder(locationId + "." + POOL.getTopLevelId(), + Instant.now().truncatedTo(ChronoUnit.HOURS)).withConstantValue(300.0) + .build()); + }, CwmsDataApiSetupCallback.getWebUser()); + } + + @AfterAll + static void tearDown() throws Exception { + + CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); + databaseLink.connection(c -> { + DSLContext context = getDslContext(c, PROJECT_LOC.getOfficeId()); + LocationsDaoImpl locationsDao = new LocationsDaoImpl(context); + try { + CWMS_PROJECT_PACKAGE.call_DELETE_PROJECT(context.configuration(), PROJECT_LOC.getName(), + DeleteRule.DELETE_ALL.getRule(), PROJECT_LOC.getOfficeId()); + + } catch (NotFoundException ex) { + /* only an error within the tests below. */ + } + try { + locationsDao.deleteLocation(PROJECT_LOC.getName(), PROJECT_LOC.getOfficeId(), true); + } catch (NotFoundException ex) { + /* only an error within the tests below. */ + } + }, CwmsDataApiSetupCallback.getWebUser()); + } + + @ParameterizedTest + @ValueSource(strings = {Formats.JSONV2, Formats.DEFAULT}) + void test_get_create_delete(String format) { + + // Structure of test: + // 1)Create the Pool + // 2)Retrieve the Pool and assert that it exists + // 3)Delete the Pool + // 4)Retrieve the Pool and assert that it does not exist + TestAccounts.KeyUser user = TestAccounts.KeyUser.SWT_NORMAL; + String json = Formats.format(Formats.parseHeader(Formats.JSONV2, Pool.class), POOL); + //Create the Pool + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV2) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, "false") + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/pools/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)) + .body(OFFICE_ID, equalTo(POOL.getPoolName().getOfficeId())) + .body(MESSAGE, notNullValue()) + .body(IDENTIFIER, equalTo(POOL.getPoolName().getPoolName())) + ; + String office = POOL.getPoolName().getOfficeId(); + // Retrieve the Pool and assert that it exists + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .queryParam(PROJECT_ID, POOL.getProjectId()) + .queryParam(OFFICE, POOL.getPoolName().getOfficeId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/pools/" + POOL.getPoolName().getPoolName()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("attribute", equalTo(POOL.getAttribute())) + .body("bottom-level-id", equalTo(POOL.getBottomLevelId())) + .body("top-level-id", equalTo(POOL.getTopLevelId())) + .body("clob-text", equalTo(POOL.getClobText())) + .body("description", equalTo(POOL.getDescription())) + .body("project-id", equalTo(POOL.getProjectId())) + .body("pool-name.pool-name", equalTo(POOL.getPoolName().getPoolName())) + .body("pool-name.office-id", equalTo(POOL.getPoolName().getOfficeId())) + ; + + // Delete a Pool + given() + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(OFFICE, office) + .queryParam(PROJECT_ID, POOL.getProjectId()) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/pools/" + POOL.getPoolName().getPoolName()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)) + ; + + // Retrieve a Pool and assert that it does not exist + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .queryParam(PROJECT_ID, POOL.getProjectId()) + .queryParam(OFFICE, POOL.getPoolName().getOfficeId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/pools/" + POOL.getPoolName().getPoolName()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)) + ; + } + + @Test + void test_update_does_not_exist() { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SWT_NORMAL; + given() + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(OFFICE, user.getOperatingOffice()) + .queryParam(PROJECT_ID, PROJECT_LOC.getName()) + .queryParam(NAME, "NewBogus") + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/pools/bogus") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)) + ; + } + + @Test + void test_delete_does_not_exist() { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SWT_NORMAL; + // Delete a Pool + given() + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(OFFICE, user.getOperatingOffice()) + .queryParam(PROJECT_ID, PROJECT_LOC.getName()) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/pools/" + Instant.now().toEpochMilli()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)) + ; + } + + @ParameterizedTest + @ValueSource(strings = {Formats.JSONV2, Formats.DEFAULT}) + void test_get_all(String format) { + + // Structure of test: + // 1)Create the Pool + // 2)Retrieve the Pool with getAll and assert that it exists + // 3)Delete the Pool + TestAccounts.KeyUser user = TestAccounts.KeyUser.SWT_NORMAL; + String json = Formats.format(Formats.parseHeader(Formats.JSONV2, Pool.class), POOL); + //Create the Pool + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV2) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, "false") + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/pools/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)) + .body(OFFICE_ID, equalTo(POOL.getPoolName().getOfficeId())) + .body(MESSAGE, notNullValue()) + .body(IDENTIFIER, equalTo(POOL.getPoolName().getPoolName())) + ; + String office = POOL.getPoolName().getOfficeId(); + // Retrieve the Pool and assert that it exists + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .queryParam(INCLUDE_EXPLICIT, "true") + .queryParam(INCLUDE_IMPLICIT, "true") + .queryParam(ID_MASK, POOL.getProjectId()) + .queryParam(OFFICE, POOL.getPoolName().getOfficeId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/pools/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("pools[0].attribute", equalTo(POOL.getAttribute())) + .body("pools[0].bottom-level-id", equalTo(POOL.getBottomLevelId())) + .body("pools[0].top-level-id", equalTo(POOL.getTopLevelId())) + .body("pools[0].clob-text", equalTo(POOL.getClobText())) + .body("pools[0].description", equalTo(POOL.getDescription())) + .body("pools[0].project-id", equalTo(POOL.getProjectId())) + .body("pools[0].pool-name.pool-name", equalTo(POOL.getPoolName().getPoolName())) + .body("pools[0].pool-name.office-id", equalTo(POOL.getPoolName().getOfficeId())) + ; + + // Delete a Pool + given() + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(OFFICE, office) + .queryParam(PROJECT_ID, POOL.getProjectId()) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/pools/" + POOL.getPoolName().getPoolName()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)) + .body(OFFICE_ID, equalTo(POOL.getPoolName().getOfficeId())) + .body(MESSAGE, equalTo("Pool successfully deleted from CWMS")) + .body(IDENTIFIER, equalTo(POOL.getPoolName().getPoolName())) + ; + } + + private static PROJECT_OBJ_T buildProject() { + PROJECT_OBJ_T retval = new PROJECT_OBJ_T(); + retval.setPROJECT_LOCATION(LocationUtil.getLocation(PROJECT_LOC)); + retval.setPUMP_BACK_LOCATION(null); + retval.setNEAR_GAGE_LOCATION(null); + retval.setAUTHORIZING_LAW(null); + retval.setCOST_YEAR(Timestamp.from(Instant.now())); + retval.setFEDERAL_COST(BigDecimal.ONE); + retval.setNONFEDERAL_COST(BigDecimal.TEN); + retval.setFEDERAL_OM_COST(BigDecimal.ZERO); + retval.setNONFEDERAL_OM_COST(BigDecimal.valueOf(15.0)); + retval.setCOST_UNITS_ID("$"); + retval.setREMARKS("TEST RESERVOIR PROJECT"); + retval.setPROJECT_OWNER("CDA"); + retval.setHYDROPOWER_DESCRIPTION("HYDRO DESCRIPTION"); + retval.setSEDIMENTATION_DESCRIPTION("SEDIMENTATION DESCRIPTION"); + retval.setDOWNSTREAM_URBAN_DESCRIPTION("DOWNSTREAM URBAN DESCRIPTION"); + retval.setBANK_FULL_CAPACITY_DESCRIPTION("BANK FULL CAPACITY DESCRIPTION"); + retval.setYIELD_TIME_FRAME_START(Timestamp.from(Instant.now())); + retval.setYIELD_TIME_FRAME_END(Timestamp.from(Instant.now())); + return retval; + } +} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/pool.json b/cwms-data-api/src/test/resources/cwms/cda/api/pool.json new file mode 100644 index 0000000000..69c8b74364 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/pool.json @@ -0,0 +1,13 @@ +{ + "project-id": "PROJ_POOL", + "pool-name": { + "pool-name" : "Normal2", + "office-id" : "SWT" + }, + "bottom-level-id" : "Elev.Inst.0.Bottom Of Normal2", + "top-level-id" : "Elev.Inst.0.Top Of Normal2", + "attribute" : 0, + "description" : "Normal Pool Integration Test", + "clob-text" : "Normal Pool Integration Test Notes", + "implicit" : true +} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/project_pool.json b/cwms-data-api/src/test/resources/cwms/cda/api/project_pool.json new file mode 100644 index 0000000000..31394f4dc1 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/project_pool.json @@ -0,0 +1,22 @@ +{ + "office-id": "SWT", + "name": "PROJ_POOL", + "latitude": 36.153980, + "longitude": -95.992775, + "active": true, + "public-name": "CDA-PROJECT", + "long-name": "UNITED STATES", + "description": "for testing", + "timezone-name": "UTC", + "location-type": "SITE", + "location-kind": "PROJECT", + "nation": "US", + "state-initial": "CA", + "county-name": "Sacramento", + "horizontal-datum": "NGVD29", + "published-longitude": -95.992775, + "published-latitude": 36.153980, + "elevation": 10.0, + "elevation-units": "m", + "bounding-office-id": "SWT" +} \ No newline at end of file From 4b9dded152c5b68060b41a3c220b67f95a009e66 Mon Sep 17 00:00:00 2001 From: Adam Korynta Date: Tue, 11 Aug 2026 14:47:57 -0700 Subject: [PATCH 2/3] cleanup delete OpenAPI --- cwms-data-api/src/main/java/cwms/cda/api/PoolController.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java b/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java index cbc8e16d29..a7e8e8cd7d 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java @@ -311,9 +311,8 @@ public void update(@NotNull Context ctx, @NotNull String poolId) { queryParams = { @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office of " + "the pool to be deleted."), - @OpenApiParam(name = METHOD, description = "Specifies the delete method used. " + - "Defaults to \"DELETE_KEY\"", - type = JooqDao.DeleteMethod.class) + @OpenApiParam(name = PROJECT_ID, required = true, description = "Specifies the owning project of " + + "the pool to be deleted.") }, description = "Delete CWMS Pool", method = HttpMethod.DELETE, From f0bec06027605efbad0d588640512beb312f39c1 Mon Sep 17 00:00:00 2001 From: Adam Korynta Date: Tue, 11 Aug 2026 15:23:47 -0700 Subject: [PATCH 3/3] fix incorrect body assertions --- cwms-data-api/src/test/java/cwms/cda/api/PoolControllerIT.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/PoolControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/PoolControllerIT.java index 2f0a25894d..a6310c840b 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/PoolControllerIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/PoolControllerIT.java @@ -322,9 +322,6 @@ void test_get_all(String format) { .log().ifValidationFails(LogDetail.ALL, true) .assertThat() .statusCode(is(HttpServletResponse.SC_NO_CONTENT)) - .body(OFFICE_ID, equalTo(POOL.getPoolName().getOfficeId())) - .body(MESSAGE, equalTo("Pool successfully deleted from CWMS")) - .body(IDENTIFIER, equalTo(POOL.getPoolName().getPoolName())) ; }