diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java index a50fb151a..5b014c5e4 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java @@ -75,7 +75,7 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -124,6 +124,12 @@ public void create(@NotNull Context ctx) { LocationLevel level = deserializeLocationLevel(ctx); level.validate(); + if (!level.getLevelDate().truncatedTo(ChronoUnit.MINUTES).equals(level.getLevelDate())) { + Map errorDetails = new HashMap<>(); + errorDetails.put("message", "Level effective date cannot have seconds"); + throw new BadRequestResponse("", errorDetails); + } + DSLContext dsl = getDslContext(ctx); LocationLevelsDao levelsDao = getLevelsDao(dsl); levelsDao.storeLocationLevel(level); diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java index 2a967a7f1..8df8ede09 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java @@ -24,6 +24,33 @@ package cwms.cda.api; +import static cwms.cda.api.Controllers.BEGIN; +import static cwms.cda.api.Controllers.EFFECTIVE_DATE; +import static cwms.cda.api.Controllers.EFFECTIVE_DATE_EXACT; +import static cwms.cda.api.Controllers.END; +import static cwms.cda.api.Controllers.FORMAT; +import static cwms.cda.api.Controllers.INCLUDE_ALIASES; +import static cwms.cda.api.Controllers.INTERVAL; +import static cwms.cda.api.Controllers.LEVEL_ID_MASK; +import static cwms.cda.api.Controllers.PAGE; +import static cwms.cda.api.Controllers.PAGE_SIZE; +import static cwms.cda.api.Controllers.START; +import static cwms.cda.api.Controllers.UNIT; +import static cwms.cda.security.ApiKeyIdentityProvider.AUTH_HEADER; +import static helpers.FloatCloseTo.floatCloseTo; +import static io.restassured.RestAssured.given; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isOneOf; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + import cwms.cda.ApiServlet; import cwms.cda.data.dao.LocationCategoryDao; import cwms.cda.data.dao.LocationGroupDao; @@ -33,9 +60,9 @@ import cwms.cda.data.dto.AssignedLocation; import cwms.cda.data.dto.LocationCategory; import cwms.cda.data.dto.LocationGroup; +import cwms.cda.data.dto.TimeSeries; import cwms.cda.data.dto.locationlevel.ConstantLocationLevel; import cwms.cda.data.dto.locationlevel.LocationLevel; -import cwms.cda.data.dto.TimeSeries; import cwms.cda.data.dto.locationlevel.SeasonalLocationLevel; import cwms.cda.data.dto.locationlevel.SeasonalValueBean; import cwms.cda.data.dto.locationlevel.TimeSeriesLocationLevel; @@ -44,11 +71,24 @@ import fixtures.CwmsDataApiSetupCallback; import fixtures.MinimumSchema; import fixtures.TestAccounts; +import hec.data.RatingException; +import hec.data.cwmsRating.io.RatingSetContainer; +import hec.data.cwmsRating.io.RatingSpecContainer; import io.restassured.filter.log.LogDetail; - import io.restassured.path.json.JsonPath; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import java.io.IOException; +import java.sql.SQLException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; +import javax.servlet.http.HttpServletResponse; import mil.army.usace.hec.cwms.rating.io.xml.RatingContainerXmlFactory; import mil.army.usace.hec.cwms.rating.io.xml.RatingSetContainerXmlFactory; import mil.army.usace.hec.cwms.rating.io.xml.RatingSpecXmlFactory; @@ -61,32 +101,6 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.sql.SQLException; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.NavigableMap; -import java.util.TreeMap; - -import hec.data.RatingException; -import hec.data.cwmsRating.io.RatingSetContainer; -import hec.data.cwmsRating.io.RatingSpecContainer; - -import static cwms.cda.api.Controllers.*; -import static cwms.cda.security.ApiKeyIdentityProvider.AUTH_HEADER; -import static helpers.FloatCloseTo.floatCloseTo; -import static io.restassured.RestAssured.given; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - @Tag("integration") public class LevelsControllerTestIT extends DataApiTestIT { @@ -2414,6 +2428,42 @@ void test_get_aliases() throws Exception { ; } + @Test + void test_create_subminute_level() throws Exception { + String locName = "subminuteloc"; + createLocation(locName, true, OFFICE); + String levelId = String.format("%s.Elev.Ave.1Day.Top of Inlet", locName); + ZonedDateTime time = ZonedDateTime.ofInstant(Instant.parse("2024-01-01T00:00:25Z"), ZoneId.of("UTC")); + ConstantLocationLevel level = new ConstantLocationLevel.Builder(levelId, time.toInstant()) + .withOfficeId(OFFICE) + .withLevelUnitsId("ft") + .withConstantValue(8675.309) + .withExpirationDate(time.toInstant()) + .build(); + + String levelJson = Formats.format(new ContentType(Formats.JSONV2), level); + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(Controllers.OFFICE, OFFICE) + .header("Authorization", TestAccounts.KeyUser.SPK_NORMAL.toHeaderValue()) + .body(levelJson) + .contentType(Formats.JSONV2) + .when() + .redirects() + .follow(true) + .redirects() + .max(3) + .post("/levels/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_BAD_REQUEST)) + .body("details.message", is("Level effective date cannot have seconds")) + .body("message", is("Bad Request")) + .body("source", is("User Input")); + } + enum GetAllTestNewAliases { DEFAULT(Formats.DEFAULT, Formats.JSONV2), JSON(Formats.JSON, Formats.JSONV2),