From af68dec320c7ea5d5662474aca6f31962176c588 Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Fri, 28 Aug 2026 14:02:37 -0700 Subject: [PATCH] CDA-134 - adds test coverage for SITE conversion --- .../java/cwms/cda/api/BasinControllerIT.java | 108 +++++++++++++++ .../cwms/cda/api/StreamControllerTestIT.java | 109 +++++++++++++++ .../api/StreamLocationControllerTestIT.java | 119 ++++++++++++++++ .../cda/api/StreamReachControllerTestIT.java | 128 ++++++++++++++++++ 4 files changed, 464 insertions(+) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/BasinControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/BasinControllerIT.java index d6e6e7b505..96df5d08e6 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/BasinControllerIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/BasinControllerIT.java @@ -32,6 +32,8 @@ import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.basin.Basin; import cwms.cda.formatters.Formats; +import cwms.cda.formatters.json.JsonV1; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import fixtures.CwmsDataApiSetupCallback; import fixtures.TestAccounts; import io.restassured.filter.log.LogDetail; @@ -573,4 +575,110 @@ void test_get_one_connectivity(String format) { ; } + + /** + * A brand new CWMS location is always created with a location kind of SITE, regardless of + * what kind is requested at creation time. Storing a Basin against an existing SITE location + * is what actually converts its kind in place. This test exercises that conversion through + * the REST API: create a plain SITE location, confirm its kind, + * then create a Basin against that same location and confirm the kind changed to BASIN. + */ + @Test + void test_site_location_converts_to_basin() throws Exception { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SWT_NORMAL; + String basinId = "SiteConvertsToBasin"; + + Location location = new Location.Builder(basinId, "SITE", ZoneId.of("UTC"), + 38.5613824, -121.7298432, "WGS84", OFFICE) + .withActive(true) + .build(); + String locationJson = JsonV1.buildObjectMapper().registerModule(new Jdk8Module()) + .writeValueAsString(location); + + // Create a plain location - the database always stores it with kind SITE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSON) + .body(locationJson) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/locations") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Confirm it starts out as SITE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, OFFICE) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/locations/" + basinId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("location-kind", equalTo("SITE")); + + Basin basin = new Basin.Builder() + .withBasinId(new CwmsId.Builder().withName(basinId).withOfficeId(OFFICE).build()) + .withSortOrder(1.0) + .withTotalDrainageArea(100.0) + .withAreaUnit("mi2") + .build(); + String basinJson = Formats.format(Formats.parseHeader(Formats.JSON, Basin.class), basin); + + // Store a Basin against that same, existing SITE location + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .contentType(Formats.JSONV1) + .body(basinJson) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("basins/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Confirm the location's kind was converted to BASIN + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, OFFICE) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/locations/" + basinId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("location-kind", equalTo("BASIN")); + + // Cleanup + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(METHOD, DeleteRule.DELETE_ALL.getRule()) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("basins/" + basinId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + } } \ No newline at end of file diff --git a/cwms-data-api/src/test/java/cwms/cda/api/StreamControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/StreamControllerTestIT.java index a34d09e37f..48f1bc2301 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/StreamControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/StreamControllerTestIT.java @@ -24,6 +24,7 @@ package cwms.cda.api; +import static cwms.cda.api.Controllers.FAIL_IF_EXISTS; import static cwms.cda.api.Controllers.NAME; import static cwms.cda.api.Controllers.OFFICE; import static cwms.cda.api.Controllers.OFFICE_MASK; @@ -34,14 +35,18 @@ import cwms.cda.data.dao.DeleteRule; import cwms.cda.data.dao.StreamDao; import cwms.cda.data.dto.CwmsId; +import cwms.cda.data.dto.Location; import cwms.cda.data.dto.stream.Stream; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; +import cwms.cda.formatters.json.JsonV1; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import fixtures.CwmsDataApiSetupCallback; import fixtures.TestAccounts; import io.restassured.filter.log.LogDetail; import java.sql.SQLException; import java.time.Instant; +import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import mil.army.usace.hec.test.database.CwmsDatabaseContainer; @@ -348,4 +353,108 @@ void test_get_all(String format) throws IOException { .body(IDENTIFIER, equalTo(streamId)); } + /** + * A brand new CWMS location is always created with a location kind of SITE, regardless of + * what kind is requested at creation time. Storing a Stream against an existing SITE + * location is what actually converts its kind in place. This test exercises that conversion + * through the REST API: create a plain SITE location, confirm its + * kind, then create a Stream against that same location and confirm the kind changed to + * STREAM. + */ + @Test + void test_site_location_converts_to_stream() throws Exception { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + String locId = "SiteConvertsToStream123"; + + Location location = new Location.Builder(locId, "SITE", ZoneId.of("UTC"), + 38.5613824, -121.7298432, "WGS84", OFFICE_ID) + .withActive(true) + .build(); + String locationJson = JsonV1.buildObjectMapper().registerModule(new Jdk8Module()) + .writeValueAsString(location); + + // Create a plain location - the database always stores it with kind SITE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSON) + .body(locationJson) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/locations") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Confirm it starts out as SITE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(OFFICE, OFFICE_ID) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/locations/" + locId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("location-kind", equalTo("SITE")); + + Stream stream = new Stream.Builder() + .withId(new CwmsId.Builder().withName(locId).withOfficeId(OFFICE_ID).build()) + .withStartsDownstream(true) + .withLength(100.0) + .withLengthUnits("km") + .build(); + String streamJson = Formats.format(Formats.parseHeader(Formats.JSON, Stream.class), stream); + + // Store a Stream against that same, existing SITE location + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSON) + .body(streamJson) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/streams/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Confirm the location's kind was converted to STREAM + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(OFFICE, OFFICE_ID) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/locations/" + locId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("location-kind", equalTo("STREAM")); + + // Cleanup + given() + .log().ifValidationFails(LogDetail.ALL, true) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(OFFICE, OFFICE_ID) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/streams/" + locId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + } + } \ No newline at end of file diff --git a/cwms-data-api/src/test/java/cwms/cda/api/StreamLocationControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/StreamLocationControllerTestIT.java index 435ee63eb5..890f6d4329 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/StreamLocationControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/StreamLocationControllerTestIT.java @@ -38,14 +38,21 @@ import cwms.cda.data.dao.DeleteRule; import cwms.cda.data.dao.StreamDao; import cwms.cda.data.dto.CwmsId; +import cwms.cda.data.dto.Location; +import cwms.cda.data.dto.stream.Bank; import cwms.cda.data.dto.stream.Stream; import cwms.cda.data.dto.stream.StreamLocation; +import cwms.cda.data.dto.stream.StreamLocationNode; +import cwms.cda.data.dto.stream.StreamNode; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; +import cwms.cda.formatters.json.JsonV1; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import fixtures.CwmsDataApiSetupCallback; import fixtures.TestAccounts; import io.restassured.filter.log.LogDetail; import java.sql.SQLException; +import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import mil.army.usace.hec.test.database.CwmsDatabaseContainer; @@ -357,4 +364,116 @@ void test_get_all(String format) throws IOException { .body(MESSAGE, equalTo("Stream Location successfully deleted from CWMS.")) .body(IDENTIFIER, equalTo(streamLocation.getStreamId().getName())); } + + /** + * A brand new CWMS location is always created with a location kind of SITE, regardless of + * what kind is requested at creation time. Storing a StreamLocation against an existing SITE + * location is what actually converts its kind in place. This test exercises that conversion + * through the REST API: create a plain SITE location, confirm its + * kind, then create a StreamLocation against that same location and confirm the kind changed + * to STREAM_LOCATION. + */ + @Test + void test_site_location_converts_to_stream_location() throws Exception { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SWT_NORMAL; + String streamId = "ImOnThisStream2"; // stream created in setup() + String locId = "SiteConvStreamLoc321"; + + Location location = new Location.Builder(locId, "SITE", ZoneId.of("UTC"), + 38.5613824, -121.7298432, "WGS84", OFFICE_ID) + .withActive(true) + .build(); + String locationJson = JsonV1.buildObjectMapper().registerModule(new Jdk8Module()) + .writeValueAsString(location); + + // Create a plain location - the database always stores it with kind SITE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSON) + .body(locationJson) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/locations") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Confirm it starts out as SITE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(OFFICE, OFFICE_ID) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/locations/" + locId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("location-kind", equalTo("SITE")); + + StreamLocation streamLocation = new StreamLocation.Builder() + .withStreamLocationNode(new StreamLocationNode.Builder() + .withId(new CwmsId.Builder().withName(locId).withOfficeId(OFFICE_ID).build()) + .withStreamNode(new StreamNode.Builder() + .withStreamId(new CwmsId.Builder().withName(streamId).withOfficeId(OFFICE_ID).build()) + .withStation(10.0) + .withBank(Bank.LEFT) + .withStationUnits("km") + .build()) + .build()) + .build(); + String streamLocationJson = Formats.format(Formats.parseHeader(Formats.JSON, StreamLocation.class), streamLocation); + + // Store a StreamLocation against that same, existing SITE location + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSON) + .queryParam(FAIL_IF_EXISTS, false) + .body(streamLocationJson) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/stream-locations/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Confirm the location's kind was converted to STREAM_LOCATION + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(OFFICE, OFFICE_ID) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/locations/" + locId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("location-kind", equalTo("STREAM_LOCATION")); + + // Cleanup + given() + .log().ifValidationFails(LogDetail.ALL, true) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(OFFICE, OFFICE_ID) + .queryParam(STREAM_ID, streamId) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/stream-locations/" + locId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/StreamReachControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/StreamReachControllerTestIT.java index f04553f66a..6aad4b519a 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/StreamReachControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/StreamReachControllerTestIT.java @@ -31,6 +31,7 @@ import cwms.cda.data.dao.StreamDao; import cwms.cda.data.dao.StreamLocationDao; import cwms.cda.data.dto.CwmsId; +import cwms.cda.data.dto.Location; import cwms.cda.data.dto.stream.Bank; import cwms.cda.data.dto.stream.Stream; import cwms.cda.data.dto.stream.StreamLocation; @@ -39,10 +40,13 @@ import cwms.cda.data.dto.stream.StreamReach; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; +import cwms.cda.formatters.json.JsonV1; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import fixtures.CwmsDataApiSetupCallback; import fixtures.TestAccounts; import io.restassured.filter.log.LogDetail; import java.sql.SQLException; +import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import mil.army.usace.hec.test.database.CwmsDatabaseContainer; @@ -429,4 +433,128 @@ void test_get_all(String format) throws IOException { .assertThat() .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); } + + /** + * A brand new CWMS location is always created with a location kind of SITE, regardless of + * what kind is requested at creation time. Storing a StreamReach against an existing SITE + * location is what actually converts its kind in place. This test exercises that conversion + * through the REST API: create a plain SITE location, confirm its + * kind, then create a StreamReach against that same location and confirm the kind changed to + * STREAM_REACH. + */ + @Test + void test_site_location_converts_to_stream_reach() throws Exception { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + String streamId = "Stream123"; // stream created in setup() + String upstreamLocId = "UpstreamLoc123"; // stream location created in setup() + String downstreamLocId = "DownstreamLoc123"; // stream location created in setup() + String reachId = "SiteConvertsToReach123"; + + Location location = new Location.Builder(reachId, "SITE", ZoneId.of("UTC"), + 38.5613824, -121.7298432, "WGS84", OFFICE_ID) + .withActive(true) + .build(); + String locationJson = JsonV1.buildObjectMapper().registerModule(new Jdk8Module()) + .writeValueAsString(location); + + // Create a plain location - the database always stores it with kind SITE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSON) + .body(locationJson) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/locations") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Confirm it starts out as SITE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(OFFICE, OFFICE_ID) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/locations/" + reachId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("location-kind", equalTo("SITE")); + + StreamReach reach = new StreamReach.Builder() + .withId(new CwmsId.Builder().withName(reachId).withOfficeId(OFFICE_ID).build()) + .withStreamId(new CwmsId.Builder().withName(streamId).withOfficeId(OFFICE_ID).build()) + .withUpstreamNode(new StreamLocationNode.Builder() + .withId(new CwmsId.Builder().withName(upstreamLocId).withOfficeId(OFFICE_ID).build()) + .withStreamNode(new StreamNode.Builder() + .withStreamId(new CwmsId.Builder().withName(streamId).withOfficeId(OFFICE_ID).build()) + .withStation(20.0) + .withBank(Bank.LEFT) + .withStationUnits("km") + .build()) + .build()) + .withDownstreamNode(new StreamLocationNode.Builder() + .withId(new CwmsId.Builder().withName(downstreamLocId).withOfficeId(OFFICE_ID).build()) + .withStreamNode(new StreamNode.Builder() + .withStreamId(new CwmsId.Builder().withName(streamId).withOfficeId(OFFICE_ID).build()) + .withStation(25.0) + .withBank(Bank.RIGHT) + .withStationUnits("km") + .build()) + .build()) + .withComment("Site to Stream Reach conversion test") + .build(); + String reachJson = Formats.format(Formats.parseHeader(Formats.JSON, StreamReach.class), reach); + + // Store a StreamReach against that same, existing SITE location + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSON) + .body(reachJson) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/stream-reaches/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Confirm the location's kind was converted to STREAM_REACH + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(OFFICE, OFFICE_ID) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/locations/" + reachId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("location-kind", equalTo("STREAM_REACH")); + + // Cleanup + given() + .log().ifValidationFails(LogDetail.ALL, true) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(OFFICE, OFFICE_ID) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/stream-reaches/" + reachId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + } }