diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 28dd4b724..2d5c20789 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,25 +14,12 @@ updates: reviewers: - "@MikeNeilson" - "@DanielTOsborne" - - package-ecosystem: "gradle" - directory: "/" - schedule: - interval: "weekly" - groups: - alldependencies: - patterns: - - "*" - update-types: - - "major" - reviewers: - - "@MikeNeilson" - - "@DanielTOsborne" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" groups: - alldependencies: + github-action-dependencies: patterns: - "*" update-types: @@ -40,4 +27,4 @@ updates: - "patch" reviewers: - "@MikeNeilson" - - "@DanielTOsborne" \ No newline at end of file + - "@DanielTOsborne" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3f6c46a8a..f14ff71a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,17 +17,18 @@ jobs: thewar: ${{steps.thebuild.outputs.WARFILE}} steps: - name: checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4.2.1 - name: setup java - uses: actions/setup-java@v1 + uses: actions/setup-java@v4.4.0 with: + distribution: 'temurin' java-version: '8' - java-package: jdk + cache: 'gradle' - name: build and test id: thebuild run: ./gradlew build --info --init-script init.gradle - name: Upload WAR - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4.4.3 with: name: warfile path: cwms-data-api/build/libs/${{steps.thebuild.outputs.WARFILE}} @@ -43,15 +44,16 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4.2.1 - name: setup java - uses: actions/setup-java@v1 + uses: actions/setup-java@v4.4.0 with: + distribution: 'temurin' java-version: '8' - java-package: jdk + cache: 'gradle' - name: Download all workflow run artifacts from build id: artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4.1.8 with: path: ./ - name: get version @@ -59,37 +61,15 @@ jobs: run: .github/workflows/get_version.sh - name: show version run: echo ${VERSION} - - name: tag repo - uses: actions/github-script@v3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - github.git.createRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: "refs/tags/${{env.VERSION}}", - sha: context.sha - }) - name: Create Release id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN }} + uses: softprops/action-gh-release@v2.0.8 with: + files: warfile/${{ needs.build.outputs.thewar}} tag_name: ${{env.VERSION}} - release_name: Release ${{env.VERSION}} - - name: Upload Tomcat War - id: upload-war - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: warfile/${{ needs.build.outputs.thewar}} - asset_name: ${{ env.WAR_FILE_NAME}} - asset_content_type: application/x-webarchive + generate_release_notes: true - name: Login to Alt Registry - uses: docker/login-action@v2.0.0 + uses: docker/login-action@v3.3.0 id: login-alt with: registry: ${{ secrets.ALT_REGISTRY }} @@ -108,68 +88,3 @@ jobs: - name: Logout of ALT registry if: ${{ always() }} run: docker logout ${{ steps.login-alt.outputs.registry }} - - - publish: - if: github.event_name == 'push' && startsWith(github.ref,'refs/heads/develop') - name: API Container Image - # The type of runner that the job will run on - runs-on: ubuntu-latest - needs: build - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - name: checkout code - uses: actions/checkout@v2 - - name: setup java - uses: actions/setup-java@v1 - with: - java-version: '8' - java-package: jdk - - name: get Data-API version - id: get_version - run: .github/workflows/get_version.sh - - name: Configure AWS credentials - if: ${{ always() }} - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.CWBICI_DEVELOP_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.CWBICI_DEVELOP_AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.CWBICI_DEVELOP_AWS_REGION }} - - name: Login to Amazon ECR - if: ${{ success() }} - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - - name: Login to Alt Registry - uses: docker/login-action@v2.0.0 - id: login-alt - with: - registry: ${{ secrets.ALT_REGISTRY }} - username: ${{ secrets.ALT_REG_USER }} - password: ${{ secrets.ALT_REG_PASSWORD }} - - name: Build, tag, and push image to Amazon ECR (cwms/data-api) - if: ${{ success() }} - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - # do not change to cwms-data-api; upstream config also requires change - ECR_REPOSITORY: cwms-radar-api - IMAGE_TAG: ${{env.VERSION}} - ALT_REGISTRY: ${{ secrets.ALT_REGISTRY }} - run: | - docker build -t cda:build-latest . - - docker tag cda:build-latest $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - docker tag cda:build-latest $ECR_REGISTRY/$ECR_REPOSITORY:latest - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest - - docker tag cda:build-latest $ALT_REGISTRY/cwms/data-api:$IMAGE_TAG - docker tag cda:build-latest $ALT_REGISTRY/cwms/data-api:latest-dev - docker push $ALT_REGISTRY/cwms/data-api:$IMAGE_TAG - docker push $ALT_REGISTRY/cwms/data-api:latest-dev - - name: Logout of Amazon ECR - if: ${{ always() }} - run: docker logout ${{ steps.login-ecr.outputs.registry }} - - name: Logout of ALT registry - if: ${{ always() }} - run: docker logout ${{ steps.login-alt.outputs.registry }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2dc67158b..24b6b1e80 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,24 +14,24 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.24.10 + uses: github/codeql-action/init@v3.26.13 with: languages: 'java' - name: setup java - uses: actions/setup-java@v4.2.1 + uses: actions/setup-java@v4.4.0 with: java-version: '8' java-package: jdk distribution: 'temurin' - name: Setup Gradle - uses: gradle/gradle-build-action@v3.1.0 + uses: gradle/gradle-build-action@v3.5.0 with: dependency-graph: generate-and-submit - name: build and test id: build run: ./gradlew build --info --init-script init.gradle - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.24.10 \ No newline at end of file + uses: github/codeql-action/analyze@v3.26.13 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 69ddd1e2e..b069b258e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,6 +101,29 @@ However, *DO NOT* be afraid to say, "that looks terrible", and tweak it until it Otherwise JOOQ creates a new name each time the query is run which can starved the shared memory. 2. Joins are your friend. They are a much better friend IF you let the database do them for you. Do not pull data into java just to do a join. Write the appropriate SQL. +3. Whenever possible limit by office first. + +#### Database version support + +1. Given we have active development of both the API and the database and things are not always available at the same time, it is reasonable to gate new features behind a database version check and return an appropriate error message. + This is preffered over default errors of things not working + +2. If it is known that an integration test requires a specific database version it should be gated behind a EnableIfSchemaVersion (NOTE: not implemented at the time of this writing) annotation so streamline automated testing results. + +#### Tests + +1. Assume the following when creating and naming your test: + a. Someone will come in cold to the entire project. + b. The tests will be used by API users to guide their client application designs +3. For repeated tests with different, but very similar data, ParameterizedTests are preferred. +4. In integration tests for data that should be cleaned up after all tests register them with the functions available in the base class. Create if reasonable. +5. If it adds clarity, do not be afraid to use the `@Order` annotation to sequence tests. (See the [ApiKey Controller Test](https://github.com/USACE/cwms-data-api/blob/develop/cwms-data-api/src/test/java/cwms/cda/api/auth/ApiKeyControllerTestIT.java) for an example) +6. Prefer disabling test by database schema version, if that does work use `EnabledIfProperty` and share a property name between related tests. +7. Use "real" names for data in test data set. Either use actual real location/project/basin/etc names, or make up something that feels like one. + a. NOTE: within reason. Location names, absolutely, but otherwise make sure the purpose of the name is clear. +8. Name files consistent with the purpose of the test. + + ## Submitting an Issue diff --git a/Dockerfile b/Dockerfile index 25e532b40..853ff6e29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,19 +5,21 @@ WORKDIR /builddir COPY . /builddir/ RUN gradle clean prepareDockerBuild --info --no-daemon -FROM alpine:3.19.0 as tomcat_base -RUN apk update && apk upgrade --no-cache -RUN apk add openjdk8-jre curl -RUN apk add --no-cache bash +FROM alpine:3.20.3 as tomcat_base +RUN apk --no-cache upgrade && \ + apk --no-cache add \ + openjdk8-jre \ + curl \ + bash RUN mkdir /download && \ cd /download && \ - wget https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.91/bin/apache-tomcat-9.0.91.tar.gz && \ - echo "b22054c9141782232a693765d23d944f0f50774af17dd8968331e020b425e71459b5877a7ba8c2121246a5ce47e6b6a31c3f4215ef133e942da45b49cb534948 *apache-tomcat-9.0.91.tar.gz" > checksum.txt && \ + wget https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.93/bin/apache-tomcat-9.0.93.tar.gz && \ + echo "3069924eb7041ccc0f2aeceb7d8626793a1a073a5b739a840d7974a18ebeb26cc3374cc5f4a3ffc74d3b019c0cb33e3d1fe96296e6663ac75a73c1171811726d *apache-tomcat-9.0.93.tar.gz" > checksum.txt && \ sha512sum -c checksum.txt && \ tar xzf apache-tomcat-*tar.gz && \ - mv apache-tomcat-9.0.91 /usr/local/tomcat/ && \ + mv apache-tomcat-9.0.93 /usr/local/tomcat/ && \ cd / && \ rm -rf /download CMD ["/usr/local/tomcat/bin/catalina.sh","run"] @@ -38,10 +40,12 @@ ENV CDA_POOL_INIT_SIZE "5" ENV CDA_POOL_MAX_ACTIVE "30" ENV CDA_POOL_MAX_IDLE "10" ENV CDA_POOL_MIN_IDLE "5" -ENV cwms.dataapi.access.providers "" -ENV cwms.dataapi.access.openid.wellKnownUrl "" -ENV cwms.dataapi.access.openid.issuer "" +ENV cwms.dataapi.access.providers "KeyAccessManager,OpenID" +ENV cwms.dataapi.access.openid.wellKnownUrl "https://identity-test.cwbi.us/auth/realms/cwbi/.well-known/openid-configuration" +ENV cwms.dataapi.access.openid.issuer "https://identity-test.cwbi.us/auth/realms/cwbi" ENV cwms.dataapi.access.openid.timeout "604800" +ENV cwms.dataapi.access.openid.altAuthUrl "https://identityc-test.cwbi.us/auth/realms/cwbi" + # used to simplify redeploy in certain contexts. Update to match - in image label ENV IMAGE_MARKER="a" EXPOSE 7000 diff --git a/build.gradle b/build.gradle index 046c0d014..b0a1ac85c 100644 --- a/build.gradle +++ b/build.gradle @@ -6,5 +6,5 @@ plugins { allprojects { apply plugin: 'cda.java-conventions' group = 'mil.army.usace.hec.cwms' - version = '3.1.5-SNAPSHOT' // ApiServlet.VERSION should be updated to match MAJOR.MINOR changes. + version = '3.1.6-SNAPSHOT' // ApiServlet.VERSION should be updated to match MAJOR.MINOR changes. } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 609e13ca2..ada8daeda 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -6,3 +6,7 @@ repositories { gradlePluginPortal() // so that external plugins can be resolved in dependencies section mavenCentral() } + +dependencies { + implementation 'org.owasp:dependency-check-gradle:10.0.3' +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/cda.java-conventions.gradle b/buildSrc/src/main/groovy/cda.java-conventions.gradle index 6d9f54694..337235695 100644 --- a/buildSrc/src/main/groovy/cda.java-conventions.gradle +++ b/buildSrc/src/main/groovy/cda.java-conventions.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'checkstyle' id 'jacoco' + id 'org.owasp.dependencycheck' } eclipse { diff --git a/cwms-data-api/build.gradle b/cwms-data-api/build.gradle index d0f2a5335..437a282b3 100644 --- a/cwms-data-api/build.gradle +++ b/cwms-data-api/build.gradle @@ -16,6 +16,13 @@ configurations.implementation { exclude group: 'com.oracle.database.jdbc' } +configurations.all { + exclude group: 'dom4j' + exclude group: 'org.apache.xmlbeans' + exclude group: 'org.apache.poi' + exclude group: 'org.bouncycastle' +} + dependencies { implementation(libs.jaxb.api) implementation(libs.jaxb.core) @@ -62,7 +69,9 @@ dependencies { implementation(libs.google.errorProne) runtimeOnly(libs.google.flogger.backend) - implementation(libs.nucleus.data) + implementation(libs.nucleus.data) { + exclude group: "org.jdom" + } implementation(libs.nucleus.metadata) implementation(libs.cwms.ratings.core) { @@ -144,6 +153,9 @@ dependencies { transitive = false } + + // override versions + implementation(libs.bundles.overrides) } task extractWebJars(type: Copy) { diff --git a/cwms-data-api/src/docker/server.xml b/cwms-data-api/src/docker/server.xml index ae1135c1d..408202f68 100644 --- a/cwms-data-api/src/docker/server.xml +++ b/cwms-data-api/src/docker/server.xml @@ -45,6 +45,8 @@ diff --git a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java index 3cf9cf07f..29e8218bb 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -26,6 +26,7 @@ import static cwms.cda.api.Controllers.CONTRACT_NAME; +import static cwms.cda.api.Controllers.LOCATION_ID; import static cwms.cda.api.Controllers.NAME; import static cwms.cda.api.Controllers.OFFICE; import static cwms.cda.api.Controllers.PROJECT_ID; @@ -140,6 +141,7 @@ import cwms.cda.formatters.FormattingException; import cwms.cda.formatters.UnsupportedFormatException; import cwms.cda.security.CwmsAuthException; +import cwms.cda.security.MissingRolesException; import cwms.cda.security.Role; import cwms.cda.spi.AccessManagers; import cwms.cda.spi.CdaAccessManager; @@ -213,6 +215,7 @@ "/streams/*", "/stream-locations/*", "/stream-reaches/*", + "/measurements/*", "/blobs/*", "/clobs/*", "/pools/*", @@ -233,6 +236,7 @@ public class ApiServlet extends HttpServlet { // based on https://bitbucket.hecdev.net/projects/CWMS/repos/cwms_aaa/browse/IntegrationTests/src/test/resources/sql/load_testusers.sql public static final String CWMS_USERS_ROLE = "CWMS Users"; + public static final String CAC_USER = "cac_auth"; /** Default OFFICE where needed. Based on context. e.g. /cwms-data -> HQ, /spk-data -> SPK */ public static final String OFFICE_ID = "office_id"; public static final String DATA_SOURCE = "data_source"; @@ -382,6 +386,16 @@ public void init() { CdaError re = new CdaError(e.getMessage(), e.getDetails(), true); ctx.status(HttpServletResponse.SC_BAD_REQUEST).json(re); }) + .exception(MissingRolesException.class, (e,ctx) -> { + CdaError re = new CdaError(e.getMessage(), true); + if (logger.atFine().isEnabled()) { + logger.atFine().withCause(e).log(e.getMessage()); + } else { + logger.atInfo().log(e.getMessage()); + } + + ctx.status(e.getAuthFailCode()).json(re); + }) .exception(CwmsAuthException.class, (e,ctx) -> { CdaError re; switch (e.getAuthFailCode()) { @@ -447,7 +461,7 @@ protected void configureRoutes() { get("/", ctx -> ctx.result("Welcome to the CWMS REST API") .contentType(Formats.PLAIN)); // Even view on this one requires authorization - crud("/auth/keys/{key-name}",new ApiKeyController(metrics), requiredRoles); + crud("/auth/keys/{key-name}",new ApiKeyController(metrics), new RouteRole[]{new Role(CAC_USER), new Role(CWMS_USERS_ROLE)}); cdaCrudCache("/location/category/{category-id}", new LocationCategoryController(metrics), requiredRoles, 5, TimeUnit.MINUTES); cdaCrudCache("/location/group/{group-id}", @@ -492,7 +506,7 @@ protected void configureRoutes() { cdaCrudCache("/timeseries/category/{category-id}", new TimeSeriesCategoryController(metrics), requiredRoles,5, TimeUnit.MINUTES); - cdaCrudCache("/timeseries/identifier-descriptor/{timeseries-id}", + cdaCrudCache("/timeseries/identifier-descriptor/{name}", new TimeSeriesIdentifierDescriptorController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache("/timeseries/group/{group-id}", new TimeSeriesGroupController(metrics), requiredRoles,5, TimeUnit.MINUTES); @@ -526,6 +540,9 @@ protected void configureRoutes() { new StreamLocationController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache(format("/stream-reaches/{%s}", NAME), new StreamReachController(metrics), requiredRoles,1, TimeUnit.DAYS); + String measurements = "/measurements/"; + cdaCrudCache(format(measurements + "{%s}", LOCATION_ID), + new cwms.cda.api.MeasurementController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache("/blobs/{blob-id}", new BlobController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache("/clobs/{clob-id}", @@ -580,7 +597,7 @@ protected void configureRoutes() { cdaCrudCache(format("/projects/{%s}", Controllers.NAME), new ProjectController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache(format("/properties/{%s}", Controllers.NAME), - new PropertyController(metrics), requiredRoles,1, TimeUnit.DAYS); + new PropertyController(metrics), true, requiredRoles,1, TimeUnit.DAYS); cdaCrudCache(format("/lookup-types/{%s}", Controllers.NAME), new LookupTypeController(metrics), requiredRoles,1, TimeUnit.DAYS); @@ -610,16 +627,16 @@ private void addProjectLockRightsHandlers(String path, RouteRole[] requiredRoles private void addWaterUserHandlers(String path, RouteRole[] requiredRoles) { - get(path + format("/{%s}", WATER_USER), new WaterUserController(metrics)); - get(path, new WaterUserCatalogController(metrics)); + get(path + format("/{%s}", WATER_USER), new WaterUserController(metrics), requiredRoles); + get(path, new WaterUserCatalogController(metrics), requiredRoles); post(path, new WaterUserCreateController(metrics), requiredRoles); patch(path + format("/{%s}", WATER_USER), new WaterUserUpdateController(metrics), requiredRoles); delete(path + format("/{%s}", WATER_USER), new WaterUserDeleteController(metrics), requiredRoles); } private void addWaterContractHandlers(String path, RouteRole[] requiredRoles) { - get(path + format("/{%s}", CONTRACT_NAME), new WaterContractController(metrics)); - get(path, new WaterContractCatalogController(metrics)); + get(path + format("/{%s}", CONTRACT_NAME), new WaterContractController(metrics), requiredRoles); + get(path, new WaterContractCatalogController(metrics), requiredRoles); post(path, new WaterContractCreateController(metrics), requiredRoles); patch(path + format("/{%s}", CONTRACT_NAME), new WaterContractUpdateController(metrics), requiredRoles); delete(path + format("/{%s}", CONTRACT_NAME), new WaterContractDeleteController(metrics), requiredRoles); @@ -627,7 +644,7 @@ private void addWaterContractHandlers(String path, RouteRole[] requiredRoles) { private void addWaterContractTypeHandlers(String path, RouteRole[] requiredRoles) { post(path, new WaterContractTypeCreateController(metrics), requiredRoles); - get(path, new WaterContractTypeCatalogController(metrics)); + get(path, new WaterContractTypeCatalogController(metrics), requiredRoles); delete(path + "/{display-value}", new WaterContractTypeDeleteController(metrics), requiredRoles); } @@ -648,7 +665,28 @@ private void addWaterContractTypeHandlers(String path, RouteRole[] requiredRoles */ public static void cdaCrudCache(@NotNull String path, @NotNull CrudHandler crudHandler, @NotNull RouteRole[] roles, long duration, TimeUnit timeUnit) { - cdaCrud(path, crudHandler, roles); + cdaCrudCache(path, crudHandler, false, roles, duration, timeUnit); + } + + /** + * This method delegates to the cdaCrud method but also adds an after filter for the specified + * path. If the request was a GET request and the response does not already include + * Cache-Control then the filter will add the Cache-Control max-age header with the specified + * number of seconds. + * Controllers can include their own Cache-Control headers via: + * "ctx.header(Header.CACHE_CONTROL, " public, max-age=" + 60);" + * This method lets the ApiServlet configure a default max-age for controllers that don't or + * forget to set their own. + * @param path where to register the routes. + * @param crudHandler the handler requests should be forwarded to. + * @param getRequriesAuth if the get handlers should have an authoriation check + * @param roles the required these roles are present to access post, patch + * @param duration the number of TimeUnit to cache GET responses. + * @param timeUnit the TimeUnit to use for duration. + */ + public static void cdaCrudCache(@NotNull String path, @NotNull CrudHandler crudHandler, boolean getRequiresAuth, + @NotNull RouteRole[] roles, long duration, TimeUnit timeUnit) { + cdaCrud(path, crudHandler, getRequiresAuth, roles); // path like /offices/{office} will match /offices/SWT getOne style url addCacheControl(path, duration, timeUnit); @@ -671,7 +709,6 @@ private static void addCacheControl(@NotNull String path, long duration, TimeUni }); } } - /** * This method is very similar to the ApiBuilder.crud method but the specified roles * are only required for the post, patch and delete methods. getOne and getAll are always @@ -683,23 +720,40 @@ private static void addCacheControl(@NotNull String path, long duration, TimeUni */ public static void cdaCrud(@NotNull String path, @NotNull CrudHandler crudHandler, @NotNull RouteRole... roles) { + cdaCrud(path, crudHandler, false, roles); + } + + /** + * This method is very similar to the ApiBuilder.crud method but the specified roles + * are only required for the post, patch and delete methods. getOne and getAll are always + * allowed. + * @param path where to register the routes. + * @param crudHandler the handler requests should be forwarded to. + * @param getRequiresAuth If all operations on this handler should have an authorization check + * @param roles the accessmanager will require these roles are present to access post, patch + * and delete methods + */ + public static void cdaCrud(@NotNull String path, @NotNull CrudHandler crudHandler, boolean getRequiresAuth, + @NotNull RouteRole... roles) { String fullPath = prefixPath(path); String resourceId = getResourceId(fullPath); //noinspection KotlinInternalInJava - Map crudFunctions = - CrudHandlerKt.getCrudFunctions(crudHandler, resourceId); + Map crudFunctions = CrudHandlerKt.getCrudFunctions(crudHandler, resourceId); Javalin instance = staticInstance(); // getOne and getAll are assumed not to need authorization - instance.get(fullPath, crudFunctions.get(CrudFunction.GET_ONE)); String pathWithoutResource = fullPath.replace(resourceId, ""); - instance.get(pathWithoutResource, - crudFunctions.get(CrudFunction.GET_ALL)); + if (getRequiresAuth) { + instance.get(fullPath, crudFunctions.get(CrudFunction.GET_ONE), roles); + instance.get(pathWithoutResource, crudFunctions.get(CrudFunction.GET_ALL), roles); + } else { + instance.get(fullPath, crudFunctions.get(CrudFunction.GET_ONE)); + instance.get(pathWithoutResource, crudFunctions.get(CrudFunction.GET_ALL)); + } // create, update and delete need authorization. - instance.post(pathWithoutResource, - crudFunctions.get(CrudFunction.CREATE), roles); + instance.post(pathWithoutResource, crudFunctions.get(CrudFunction.CREATE), roles); instance.patch(fullPath, crudFunctions.get(CrudFunction.UPDATE), roles); instance.delete(fullPath, crudFunctions.get(CrudFunction.DELETE), roles); } @@ -782,7 +836,7 @@ private void getOpenApiOptions(JavalinConfig config) { private static void setSecurityRequirements(String key, PathItem path,List secReqs) { /* clear the lock icon from the GET handlers to reduce user confusion */ logger.atFinest().log("setting security constraints for " + key); - if (key.contains("/auth/")) { + if ((path.getGet() != null && path.getGet().getSecurity() != null)) { setSecurity(path.getGet(), secReqs); } else { setSecurity(path.getGet(), new ArrayList<>()); 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 fb36e3dd4..154f83849 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 @@ -134,6 +134,15 @@ public final class Controllers { public static final String ISSUE_DATE = "issue-date"; public static final String LOCATION_KIND_LIKE = "location-kind-like"; public static final String LOCATION_TYPE_LIKE = "location-type-like"; + public static final String MIN_NUMBER = "min-number"; + public static final String MAX_NUMBER = "max-number"; + public static final String MIN_HEIGHT = "min-height"; + public static final String MAX_HEIGHT = "max-height"; + public static final String MIN_FLOW = "min-flow"; + public static final String MAX_FLOW = "max-flow"; + public static final String AGENCY = "agency"; + public static final String QUALITY = "quality"; + public static final String GROUP_ID = "group-id"; public static final String REPLACE_ASSIGNED_LOCS = "replace-assigned-locs"; @@ -197,6 +206,8 @@ public final class Controllers { public static final String ALLOW = "allow"; public static final String SOURCE_ID = "source-id"; + public static final String CWMS_OFFICE = "CWMS"; + private static final String DEPRECATED_HEADER = "CWMS-DATA-Format-Deprecated"; private static final String DEPRECATED_TAB = "2024-11-01 TAB is not used often."; private static final String DEPRECATED_CSV = "2024-11-01 CSV is not used often."; @@ -370,6 +381,16 @@ public static T requiredParamAs(io.javalin.http.Context ctx, String name, Cl .getOrThrow(e -> new RequiredQueryParameterException(name)); } + @Nullable + public static Double queryParamAsDouble(Context ctx, String param) { + Double retVal = null; + String numberStr = ctx.queryParam(param); + if (numberStr != null) { + retVal = Double.parseDouble(numberStr); + } + return retVal; + } + @Nullable public static ZonedDateTime queryParamAsZdt(Context ctx, String param, String timezone) { ZonedDateTime beginZdt = null; 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 6e4e8903d..5b7ec7a5a 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 @@ -24,6 +24,37 @@ package cwms.cda.api; +import static com.codahale.metrics.MetricRegistry.name; +import static cwms.cda.api.Controllers.BEGIN; +import static cwms.cda.api.Controllers.CASCADE_DELETE; +import static cwms.cda.api.Controllers.CREATE; +import static cwms.cda.api.Controllers.DATE; +import static cwms.cda.api.Controllers.DATUM; +import static cwms.cda.api.Controllers.DELETE; +import static cwms.cda.api.Controllers.EFFECTIVE_DATE; +import static cwms.cda.api.Controllers.END; +import static cwms.cda.api.Controllers.FORMAT; +import static cwms.cda.api.Controllers.GET_ALL; +import static cwms.cda.api.Controllers.GET_ONE; +import static cwms.cda.api.Controllers.LEVEL_ID; +import static cwms.cda.api.Controllers.LEVEL_ID_MASK; +import static cwms.cda.api.Controllers.NAME; +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.RESULTS; +import static cwms.cda.api.Controllers.SIZE; +import static cwms.cda.api.Controllers.STATUS_200; +import static cwms.cda.api.Controllers.TIMEZONE; +import static cwms.cda.api.Controllers.UNIT; +import static cwms.cda.api.Controllers.UPDATE; +import static cwms.cda.api.Controllers.VERSION; +import static cwms.cda.api.Controllers.addDeprecatedContentTypeWarning; +import static cwms.cda.api.Controllers.queryParamAsClass; +import static cwms.cda.api.Controllers.queryParamAsZdt; +import static cwms.cda.api.Controllers.requiredParam; +import static cwms.cda.data.dao.JooqDao.getDslContext; + import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; @@ -58,19 +89,14 @@ import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; -import org.jetbrains.annotations.NotNull; -import org.jooq.DSLContext; - import javax.servlet.http.HttpServletResponse; import java.math.BigDecimal; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.List; - -import static com.codahale.metrics.MetricRegistry.name; -import static cwms.cda.api.Controllers.*; -import static cwms.cda.data.dao.JooqDao.getDslContext; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; public class LevelsController implements CrudHandler { @@ -195,11 +221,10 @@ public void delete(@NotNull Context ctx, @NotNull String levelId) { + "specified or default units above the NGVD-29 datum."), @OpenApiParam(name = BEGIN, description = "Specifies the start of the time " + "window for data to be included in the response. If this field is " - + "not specified, any required time window begins 24 hours prior to " - + "the specified or default end time."), + + "not specified, no beginning time will be used."), @OpenApiParam(name = END, description = "Specifies the end of the time " + "window for data to be included in the response. If this field is " - + "not specified, any required time window ends at the current time"), + + "not specified, no end time will be used."), @OpenApiParam(name = TIMEZONE, description = "Specifies the time zone of " + "the values of the begin and end fields (unless otherwise " + "specified), as well as the time zone of any times in the response." @@ -261,16 +286,8 @@ public void getAll(@NotNull Context ctx) { int pageSize = ctx.queryParamAsClass(PAGE_SIZE, Integer.class) .getOrDefault(DEFAULT_PAGE_SIZE); - ZoneId tz = ZoneId.of(timezone, ZoneId.SHORT_IDS); - - ZonedDateTime endZdt = end != null ? DateUtils.parseUserDate(end, timezone) : - ZonedDateTime.now(tz); - ZonedDateTime beginZdt; - if (begin != null) { - beginZdt = DateUtils.parseUserDate(begin, timezone); - } else { - beginZdt = endZdt.minusHours(24); - } + ZonedDateTime endZdt = queryParamAsZdt(ctx, END); + ZonedDateTime beginZdt = queryParamAsZdt(ctx, BEGIN); LocationLevels levels = levelsDao.getLocationLevels(cursor, pageSize, levelIdMask, office, unit, datum, beginZdt, endZdt); @@ -291,8 +308,7 @@ public void getAll(@NotNull Context ctx) { requestResultSize.update(results.length()); if (isLegacyVersion) { ctx.contentType(contentType.toString()); - } - else { + } else { ctx.contentType(contentType.getType()); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java b/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java index d9d91a081..6d39437c5 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java @@ -224,7 +224,8 @@ public void create(@NotNull Context ctx) { } @OpenApi( - description = "Update existing LocationGroup", + description = "Update existing LocationGroup. Allows for renaming group, assigning new locations, " + + "and unassigning all locations from the group.", requestBody = @OpenApiRequestBody( content = { @OpenApiContent(from = LocationGroup.class, type = Formats.JSON) @@ -235,31 +236,32 @@ public void create(@NotNull Context ctx) { + "unassign all existing locations before assigning new locations specified in the content body " + "Default: false"), @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " - + "owning office of the location group to be updated"), + + "office of the user making the request. This is the office that the location, group, and category " + + "belong to. If the group and/or category belong to the CWMS office, this only identifies the location."), }, method = HttpMethod.PATCH, tags = {TAG} ) @Override - public void update(@NotNull Context ctx, String oldGroupId) { + public void update(@NotNull Context ctx, @NotNull String oldGroupId) { try (Timer.Context ignored = markAndTime(CREATE)) { DSLContext dsl = getDslContext(ctx); - String formatHeader = ctx.req.getContentType(); String body = ctx.body(); + String office = requiredParam(ctx, OFFICE); ContentType contentType = Formats.parseHeader(formatHeader, LocationGroup.class); LocationGroup deserialize = Formats.parseContent(contentType, body, LocationGroup.class); boolean replaceAssignedLocs = ctx.queryParamAsClass(REPLACE_ASSIGNED_LOCS, Boolean.class).getOrDefault(false); LocationGroupDao locationGroupDao = new LocationGroupDao(dsl); - if (!oldGroupId.equals(deserialize.getId())) { + if (!office.equalsIgnoreCase(CWMS_OFFICE) && !oldGroupId.equals(deserialize.getId())) { locationGroupDao.renameLocationGroup(oldGroupId, deserialize); } if (replaceAssignedLocs) { - locationGroupDao.unassignAllLocs(deserialize); + locationGroupDao.unassignAllLocs(deserialize, office); } - locationGroupDao.assignLocs(deserialize); + locationGroupDao.assignLocs(deserialize, office); ctx.status(HttpServletResponse.SC_OK); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java b/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java new file mode 100644 index 000000000..8f2607176 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java @@ -0,0 +1,267 @@ +/* + * 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 com.codahale.metrics.Histogram; +import com.codahale.metrics.MetricRegistry; +import static com.codahale.metrics.MetricRegistry.name; +import com.codahale.metrics.Timer; +import static cwms.cda.api.Controllers.AGENCY; +import static cwms.cda.api.Controllers.BEGIN; +import static cwms.cda.api.Controllers.CREATE; +import static cwms.cda.api.Controllers.DATE_FORMAT; +import static cwms.cda.api.Controllers.DELETE; +import static cwms.cda.api.Controllers.EXAMPLE_DATE; +import static cwms.cda.api.Controllers.FAIL_IF_EXISTS; +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.LOCATION_ID; +import static cwms.cda.api.Controllers.END; +import static cwms.cda.api.Controllers.MAX_FLOW; +import static cwms.cda.api.Controllers.MAX_HEIGHT; +import static cwms.cda.api.Controllers.MIN_FLOW; +import static cwms.cda.api.Controllers.MIN_HEIGHT; +import static cwms.cda.api.Controllers.NOT_SUPPORTED_YET; +import static cwms.cda.api.Controllers.MIN_NUMBER; +import static cwms.cda.api.Controllers.MAX_NUMBER; +import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.OFFICE_MASK; +import static cwms.cda.api.Controllers.QUALITY; +import static cwms.cda.api.Controllers.TIMEZONE; +import static cwms.cda.api.Controllers.UNIT_SYSTEM; +import static cwms.cda.api.Controllers.queryParamAsDouble; +import static cwms.cda.api.Controllers.queryParamAsInstant; +import static cwms.cda.api.Controllers.requiredParam; +import cwms.cda.api.enums.UnitSystem; +import cwms.cda.data.dao.MeasurementDao; +import cwms.cda.data.dto.measurement.Measurement; +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.time.Instant; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +import static cwms.cda.data.dao.JooqDao.getDslContext; + +public final class MeasurementController implements CrudHandler { + + static final String TAG = "Measurements"; + + private final MetricRegistry metrics; + private final Histogram requestResultSize; + + public MeasurementController(MetricRegistry metrics) { + this.metrics = metrics; + String className = this.getClass().getName(); + requestResultSize = this.metrics.histogram(name(className, "results", "size")); + } + + private Timer.Context markAndTime(String subject) { + return Controllers.markAndTime(metrics, getClass().getName(), subject); + } + + @OpenApi( + queryParams = { + @OpenApiParam(name = OFFICE_MASK, description = "Office id mask for filtering measurements. Use null to retrieve measurements for all offices."), + @OpenApiParam(name = ID_MASK, description = "Location id mask for filtering measurements. Use null to retrieve measurements for all locations."), + @OpenApiParam(name = MIN_NUMBER, description = "Minimum measurement number-id for filtering measurements."), + @OpenApiParam(name = MAX_NUMBER, description = "Maximum measurement number-id for filtering measurements."), + @OpenApiParam(name = BEGIN, description = "The start of the time " + + "window to delete. The format for this field is ISO 8601 extended, with " + + "optional offset and timezone, i.e., '" + DATE_FORMAT + "', e.g., '" + + EXAMPLE_DATE + "'. A null value is treated as an unbounded start."), + @OpenApiParam(name = END, description = "The end of the time " + + "window to delete.The format for this field is ISO 8601 extended, with " + + "optional offset and timezone, i.e., '" + DATE_FORMAT + "', e.g., '" + + EXAMPLE_DATE + "'.A null value is treated as an unbounded end."), + @OpenApiParam(name = TIMEZONE, description = "This field specifies a default timezone " + + "to be used if the format of the " + BEGIN + "and " + END + + " parameters do not include offset or time zone information. " + + "Defaults to UTC."), + @OpenApiParam(name = MIN_HEIGHT, description = "Minimum height for filtering measurements."), + @OpenApiParam(name = MAX_HEIGHT, description = "Maximum height for filtering measurements."), + @OpenApiParam(name = MIN_FLOW, description = "Minimum flow for filtering measurements."), + @OpenApiParam(name = MAX_FLOW, description = "Maximum flow for filtering measurements."), + @OpenApiParam(name = AGENCY, description = "Agencies for filtering measurements."), + @OpenApiParam(name = QUALITY, description = "Quality for filtering measurements."), + @OpenApiParam(name = UNIT_SYSTEM, description = "Specifies the unit system" + + " of the response. Valid values for the unit field are: " + + "\n* `EN` Specifies English unit system. Location values will be in the " + + "default English units for their parameters." + + "\n* `SI` Specifies the SI unit system. Location values will be in the " + + "default SI units for their parameters. If not specified, EN is used.") + }, + responses = { + @OpenApiResponse(status = "200", content = { + @OpenApiContent(isArray = true, type = Formats.JSONV1, from = Measurement.class), + @OpenApiContent(isArray = true, type = Formats.JSON, from = Measurement.class) + }) + }, + description = "Returns matching measurement data.", + tags = {TAG} + ) + @Override + public void getAll(@NotNull Context ctx) { + String officeId = ctx.queryParam(OFFICE_MASK); + String locationId = ctx.queryParam(ID_MASK); + String unitSystem = ctx.queryParamAsClass(UNIT_SYSTEM, String.class).getOrDefault(UnitSystem.EN.value()); + Instant minDate = queryParamAsInstant(ctx, BEGIN); + Instant maxDate = queryParamAsInstant(ctx, END); + String minNum = ctx.queryParam(MIN_NUMBER); + String maxNum = ctx.queryParam(MAX_NUMBER); + Number minHeight = queryParamAsDouble(ctx, MIN_HEIGHT); + Number maxHeight = queryParamAsDouble(ctx, MAX_HEIGHT); + Number minFlow = queryParamAsDouble(ctx, MIN_FLOW); + Number maxFlow = queryParamAsDouble(ctx, MAX_FLOW); + String agency = ctx.queryParam(AGENCY); + String quality = ctx.queryParam(QUALITY); + try (Timer.Context ignored = markAndTime(GET_ALL)) { + DSLContext dsl = getDslContext(ctx); + MeasurementDao dao = new MeasurementDao(dsl); + List measurements = dao.retrieveMeasurements(officeId, locationId, minDate, maxDate, unitSystem, + minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agency, quality); + String formatHeader = ctx.header(Header.ACCEPT); + ContentType contentType = Formats.parseHeader(formatHeader, Measurement.class); + ctx.contentType(contentType.toString()); + String serialized = Formats.format(contentType, measurements, Measurement.class); + ctx.result(serialized); + ctx.status(HttpServletResponse.SC_OK); + requestResultSize.update(serialized.length()); + } + } + + @OpenApi(ignore = true) + @Override + public void getOne(@NotNull Context ctx, @NotNull String locationId) { + try (final Timer.Context ignored = markAndTime(GET_ONE)) { + throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + } + + } + + @OpenApi( + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(isArray = true, from = Measurement.class, type = Formats.JSONV1), + @OpenApiContent(isArray = true, from = Measurement.class, type = Formats.JSON) + }, + required = true), + queryParams = { + @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, + description = "Create will fail if provided Measurement(s) already exist. Default: true") + }, + description = "Create new measurement(s).", + method = HttpMethod.POST, + tags = {TAG}, + responses = { + @OpenApiResponse(status = "204", description = "Measurement(s) successfully stored.") + } + ) + @Override + public void create(Context ctx) { + + try (Timer.Context ignored = markAndTime(CREATE)) { + String formatHeader = ctx.req.getContentType(); + ContentType contentType = Formats.parseHeader(formatHeader, Measurement.class); + List measurements = Formats.parseContentList(contentType, ctx.body(), Measurement.class); + boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(true); + DSLContext dsl = getDslContext(ctx); + MeasurementDao dao = new MeasurementDao(dsl); + dao.storeMeasurements(measurements, failIfExists); + String statusMsg = "Created Measurement"; + if(measurements.size() > 1) + { + statusMsg += "s"; + } + ctx.status(HttpServletResponse.SC_CREATED).json(statusMsg); + } + } + + @OpenApi(ignore = true) + @Override + public void update(@NotNull Context ctx, @NotNull String locationId) { + try (final Timer.Context ignored = markAndTime(GET_ONE)) { + throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + } + } + + @OpenApi( + pathParams = { + @OpenApiParam(name = LOCATION_ID, description = "Specifies the location-id of " + + "the measurement(s) to be deleted."), + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the office of the measurements to delete"), + @OpenApiParam(name = BEGIN, required = true, description = "The start of the time " + + "window to delete. The format for this field is ISO 8601 extended, with " + + "optional offset and timezone, i.e., '" + DATE_FORMAT + "', e.g., '" + + EXAMPLE_DATE + "'."), + @OpenApiParam(name = END, required = true, description = "The end of the time " + + "window to delete.The format for this field is ISO 8601 extended, with " + + "optional offset and timezone, i.e., '" + DATE_FORMAT + "', e.g., '" + + EXAMPLE_DATE + "'."), + @OpenApiParam(name = TIMEZONE, description = "This field specifies a default timezone " + + "to be used if the format of the " + BEGIN + "and " + END + + " parameters do not include offset or time zone information. " + + "Defaults to UTC."), + @OpenApiParam(name = MIN_NUMBER, description = "Specifies the min number-id of the measurement to delete."), + @OpenApiParam(name = MAX_NUMBER, description = "Specifies the max number-id of the measurement to delete."), + }, + description = "Delete an existing measurement.", + method = HttpMethod.DELETE, + tags = {TAG}, + responses = { + @OpenApiResponse(status = "204", description = "Measurement successfully deleted."), + @OpenApiResponse(status = "404", description = "Measurement not found.") + } + ) + @Override + public void delete(@NotNull Context ctx, @NotNull String locationId) { + String officeId = requiredParam(ctx, OFFICE); + String minNum = ctx.queryParam(MIN_NUMBER); + String maxNum = ctx.queryParam(MAX_NUMBER); + Instant minDate = queryParamAsInstant(ctx, BEGIN); + Instant maxDate = queryParamAsInstant(ctx, END); + try (Timer.Context ignored = markAndTime(DELETE)) { + DSLContext dsl = getDslContext(ctx); + MeasurementDao dao = new MeasurementDao(dsl); + dao.deleteMeasurements(officeId, locationId, minDate, maxDate,minNum, maxNum); + ctx.status(HttpServletResponse.SC_NO_CONTENT).json( "Measurements for " + locationId + " Deleted"); + } + } + +} diff --git a/cwms-data-api/src/main/java/cwms/cda/api/OfficeController.java b/cwms-data-api/src/main/java/cwms/cda/api/OfficeController.java index 04b1a2018..150660104 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/OfficeController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/OfficeController.java @@ -107,7 +107,7 @@ public void getAll(Context ctx) { List offices = dao.getOffices(hasDataParm); String formatHeader = ctx.header(Header.ACCEPT); - ContentType contentType = Formats.parseHeaderAndQueryParm(formatHeader, formatParm, Office.class); + ContentType contentType = Formats.parseQueryOrHeaderParam(formatHeader, formatParm, Office.class); String result = Formats.format(contentType, offices, Office.class); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/ParametersController.java b/cwms-data-api/src/main/java/cwms/cda/api/ParametersController.java index 66520ca79..c7566f989 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/ParametersController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/ParametersController.java @@ -94,7 +94,7 @@ public void getAll(Context ctx) { String format = ctx.queryParamAsClass(FORMAT, String.class).getOrDefault(""); String office = ctx.queryParamAsClass(OFFICE, String.class).getOrDefault(null); String header = ctx.header(ACCEPT); - ContentType contentType = Formats.parseHeaderAndQueryParm(header, format, Parameter.class); + ContentType contentType = Formats.parseQueryOrHeaderParam(header, format, Parameter.class); String version = contentType.getParameters() .getOrDefault(VERSION, ""); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java b/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java index 9067bd3c8..aa450d843 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java @@ -40,6 +40,8 @@ import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import io.javalin.plugin.openapi.annotations.OpenApiSecurity; + import org.jooq.DSLContext; import javax.servlet.http.HttpServletResponse; @@ -81,6 +83,9 @@ private Timer.Context markAndTime(String subject) { @OpenApiContent(isArray = true, type = Formats.JSON, from = Property.class) }) }, + security = { + @OpenApiSecurity(name = "gets overridden allows lock icon.") + }, description = "Returns matching CWMS Property Data.", tags = {TAG} ) @@ -122,6 +127,9 @@ public void getAll(Context ctx) { @OpenApiContent(type = Formats.JSON, from = Property.class) }) }, + security = { + @OpenApiSecurity(name = "gets overridden allows lock icon.") + }, description = "Returns CWMS Property Data", tags = {TAG} ) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java index f838babd0..495183dbf 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java @@ -228,7 +228,8 @@ public void create(@NotNull Context ctx) { } @OpenApi( - description = "Update existing TimeSeriesGroup", + description = "Update existing TimeSeriesGroup. Allows for renaming of the group, " + + "assigning new time series, and unassigning all time series from the group.", requestBody = @OpenApiRequestBody( content = { @OpenApiContent(from = TimeSeriesGroup.class, type = Formats.JSON) @@ -239,31 +240,31 @@ public void create(@NotNull Context ctx) { + "unassign all existing time series before assigning new time series specified in the content body " + "Default: false"), @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " - + "owning office of the time series group to be updated"), + + "office of the user making the request. This is the office that the timeseries, group, and category " + + "belong to. If the group and/or category belong to the CWMS office, this only identifies the timeseries."), }, method = HttpMethod.PATCH, tags = {TAG} ) @Override - public void update(@NotNull Context ctx, String oldGroupId) { - + public void update(@NotNull Context ctx, @NotNull String oldGroupId) { try (Timer.Context ignored = markAndTime(CREATE)) { DSLContext dsl = getDslContext(ctx); - String formatHeader = ctx.req.getContentType(); String body = ctx.body(); + String office = requiredParam(ctx, OFFICE); ContentType contentType = Formats.parseHeader(formatHeader, TimeSeriesGroup.class); TimeSeriesGroup deserialize = Formats.parseContent(contentType, body, TimeSeriesGroup.class); boolean replaceAssignedTs = ctx.queryParamAsClass(REPLACE_ASSIGNED_TS, Boolean.class) .getOrDefault(false); TimeSeriesGroupDao timeSeriesGroupDao = new TimeSeriesGroupDao(dsl); - if (!oldGroupId.equals(deserialize.getId())) { + if (!office.equalsIgnoreCase(CWMS_OFFICE) && !oldGroupId.equals(deserialize.getId())) { timeSeriesGroupDao.renameTimeSeriesGroup(oldGroupId, deserialize); } if (replaceAssignedTs) { - timeSeriesGroupDao.unassignAllTs(deserialize); + timeSeriesGroupDao.unassignAllTs(deserialize, office); } - timeSeriesGroupDao.assignTs(deserialize); + timeSeriesGroupDao.assignTs(deserialize, office); ctx.status(HttpServletResponse.SC_OK); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesIdentifierDescriptorController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesIdentifierDescriptorController.java index f4c183ec8..7cc942063 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesIdentifierDescriptorController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesIdentifierDescriptorController.java @@ -25,28 +25,7 @@ package cwms.cda.api; import static com.codahale.metrics.MetricRegistry.name; -import static cwms.cda.api.Controllers.ACTIVE; -import static cwms.cda.api.Controllers.CREATE; -import static cwms.cda.api.Controllers.DELETE; -import static cwms.cda.api.Controllers.FAIL_IF_EXISTS; -import static cwms.cda.api.Controllers.GET_ALL; -import static cwms.cda.api.Controllers.GET_ONE; -import static cwms.cda.api.Controllers.INTERVAL_OFFSET; -import static cwms.cda.api.Controllers.METHOD; -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.RESULTS; -import static cwms.cda.api.Controllers.SIZE; -import static cwms.cda.api.Controllers.SNAP_BACKWARD; -import static cwms.cda.api.Controllers.SNAP_FORWARD; -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.TIMESERIES_ID; -import static cwms.cda.api.Controllers.TIMESERIES_ID_REGEX; -import static cwms.cda.api.Controllers.UPDATE; -import static cwms.cda.api.Controllers.requiredParam; +import static cwms.cda.api.Controllers.*; import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; @@ -189,7 +168,7 @@ public void getAll(Context ctx) { + "implemented")}, description = "Retrieves requested timeseries identifier descriptor", tags = {TAG}) @Override - public void getOne(Context ctx, @NotNull String timeseriesId) { + public void getOne(@NotNull Context ctx, @NotNull String timeseriesId) { try (final Timer.Context ignored = markAndTime(GET_ONE)){ DSLContext dsl = getDslContext(ctx); @@ -263,7 +242,7 @@ public void create(@NotNull Context ctx) { @OpenApi( pathParams = { - @OpenApiParam(name = TIMESERIES_ID, description = "The timeseries id"), + @OpenApiParam(name = NAME, description = "The timeseries id"), }, queryParams = { @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " @@ -280,7 +259,7 @@ public void create(@NotNull Context ctx) { }, tags = {TAG} ) @Override - public void update(Context ctx, @NotNull String timeseriesId) { + public void update(@NotNull Context ctx, @NotNull String name) { String office = requiredParam(ctx, OFFICE); String newTimeseriesId = ctx.queryParam(TIMESERIES_ID); @@ -306,13 +285,13 @@ public void update(Context ctx, @NotNull String timeseriesId) { if (foundUpdateKeys.isEmpty()) { // basic rename. - dao.rename(office, timeseriesId, newTimeseriesId, intervalOffset); + dao.rename(office, name, newTimeseriesId, intervalOffset); } else { Long forward = ctx.queryParamAsClass(SNAP_FORWARD, Long.class).getOrDefault(null); Long backward = ctx.queryParamAsClass(SNAP_BACKWARD, Long.class).getOrDefault(null); boolean active = ctx.queryParamAsClass(ACTIVE, Boolean.class).getOrDefault(true); - dao.update(office, timeseriesId, intervalOffset, forward, backward, active); + dao.update(office, name, intervalOffset, forward, backward, active); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeZoneController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeZoneController.java index 4a5295508..ea8c40bbb 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeZoneController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeZoneController.java @@ -91,8 +91,7 @@ public void getAll(Context ctx) { TimeZoneDao dao = new TimeZoneDao(dsl); String format = ctx.queryParamAsClass(FORMAT, String.class).getOrDefault(""); String header = ctx.header(ACCEPT); - - ContentType contentType = Formats.parseHeaderAndQueryParm(header, format, TimeZoneId.class); + ContentType contentType = Formats.parseQueryOrHeaderParam(header, format, TimeZoneId.class); String version = contentType.getParameters() .getOrDefault(VERSION, ""); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/UnitsController.java b/cwms-data-api/src/main/java/cwms/cda/api/UnitsController.java index c60384d7d..95051ed37 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/UnitsController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/UnitsController.java @@ -88,7 +88,7 @@ public void getAll(Context ctx) { String format = ctx.queryParamAsClass(FORMAT, String.class).getOrDefault(""); String header = ctx.header(ACCEPT); - ContentType contentType = Formats.parseHeaderAndQueryParm(header, format, Unit.class); + ContentType contentType = Formats.parseQueryOrHeaderParam(header, format, Unit.class); String version = contentType.getParameters() .getOrDefault(VERSION, ""); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/auth/ApiKeyController.java b/cwms-data-api/src/main/java/cwms/cda/api/auth/ApiKeyController.java index 689bef358..1f2d78c46 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/auth/ApiKeyController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/auth/ApiKeyController.java @@ -42,6 +42,8 @@ import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import io.javalin.plugin.openapi.annotations.OpenApiSecurity; + import java.util.List; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -120,6 +122,9 @@ public void delete(@NotNull Context ctx, @NotNull String keyName) { }, status = STATUS_201 ), + security = { + @OpenApiSecurity(name = "gets overridden allows lock icon.") + }, description = "View all keys for the current user", tags = {"Authorization"} ) @@ -145,6 +150,9 @@ public void getAll(Context ctx) { }, status = STATUS_201 ), + security = { + @OpenApiSecurity(name = "gets overridden allows lock icon.") + }, description = "View specific key", tags = {"Authorization"} ) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeCreateController.java b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeCreateController.java index 0fb91c8d6..c9c92b09f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeCreateController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeCreateController.java @@ -20,6 +20,10 @@ package cwms.cda.api.location.kind; +import static cwms.cda.api.Controllers.CREATE; +import static cwms.cda.api.Controllers.FAIL_IF_EXISTS; +import static cwms.cda.api.Controllers.STATUS_201; + import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import cwms.cda.api.BaseHandler; @@ -40,7 +44,7 @@ import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; -import static cwms.cda.api.Controllers.*; + public class GateChangeCreateController extends BaseHandler { @@ -49,22 +53,22 @@ public GateChangeCreateController(MetricRegistry metrics) { } @OpenApi( - requestBody = @OpenApiRequestBody( - content = { - @OpenApiContent(from = GateChange.class, isArray = true, type = Formats.JSONV1), - @OpenApiContent(from = GateChange.class, isArray = true, type = Formats.JSON) - }, - required = true), - queryParams = { - @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, - description = "Create will fail if provided Gate Changes already exist. Default: true") + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(from = GateChange.class, isArray = true, type = Formats.JSONV1), + @OpenApiContent(from = GateChange.class, isArray = true, type = Formats.JSON) }, - description = "Create CWMS Gate Changes", - method = HttpMethod.POST, - tags = {OutletController.TAG}, - responses = { - @OpenApiResponse(status = STATUS_201, description = "Gate Changes successfully stored to CWMS.") - } + required = true), + queryParams = { + @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, + description = "Create will fail if provided Gate Changes already exist. Default: true") + }, + description = "Create CWMS Gate Changes", + method = HttpMethod.POST, + tags = {OutletController.TAG}, + responses = { + @OpenApiResponse(status = STATUS_201, description = "Gate Changes successfully stored to CWMS.") + } ) @Override public void handle(@NotNull Context context) throws Exception { @@ -73,6 +77,9 @@ public void handle(@NotNull Context context) throws Exception { ContentType contentType = Formats.parseHeader(formatHeader, GateChange.class); List changes = Formats.parseContentList(contentType, context.body(), GateChange.class); + // Sort changes by date to avoid DB errors + changes.sort(GateChange::compareTo); + try (Timer.Context ignored = markAndTime(CREATE)) { DSLContext dsl = JooqDao.getDslContext(context); OutletDao dao = new OutletDao(dsl); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/OutletController.java b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/OutletController.java index 8fcabea7e..640eaf652 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/OutletController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/OutletController.java @@ -20,17 +20,30 @@ package cwms.cda.api.location.kind; -import com.codahale.metrics.Histogram; +import static cwms.cda.api.Controllers.CREATE; +import static cwms.cda.api.Controllers.DELETE; +import static cwms.cda.api.Controllers.FAIL_IF_EXISTS; +import static cwms.cda.api.Controllers.GET_ALL; +import static cwms.cda.api.Controllers.GET_ONE; +import static cwms.cda.api.Controllers.METHOD; +import static cwms.cda.api.Controllers.NAME; +import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.PROJECT_ID; +import static cwms.cda.api.Controllers.STATUS_200; +import static cwms.cda.api.Controllers.STATUS_204; +import static cwms.cda.api.Controllers.STATUS_404; +import static cwms.cda.api.Controllers.queryParamAsClass; +import static cwms.cda.api.Controllers.requiredParam; +import static cwms.cda.data.dao.JooqDao.getDslContext; + import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import cwms.cda.api.BaseCrudHandler; -import cwms.cda.api.Controllers; import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dao.location.kind.OutletDao; import cwms.cda.data.dto.location.kind.Outlet; 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; @@ -43,9 +56,7 @@ import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; -import static com.codahale.metrics.MetricRegistry.name; -import static cwms.cda.api.Controllers.*; -import static cwms.cda.data.dao.JooqDao.getDslContext; + public class OutletController extends BaseCrudHandler { static final String TAG = "Outlets"; @@ -55,22 +66,22 @@ public OutletController(MetricRegistry metrics) { } @OpenApi( - requestBody = @OpenApiRequestBody( - content = { - @OpenApiContent(from = Outlet.class, type = Formats.JSONV1), - @OpenApiContent(from = Outlet.class, type = Formats.JSON) - }, - required = true), - queryParams = { - @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, - description = "Create will fail if provided ID already exists. Default: true") + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(from = Outlet.class, type = Formats.JSONV1), + @OpenApiContent(from = Outlet.class, type = Formats.JSON) }, - description = "Create CWMS Outlet", - method = HttpMethod.POST, - tags = {TAG}, - responses = { - @OpenApiResponse(status = STATUS_204, description = "Outlet successfully stored to CWMS.") - } + required = true), + queryParams = { + @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, + description = "Create will fail if provided ID already exists. Default: true") + }, + description = "Create CWMS Outlet", + method = HttpMethod.POST, + tags = {TAG}, + responses = { + @OpenApiResponse(status = STATUS_204, description = "Outlet successfully stored to CWMS.") + } ) @Override public void create(@NotNull Context ctx) { @@ -88,20 +99,20 @@ public void create(@NotNull Context ctx) { } @OpenApi( - queryParams = { - @OpenApiParam(name = OFFICE, required = true, description = "Office id for the reservoir project location " + - "associated with the outlets."), - @OpenApiParam(name = PROJECT_ID, required = true, description = "Specifies the project-id of the " + - "Outlets whose data is to be included in the response."), - }, - responses = { - @OpenApiResponse(status = STATUS_200, content = { - @OpenApiContent(from = Outlet.class, isArray = true, type = Formats.JSONV1), - @OpenApiContent(from = Outlet.class, isArray = true, type = Formats.JSON) - }) - }, - description = "Returns matching CWMS Outlet Data for a Reservoir Project.", - tags = {TAG} + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Office id for the reservoir project location " + + "associated with the outlets."), + @OpenApiParam(name = PROJECT_ID, required = true, description = "Specifies the project-id of the " + + "Outlets whose data is to be included in the response."), + }, + responses = { + @OpenApiResponse(status = STATUS_200, content = { + @OpenApiContent(from = Outlet.class, isArray = true, type = Formats.JSONV1), + @OpenApiContent(from = Outlet.class, isArray = true, type = Formats.JSON) + }) + }, + description = "Returns matching CWMS Outlet Data for a Reservoir Project.", + tags = {TAG} ) @Override public void getAll(@NotNull Context ctx) { @@ -122,23 +133,23 @@ public void getAll(@NotNull Context ctx) { } @OpenApi( - pathParams = { - @OpenApiParam(name = NAME, required = true, description = "Specifies the location-id of the " + - "Outlet to be created."), - }, - queryParams = { - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office of " - + "the outlet to be retrieved."), - }, - responses = { - @OpenApiResponse(status = STATUS_200, - content = { - @OpenApiContent(from = Outlet.class, type = Formats.JSONV1), - @OpenApiContent(from = Outlet.class, type = Formats.JSON) - }) - }, - description = "Returns CWMS Outlet Data", - tags = {TAG} + pathParams = { + @OpenApiParam(name = NAME, required = true, description = "Specifies the location-id of the " + + "Outlet to be created."), + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office of " + + "the outlet to be retrieved."), + }, + responses = { + @OpenApiResponse(status = STATUS_200, + content = { + @OpenApiContent(from = Outlet.class, type = Formats.JSONV1), + @OpenApiContent(from = Outlet.class, type = Formats.JSON) + }) + }, + description = "Returns CWMS Outlet Data", + tags = {TAG} ) @Override public void getOne(@NotNull Context ctx, @NotNull String name) { @@ -158,21 +169,21 @@ public void getOne(@NotNull Context ctx, @NotNull String name) { } @OpenApi( - pathParams = { - @OpenApiParam(name = NAME, required = true, description = "Specifies the location-id of " - + "the outlet to be renamed."), - }, - queryParams = { - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office of " - + "the outlet to be renamed."), - @OpenApiParam(name = NAME, required = true, description = "Specifies the new outlet location-id."), - }, - description = "Rename CWMS Outlet", - method = HttpMethod.PATCH, - tags = {TAG}, - responses = { - @OpenApiResponse(status = STATUS_204, description = "CWMS Outlet successfully renamed.") - } + pathParams = { + @OpenApiParam(name = NAME, required = true, description = "Specifies the location-id of " + + "the outlet to be renamed."), + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office of " + + "the outlet to be renamed."), + @OpenApiParam(name = NAME, required = true, description = "Specifies the new outlet location-id."), + }, + description = "Rename CWMS Outlet", + method = HttpMethod.PATCH, + tags = {TAG}, + responses = { + @OpenApiResponse(status = STATUS_204, description = "CWMS Outlet successfully renamed.") + } ) @Override public void update(@NotNull Context ctx, @NotNull String name) { @@ -187,25 +198,24 @@ public void update(@NotNull Context ctx, @NotNull String name) { } @OpenApi( - pathParams = { - @OpenApiParam(name = NAME, required = true, description = "Specifies the location-id of the outlet to be" + - " deleted."), - }, - queryParams = { - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office of " - + "the outlet to be deleted."), - @OpenApiParam(name = METHOD, description = "Specifies the delete method used. " + - "Defaults to \"DELETE_KEY\"", - type = JooqDao.DeleteMethod.class) - }, - description = "Delete CWMS Outlet", - method = HttpMethod.DELETE, - tags = {TAG}, - responses = { - @OpenApiResponse(status = STATUS_204, description = "Outlet successfully deleted from CWMS."), - @OpenApiResponse(status = STATUS_404, description = "Based on the combination of " - + "inputs provided the outlet was not found.") - } + pathParams = { + @OpenApiParam(name = NAME, required = true, description = "Specifies the location-id of the outlet to be" + + " deleted."), + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office of " + + "the outlet to be deleted."), + @OpenApiParam(name = METHOD, description = "Specifies the delete method used. " + + "Defaults to \"DELETE_KEY\"", type = JooqDao.DeleteMethod.class) + }, + description = "Delete CWMS Outlet", + method = HttpMethod.DELETE, + tags = {TAG}, + responses = { + @OpenApiResponse(status = STATUS_204, description = "Outlet successfully deleted from CWMS."), + @OpenApiResponse(status = STATUS_404, description = "Based on the combination of " + + "inputs provided the outlet was not found.") + } ) @Override public void delete(@NotNull Context ctx, @NotNull String name) { diff --git a/cwms-data-api/src/main/java/cwms/cda/api/project/LockRevokerRightsCatalog.java b/cwms-data-api/src/main/java/cwms/cda/api/project/LockRevokerRightsCatalog.java index afb162fc8..a7e725f6a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/project/LockRevokerRightsCatalog.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/project/LockRevokerRightsCatalog.java @@ -49,6 +49,8 @@ import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import io.javalin.plugin.openapi.annotations.OpenApiSecurity; + import java.util.List; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; @@ -85,6 +87,9 @@ public LockRevokerRightsCatalog(MetricRegistry metrics) { + "application mask to be used to filter the lock revoker rights. " + "Defaults to '*'"), }, + security = { + @OpenApiSecurity(name = "gets overridden allows lock icon.") + }, responses = { @OpenApiResponse(status = STATUS_200, content = { @OpenApiContent(type = Formats.JSON, from = LockRevokerRights.class, isArray = true)} diff --git a/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectLockCatalog.java b/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectLockCatalog.java index 815b54667..488d2abc6 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectLockCatalog.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectLockCatalog.java @@ -49,6 +49,8 @@ import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import io.javalin.plugin.openapi.annotations.OpenApiSecurity; + import java.util.List; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; @@ -90,6 +92,9 @@ public ProjectLockCatalog(MetricRegistry metrics) { @OpenApiResponse(status = STATUS_200, content = { @OpenApiContent(type = Formats.JSON, from = ProjectLock.class)} )}, + security = { + @OpenApiSecurity(name = "gets overridden allows lock icon.") + }, tags = {TAGS}, path = PATH, method = HttpMethod.GET diff --git a/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectLockGetOne.java b/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectLockGetOne.java index 22c21ab00..c3da7dcd9 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectLockGetOne.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectLockGetOne.java @@ -50,6 +50,8 @@ import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import io.javalin.plugin.openapi.annotations.OpenApiSecurity; + import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; @@ -87,6 +89,9 @@ public ProjectLockGetOne(MetricRegistry metrics) { ), @OpenApiResponse(status = STATUS_404, description = "No matching Lock was found.") }, + security = { + @OpenApiSecurity(name = "gets overridden allows lock icon.") + }, tags = {TAGS}, method = HttpMethod.GET ) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractController.java index d251086d7..9b208dc28 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractController.java @@ -49,6 +49,8 @@ import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import io.javalin.plugin.openapi.annotations.OpenApiSecurity; + import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -80,6 +82,9 @@ public WaterContractController(MetricRegistry metrics) { + " did not find any contracts."), @OpenApiResponse(status = "501", description = "Requested format is not implemented.") }, + security = { + @OpenApiSecurity(name = "gets overridden allows lock icon.") + }, description = "Return a specified water contract", path = "/projects/{office}/{project-id}/water-users/{water-user}/contracts/{contract-name}", method = HttpMethod.GET, diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeCatalogController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeCatalogController.java index 4b8af1208..7a2ffa4e1 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeCatalogController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeCatalogController.java @@ -44,6 +44,8 @@ import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import io.javalin.plugin.openapi.annotations.OpenApiSecurity; + import java.util.List; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; @@ -70,6 +72,9 @@ public WaterContractTypeCatalogController(MetricRegistry metrics) { + " did not find any contracts."), @OpenApiResponse(status = "501", description = "Requested format is not implemented.") }, + security = { + @OpenApiSecurity(name = "gets overridden allows lock icon.") + }, description = "Get all water contract types", path = "/projects/contracts/{office}/contract-types", method = HttpMethod.GET, diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserCatalogController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserCatalogController.java index ddd66862c..758ca2f00 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserCatalogController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserCatalogController.java @@ -48,6 +48,8 @@ import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import io.javalin.plugin.openapi.annotations.OpenApiSecurity; + import java.util.List; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; @@ -73,6 +75,9 @@ public WaterUserCatalogController(MetricRegistry metrics) { @OpenApiContent(type = Formats.JSONV1, from = WaterUser.class) }) }, + security = { + @OpenApiSecurity(name = "gets overridden allows lock icon.") + }, description = "Gets all water users.", method = HttpMethod.GET, path = "/projects/{office}/{project-id}/water-user/all-users", diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserController.java index 57bf93241..90886a3ca 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserController.java @@ -49,6 +49,8 @@ import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import io.javalin.plugin.openapi.annotations.OpenApiSecurity; + import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -75,6 +77,9 @@ public WaterUserController(MetricRegistry metrics) { @OpenApiContent(type = Formats.JSONV1, from = WaterUserContract.class) }) }, + security = { + @OpenApiSecurity(name = "gets overridden allows lock icon.") + }, description = "Gets a specified water user.", method = HttpMethod.GET, path = "/projects/{office}/{project-id}/water-user/{water-user}", diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/AuthDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/AuthDao.java index 25ca55542..bc3673b27 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/AuthDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/AuthDao.java @@ -12,6 +12,7 @@ import cwms.cda.helpers.ResourceHelper; import cwms.cda.security.CwmsAuthException; import cwms.cda.security.DataApiPrincipal; +import cwms.cda.security.MissingRolesException; import cwms.cda.security.Role; import io.javalin.core.security.RouteRole; import io.javalin.http.Context; @@ -27,6 +28,7 @@ import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -35,6 +37,8 @@ import java.util.Set; import java.util.TimeZone; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + import javax.sql.DataSource; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -321,8 +325,7 @@ public static void isAuthorized(Context ctx, DataApiPrincipal p, Set logger.atFinest().log("User has required roles."); return; } else { - logger.atFine().log(); - throw new CwmsAuthException(getFailMessage(ctx,routeRoles,p),403); + throw new MissingRolesException(getMissingRoles(ctx, routeRoles, p)); } } else { throw new CwmsAuthException("No credentials provided.",401); @@ -351,14 +354,16 @@ public void prepareGuestContext(Context ctx) { } } - private static String getFailMessage(@NotNull Context ctx, + private static List getMissingRoles(@NotNull Context ctx, @NotNull Set requiredRoles, @NotNull DataApiPrincipal p) { Set specifiedRoles = p.getRoles(); Set missing = new LinkedHashSet<>(requiredRoles); missing.removeAll(specifiedRoles); - - return "Request for: " + ctx.req.getRequestURI() + " denied. Missing roles: " + missing; + return Collections.unmodifiableList( + missing.stream() + .map(r -> r.toString()) + .collect(Collectors.toList())); } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationGroupDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationGroupDao.java index 64b47e467..c2172669b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationGroupDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationGroupDao.java @@ -478,15 +478,15 @@ public void delete(String categoryId, String groupId, boolean cascadeDelete, Str * @param group The location group to create. */ public void create(LocationGroup group) { - String office = group.getOfficeId(); + String office = group.getOfficeId(); String categoryId = group.getLocationCategory().getId(); connection(dsl, conn -> { DSLContext dslContext = getDslContext(conn, office); CWMS_LOC_PACKAGE.call_CREATE_LOC_GROUP2(dslContext.configuration(), categoryId, - group.getId(), group.getDescription(), office, group.getSharedLocAliasId(), + group.getId(), group.getDescription(), group.getOfficeId(), group.getSharedLocAliasId(), group.getSharedRefLocationId()); - assignLocs(group); + assignLocs(dslContext, group, office); }); } @@ -512,8 +512,7 @@ public void renameLocationGroup(String oldGroupId, LocationGroup newGroup) { }); } - public void unassignAllLocs(LocationGroup group) { - String office = group.getOfficeId(); + public void unassignAllLocs(LocationGroup group, String office) { LocationCategory cat = group.getLocationCategory(); connection(dsl, conn -> { DSLContext dslContext = getDslContext(conn, office); @@ -522,21 +521,29 @@ public void unassignAllLocs(LocationGroup group) { }); } - public void assignLocs(LocationGroup group) { + public void assignLocs(LocationGroup group, String office) { + connection(dsl, conn -> { + DSLContext dslContext = getDslContext(conn, office); + assignLocs(dslContext,group, office); + }); + } + + /** + * Used when an appropriate context already exists to avoid opening a second connection. + * @param dslContext a dslContext that is assumed to be fully prepared for use in this operation + * @param group + * @param office + */ + public void assignLocs(DSLContext dslContext, LocationGroup group, String office) { List assignedLocations = group.getAssignedLocations(); if (assignedLocations != null) { List collect = assignedLocations.stream() .map(LocationGroupDao::convertToLocAliasType) .collect(toList()); LOC_ALIAS_ARRAY3 assignedLocs = new LOC_ALIAS_ARRAY3(collect); - - String office = group.getOfficeId(); - LocationCategory cat = group.getLocationCategory(); - connection(dsl, conn -> { - DSLContext dslContext = getDslContext(conn, office); + LocationCategory cat = group.getLocationCategory(); CWMS_LOC_PACKAGE.call_ASSIGN_LOC_GROUPS3(dslContext.configuration(), cat.getId(), group.getId(), assignedLocs, office); - }); } } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/MeasurementDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/MeasurementDao.java new file mode 100644 index 000000000..5aaac5932 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/MeasurementDao.java @@ -0,0 +1,558 @@ +/* + * 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.data.dao; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import cwms.cda.api.enums.UnitSystem; +import cwms.cda.api.errors.NotFoundException; +import cwms.cda.data.dto.CwmsId; +import cwms.cda.data.dto.measurement.Measurement; +import cwms.cda.data.dto.measurement.StreamflowMeasurement; +import cwms.cda.data.dto.measurement.SupplementalStreamflowMeasurement; +import cwms.cda.data.dto.measurement.UsgsMeasurement; +import java.io.IOException; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Date; +import java.util.TimeZone; +import mil.army.usace.hec.metadata.location.LocationTemplate; +import org.jooq.DSLContext; + +import java.util.List; + +import static java.util.stream.Collectors.toList; +import org.jooq.impl.DSL; +import usace.cwms.db.dao.util.OracleTypeMap; +import usace.cwms.db.jooq.codegen.packages.CWMS_STREAM_PACKAGE; +import usace.cwms.db.jooq.codegen.udt.records.STREAMFLOW_MEAS2_T; +import usace.cwms.db.jooq.codegen.udt.records.STREAMFLOW_MEAS2_TAB_T; + +public final class MeasurementDao extends JooqDao { + static final XmlMapper XML_MAPPER = buildXmlMapper(); + + public MeasurementDao(DSLContext dsl) { + super(dsl); + } + + /** + * Retrieve a list of measurements + * + * @param officeId - the office id + * @param locationId - the location id for filtering + * @param unitSystem - the unit system to use for the returned data + * @return a list of measurements + */ + public List retrieveMeasurements(String officeId, String locationId, Instant minDateMask, Instant maxDateMask, String unitSystem, + Number minHeight, Number maxHeight, Number minFlow, Number maxFlow, String minNum, String maxNum, + String agencies, String qualities) { + return connectionResult(dsl, conn -> { + setOffice(conn, officeId); + Timestamp minTimestamp = OracleTypeMap.buildTimestamp(minDateMask == null ? null : Date.from(minDateMask)); + Timestamp maxTimestamp = OracleTypeMap.buildTimestamp(maxDateMask == null ? null : Date.from(maxDateMask)); + TimeZone timeZone = OracleTypeMap.GMT_TIME_ZONE; + return retrieveMeasurementsJooq(conn, officeId, locationId, unitSystem, minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agencies, qualities, minTimestamp, maxTimestamp, timeZone); + }); + } + + private static List retrieveMeasurementsJooq(Connection conn, String officeId, String locationId, String unitSystem, Number minHeight, Number maxHeight, Number minFlow, Number maxFlow, String minNum, String maxNum, String agencies, String qualities, Timestamp minTimestamp, Timestamp maxTimestamp, TimeZone timeZone) { + STREAMFLOW_MEAS2_TAB_T retrieved = CWMS_STREAM_PACKAGE.call_RETRIEVE_MEAS_OBJS(DSL.using(conn).configuration(), locationId, unitSystem, minTimestamp, maxTimestamp, + minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agencies, qualities, timeZone.getID(), officeId); + List retVal = retrieved.stream() + .map(MeasurementDao::fromJooqMeasurementRecord) + .collect(toList()); + if(retVal.isEmpty()) { + throw new NotFoundException("No measurements found."); + } + return retVal; + } + + /** + * Store a list of measurements + * @param measurements - the measurements to store + * @param failIfExists - if true, fail if a measurement already exists + */ + public void storeMeasurements(List measurements, boolean failIfExists) { + connection(dsl, conn -> storeMeasurementsJooq(conn, measurements, failIfExists)); + } + + private void storeMeasurementsJooq(Connection conn, List measurements, boolean failIfExists) throws SQLException, JsonProcessingException { + if(!measurements.isEmpty()) { + Measurement measurement = measurements.get(0); + setOffice(conn, measurement.getOfficeId()); + String failIfExistsStr = formatBool(failIfExists); + String xml = toDbXml(measurements); + CWMS_STREAM_PACKAGE.call_STORE_MEAS_XML(DSL.using(conn).configuration(), xml, failIfExistsStr); + } + } + + /** + * Delete a measurement + * + * @param officeId - the office id + * @param locationId - the location id of the measurement to delete + * @param minNum + */ + public void deleteMeasurements(String officeId, String locationId, Instant minDateMask, Instant maxDateMask, String minNum, + String maxNum) { + connection(dsl, conn -> { + setOffice(conn, officeId); + Timestamp minTimestamp = OracleTypeMap.buildTimestamp(minDateMask == null ? null : Date.from(minDateMask)); + Timestamp maxTimestamp = OracleTypeMap.buildTimestamp(maxDateMask == null ? null : Date.from(maxDateMask)); + TimeZone timeZone = OracleTypeMap.GMT_TIME_ZONE; + String timeZoneId = timeZone.getID(); + verifyMeasurementsExists(conn, officeId, locationId, maxNum, maxNum); + CWMS_STREAM_PACKAGE.call_DELETE_STREAMFLOW_MEAS(DSL.using(conn).configuration(), locationId, minNum, minTimestamp, maxTimestamp, + null, null, null, null, maxNum, maxNum, null, null, timeZoneId, officeId); + }); + } + + private void verifyMeasurementsExists(Connection conn, String officeId, String locationId, String minNum, String maxNum) { + List measurements = retrieveMeasurementsJooq(conn, officeId, locationId, UnitSystem.EN.toString(), + null, null, null, null, minNum, maxNum, null, null, null, null, OracleTypeMap.GMT_TIME_ZONE); + if (measurements.isEmpty()) { + throw new NotFoundException("Could not find measurements for " + locationId + " in office " + officeId + "."); + } + } + + static String toDbXml(List measurements) throws JsonProcessingException { + MeasurementsXmlDto xmlDto = convertMeasurementsToXmlDto(measurements); + return XML_MAPPER.writeValueAsString(xmlDto); + } + + static Measurement fromJooqMeasurementRecord(STREAMFLOW_MEAS2_T record) { + LocationTemplate locationTemplate = new LocationTemplate(record.getLOCATION().getOFFICE_ID(), record.getLOCATION().getBASE_LOCATION_ID(), + record.getLOCATION().getSUB_LOCATION_ID()); + return new Measurement.Builder() + .withId(new CwmsId.Builder() + .withName(locationTemplate.getLocationId()) + .withOfficeId(locationTemplate.getOfficeId()) + .build()) + .withNumber(record.getMEAS_NUMBER()) + .withAgency(record.getAGENCY_ID()) + .withParty(record.getPARTY()) + .withUsed(parseBool(record.getUSED())) + .withWmComments(record.getWM_COMMENTS()) + .withInstant(record.getDATE_TIME().toInstant()) + .withAreaUnit(record.getAREA_UNIT()) + .withFlowUnit(record.getFLOW_UNIT()) + .withHeightUnit(record.getHEIGHT_UNIT()) + .withVelocityUnit(record.getVELOCITY_UNIT()) + .withTempUnit(record.getTEMP_UNIT()) + .withStreamflowMeasurement(new StreamflowMeasurement.Builder() + .withFlow(record.getFLOW()) + .withGageHeight(record.getGAGE_HEIGHT()) + .withQuality(record.getQUALITY()) + .build()) + .withUsgsMeasurement(new UsgsMeasurement.Builder() + .withAirTemp(record.getAIR_TEMP()) + .withCurrentRating(record.getCUR_RATING_NUM()) + .withControlCondition(record.getCTRL_COND_ID()) + .withFlowAdjustment(record.getFLOW_ADJ_ID()) + .withDeltaHeight(record.getDELTA_HEIGHT()) + .withDeltaTime(record.getDELTA_TIME()) + .withPercentDifference(record.getPCT_DIFF()) + .withRemarks(record.getREMARKS()) + .withShiftUsed(record.getSHIFT_USED()) + .withWaterTemp(record.getWATER_TEMP()) + .build()) + .withSupplementalStreamflowMeasurement(new SupplementalStreamflowMeasurement.Builder() + .withAvgVelocity(record.getSUPP_STREAMFLOW_MEAS().getAVG_VELOCITY()) + .withChannelFlow(record.getSUPP_STREAMFLOW_MEAS().getCHANNEL_FLOW()) + .withMeanGage(record.getSUPP_STREAMFLOW_MEAS().getMEAN_GAGE()) + .withMaxVelocity(record.getSUPP_STREAMFLOW_MEAS().getMAX_VELOCITY()) + .withOverbankFlow(record.getSUPP_STREAMFLOW_MEAS().getOVERBANK_FLOW()) + .withOverbankArea(record.getSUPP_STREAMFLOW_MEAS().getOVERBANK_AREA()) + .withTopWidth(record.getSUPP_STREAMFLOW_MEAS().getTOP_WIDTH()) + .withSurfaceVelocity(record.getSUPP_STREAMFLOW_MEAS().getSURFACE_VELOCITY()) + .withChannelMaxDepth(record.getSUPP_STREAMFLOW_MEAS().getCHANNEL_MAX_DEPTH()) + .withMainChannelArea(record.getSUPP_STREAMFLOW_MEAS().getMAIN_CHANNEL_AREA()) + .withOverbankMaxDepth(record.getSUPP_STREAMFLOW_MEAS().getOVERBANK_MAX_DEPTH()) + .withEffectiveFlowArea(record.getSUPP_STREAMFLOW_MEAS().getEFFECTIVE_FLOW_AREA()) + .withCrossSectionalArea(record.getSUPP_STREAMFLOW_MEAS().getCROSS_SECTIONAL_AREA()) + .build()) + .build(); + } + + static MeasurementsXmlDto convertMeasurementsToXmlDto(List measurements) { + return new MeasurementsXmlDto.Builder() + .withMeasurements(measurements.stream() + .map(MeasurementDao::convertMeasurementToXmlDto) + .collect(toList())) + .build(); + } + + static MeasurementXmlDto convertMeasurementToXmlDto(Measurement meas) + { + return new MeasurementXmlDto.Builder() + .withAgency(meas.getAgency()) + .withAreaUnit(meas.getAreaUnit()) + .withFlowUnit(meas.getFlowUnit()) + .withHeightUnit(meas.getHeightUnit()) + .withInstant(meas.getInstant()) + .withLocationId(meas.getLocationId()) + .withNumber(meas.getNumber()) + .withOfficeId(meas.getOfficeId()) + .withParty(meas.getParty()) + .withTempUnit(meas.getTempUnit()) + .withUsed(meas.isUsed()) + .withVelocityUnit(meas.getVelocityUnit()) + .withStreamflowMeasurement(meas.getStreamflowMeasurement()) + .withSupplementalStreamflowMeasurement(meas.getSupplementalStreamflowMeasurement()) + .withUsgsMeasurement(meas.getUsgsMeasurement()) + .withWmComments(meas.getWmComments()) + .build(); + } + + private static XmlMapper buildXmlMapper() { + XmlMapper retVal = new XmlMapper(); + retVal.registerModule(new JavaTimeModule()); + SimpleModule module = new SimpleModule(); + module.addSerializer(Instant.class, new InstantSerializer()); + module.addDeserializer(Instant.class, new InstantDeserializer()); + retVal.registerModule(module); + return retVal; + } + + private static class InstantSerializer extends JsonSerializer { + @Override + public void serialize(Instant value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(DateTimeFormatter.ISO_INSTANT.format(value)); + } + } + + private static class InstantDeserializer extends JsonDeserializer { + @Override + public Instant deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String text = p.getText(); + try { + return Instant.parse(text); + } catch (Exception e) { + try { + // Try parsing as OffsetDateTime next + OffsetDateTime offsetDateTime = OffsetDateTime.parse(text, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + return offsetDateTime.toInstant(); + } catch (Exception ex) { + try { + // Finally, fallback to parsing as ZonedDateTime + ZonedDateTime zonedDateTime = ZonedDateTime.parse(text, DateTimeFormatter.ISO_ZONED_DATE_TIME); + return zonedDateTime.toInstant(); + } catch (Exception finalEx) { + // Handle or rethrow as needed + throw new IOException("Failed to parse date-time string: " + text, finalEx); + } + } + } + } + } + + @JacksonXmlRootElement(localName = "measurements") + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonDeserialize(builder = MeasurementsXmlDto.Builder.class) + static class MeasurementsXmlDto { + + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "measurement") + private final List measurements; + + private MeasurementsXmlDto(Builder builder) { + this.measurements = builder.measurements; + } + + public List getMeasurements() { + return measurements; + } + static class Builder { + @JacksonXmlElementWrapper(useWrapping = false) // Disable wrapping for the collection + @JacksonXmlProperty(localName = "measurement") + private List measurements = new ArrayList<>(); + + Builder withMeasurements(List measurements) { + this.measurements = measurements; + return this; + } + + @JsonCreator + MeasurementsXmlDto build() { + return new MeasurementsXmlDto(this); + } + } + } + + + @JsonRootName(value = "measurement") + @JsonDeserialize(builder = MeasurementXmlDto.Builder.class) + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) + static final class MeasurementXmlDto { + + private final String heightUnit; + + private final String flowUnit; + + private final String tempUnit; + + private final String velocityUnit; + + private final String areaUnit; + + private final boolean used; + private final String agency; + private final String party; + private final String wmComments; + private final StreamflowMeasurement streamflowMeasurement; + private final SupplementalStreamflowMeasurement supplementalStreamflowMeasurement; + private final UsgsMeasurement usgsMeasurement; + private final String locationId; + private final String officeId; + private final String number; + private final Instant instant; + + private MeasurementXmlDto(Builder builder) { + this.heightUnit = builder.heightUnit; + this.flowUnit = builder.flowUnit; + this.tempUnit = builder.tempUnit; + this.velocityUnit = builder.velocityUnit; + this.areaUnit = builder.areaUnit; + this.used = builder.used; + this.agency = builder.agency; + this.party = builder.party; + this.wmComments = builder.wmComments; + this.locationId = builder.locationId; + this.officeId = builder.officeId; + this.number = builder.number; + this.instant = builder.instant; + this.streamflowMeasurement = builder.streamflowMeasurement; + this.supplementalStreamflowMeasurement = builder.supplementalStreamflowMeasurement; + this.usgsMeasurement = builder.usgsMeasurement; + } + + @JacksonXmlProperty(isAttribute = true) + public String getHeightUnit() { + return heightUnit; + } + + @JacksonXmlProperty(isAttribute = true) + public String getFlowUnit() { + return flowUnit; + } + + @JacksonXmlProperty(isAttribute = true) + public String getTempUnit() { + return tempUnit; + } + + @JacksonXmlProperty(isAttribute = true) + public String getVelocityUnit() { + return velocityUnit; + } + + @JacksonXmlProperty(isAttribute = true) + public String getAreaUnit() { + return areaUnit; + } + + @JacksonXmlProperty(isAttribute = true) + public boolean isUsed() { + return used; + } + + @JacksonXmlProperty(isAttribute = true) + public String getOfficeId() { + return officeId; + } + + public String getAgency() { + return agency; + } + + public String getParty() { + return party; + } + + public String getWmComments() { + return wmComments; + } + + @JsonProperty("location") + public String getLocationId() { + return locationId; + } + + @JsonProperty("date") + public Instant getInstant() { + return instant; + } + + public String getNumber() { + return number; + } + + @JsonProperty("stream-flow-measurement") + public StreamflowMeasurement getStreamflowMeasurement() { + return streamflowMeasurement; + } + + @JsonProperty("supplemental-stream-flow-measurement") + public SupplementalStreamflowMeasurement getSupplementalStreamflowMeasurement() { + return supplementalStreamflowMeasurement; + } + + public UsgsMeasurement getUsgsMeasurement() { + return usgsMeasurement; + } + + @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) + static final class Builder { + private String heightUnit; + private String flowUnit; + private String tempUnit; + private String velocityUnit; + private String areaUnit; + private boolean used; + private String agency; + private String party; + private String wmComments; + private StreamflowMeasurement streamflowMeasurement; + private SupplementalStreamflowMeasurement supplementalStreamflowMeasurement; + private UsgsMeasurement usgsMeasurement; + private String officeId; + private String locationId; + private Instant instant; + private String number; + + Builder withHeightUnit(String heightUnit) { + this.heightUnit = heightUnit; + return this; + } + + Builder withFlowUnit(String flowUnit) { + this.flowUnit = flowUnit; + return this; + } + + Builder withTempUnit(String tempUnit) { + this.tempUnit = tempUnit; + return this; + } + + Builder withVelocityUnit(String velocityUnit) { + this.velocityUnit = velocityUnit; + return this; + } + + Builder withAreaUnit(String areaUnit) { + this.areaUnit = areaUnit; + return this; + } + + Builder withUsed(boolean used) { + this.used = used; + return this; + } + + Builder withAgency(String agency) { + this.agency = agency; + return this; + } + + Builder withParty(String party) { + this.party = party; + return this; + } + + Builder withWmComments(String wmComments) { + this.wmComments = wmComments; + return this; + } + + @JsonProperty("stream-flow-measurement") + Builder withStreamflowMeasurement(StreamflowMeasurement streamflowMeasurement) { + this.streamflowMeasurement = streamflowMeasurement; + return this; + } + + @JsonProperty("supplemental-stream-flow-measurement") + Builder withSupplementalStreamflowMeasurement(SupplementalStreamflowMeasurement supplementalStreamflowMeasurement) { + this.supplementalStreamflowMeasurement = supplementalStreamflowMeasurement; + return this; + } + + Builder withUsgsMeasurement(UsgsMeasurement usgsMeasurement) { + this.usgsMeasurement = usgsMeasurement; + return this; + } + + Builder withOfficeId(String officeId) { + this.officeId = officeId; + return this; + } + + @JsonProperty("location") + Builder withLocationId(String locationId) { + this.locationId = locationId; + return this; + } + + Builder withNumber(String number) { + this.number = number; + return this; + } + + @JsonProperty("date") + Builder withInstant(Instant instant) { + this.instant = instant; + return this; + } + + MeasurementXmlDto build() { + return new MeasurementXmlDto(this); + } + } + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java index d0262dc42..f6698153f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java @@ -58,7 +58,6 @@ import org.jetbrains.annotations.Nullable; import org.jooq.CommonTableExpression; import org.jooq.Condition; -import org.jooq.Configuration; import org.jooq.DSLContext; import org.jooq.Field; import org.jooq.Record; @@ -89,6 +88,8 @@ import usace.cwms.db.jooq.codegen.tables.AV_TSV; import usace.cwms.db.jooq.codegen.tables.AV_TSV_DQU; import usace.cwms.db.jooq.codegen.tables.AV_TS_GRP_ASSGN; +import usace.cwms.db.jooq.codegen.udt.records.ZTSV_ARRAY; +import usace.cwms.db.jooq.codegen.udt.records.ZTSV_TYPE; public class TimeSeriesDaoImpl extends JooqDao implements TimeSeriesDao { private static final Logger logger = Logger.getLogger(TimeSeriesDaoImpl.class.getName()); @@ -418,11 +419,8 @@ public static String parseLocFromTimeSeriesId(String tsId) { } public static String getTimeZoneId(DSLContext dsl, String tsId, String officeId) { - return dsl.connectionResult(c -> { - Configuration config = getDslContext(c, officeId).configuration(); - String locationId = TimeSeriesDaoImpl.parseLocFromTimeSeriesId(tsId); - return CWMS_LOC_PACKAGE.call_GET_LOCAL_TIMEZONE__2(config, locationId, officeId); - }); + String locationId = TimeSeriesDaoImpl.parseLocFromTimeSeriesId(tsId); + return CWMS_LOC_PACKAGE.call_GET_LOCAL_TIMEZONE__2(dsl.configuration(), locationId, officeId); } public static VersionType getVersionType(DSLContext dsl, String names, String office, boolean dateProvided) { @@ -449,14 +447,8 @@ private static boolean isVersioned(DSLContext dsl, String tsId, String office) { Boolean cachedValue = isVersionedCache.getIfPresent(cacheKey); if (cachedValue == null) { - cachedValue = connectionResult(dsl, connection -> { - Configuration configuration = getDslContext(connection, office).configuration(); - boolean isVersioned = - parseBool(CWMS_TS_PACKAGE.call_IS_TSID_VERSIONED(configuration, - tsId, office)); - isVersionedCache.put(cacheKey, isVersioned); - return isVersioned; - }); + cachedValue = parseBool(CWMS_TS_PACKAGE.call_IS_TSID_VERSIONED(dsl.configuration(), tsId, office)); + isVersionedCache.put(cacheKey, cachedValue); } return cachedValue; } @@ -968,16 +960,16 @@ public List findMostRecentsInRange(List tsIds, Timestamp pa @NotNull private RecentValue buildRecentValue(AV_TSV_DQU tsvView, Record jrecord, String tsColumnName) { - TsvDqu tsv = buildTsvDqu(tsvView, jrecord); + TsvDqu tsv = buildTsvDqu(tsvView, jrecord, tsColumnName); String tsId = jrecord.getValue(tsColumnName, String.class); return new RecentValue(tsId, tsv); } @NotNull - private TsvDqu buildTsvDqu(AV_TSV_DQU tsvView, Record jrecord) { + private TsvDqu buildTsvDqu(AV_TSV_DQU tsvView, Record jrecord, String tsColumnName) { return new TsvDqu.Builder() .withOfficeId(jrecord.getValue(tsvView.OFFICE_ID)) - .withCwmsTsId(jrecord.getValue(tsvView.CWMS_TS_ID)) + .withCwmsTsId(jrecord.getValue(tsvView.CWMS_TS_ID.as(tsColumnName))) .withUnitId(jrecord.getValue(tsvView.UNIT_ID)) .withDateTime(jrecord.getValue(tsvView.DATE_TIME)) .withVersionDate(jrecord.getValue(tsvView.VERSION_DATE)) @@ -1130,21 +1122,18 @@ private void store(Connection connection, String officeId, String tsId, String u Timestamp versionDate, List values, boolean createAsLrts, StoreRule storeRule, boolean overrideProtection) throws SQLException { setOffice(connection,officeId); - CwmsDbTs tsDao = CwmsDbServiceLookup.buildCwmsDb(CwmsDbTs.class, connection); - - final int count = values == null ? 0 : values.size(); - final long[] timeArray = new long[count]; - final double[] valueArray = new double[count]; - final int[] qualityArray = new int[count]; + final ZTSV_ARRAY tsvArray = new ZTSV_ARRAY(); if (values != null && !values.isEmpty()) { Iterator iter = values.iterator(); - for (int i = 0; iter.hasNext(); i++) { + while (iter.hasNext()) { TimeSeries.Record value = iter.next(); - timeArray[i] = value.getDateTime().getTime(); - valueArray[i] = value.getValue(); - qualityArray[i] = value.getQualityCode(); + Double dataValue = value.getValue(); + if (dataValue != null && dataValue == -Float.MAX_VALUE) { + dataValue = null; + } + tsvArray.add(new ZTSV_TYPE(value.getDateTime(), dataValue, BigDecimal.valueOf(value.getQualityCode()))); } } @@ -1166,10 +1155,15 @@ private void store(Connection connection, String officeId, String tsId, String u } } } - - tsDao.store(connection, officeId, tsId, units, timeArray, valueArray, qualityArray, count, - storeRule.getRule(), overrideProtection, versionDate, createAsLrts); - + CWMS_TS_PACKAGE.call_ZSTORE_TS(getDslContext(connection, officeId).configuration(), + tsId, + units, + tsvArray, + storeRule.getRule(), + formatBool(overrideProtection), + versionDate, + officeId, + formatBool(createAsLrts)); } public void update(TimeSeries input, boolean createAsLrts, StoreRule storeRule, diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesGroupDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesGroupDao.java index 79fe3150b..3c06b9682 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesGroupDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesGroupDao.java @@ -253,12 +253,12 @@ public void create(TimeSeriesGroup group, boolean failIfExists) { group.getId(), group.getDescription(), formatBool(failIfExists), "T", group.getSharedAliasId(), group.getSharedRefTsId(), group.getOfficeId()); - assignTs(configuration,group); + assignTs(configuration,group, group.getOfficeId()); }); } - private void assignTs(Configuration configuration,TimeSeriesGroup group) { + private void assignTs(Configuration configuration,TimeSeriesGroup group, String office) { List assignedTimeSeries = group.getAssignedTimeSeries(); if(assignedTimeSeries != null) { @@ -267,12 +267,12 @@ private void assignTs(Configuration configuration,TimeSeriesGroup group) { .collect(toList()); TS_ALIAS_TAB_T assignedLocs = new TS_ALIAS_TAB_T(collect); CWMS_TS_PACKAGE.call_ASSIGN_TS_GROUPS(configuration, group.getTimeSeriesCategory().getId(), - group.getId(), assignedLocs, group.getOfficeId()); + group.getId(), assignedLocs, office); } } - public void assignTs(TimeSeriesGroup group) { - connection(dsl, c->assignTs(getDslContext(c,group.getOfficeId()).configuration(),group)); + public void assignTs(TimeSeriesGroup group, String office) { + connection(dsl, c->assignTs(getDslContext(c, office).configuration(),group, office)); } private static TS_ALIAS_T convertToTsAliasType(AssignedTimeSeries assignedTimeSeries) { @@ -290,12 +290,12 @@ public void renameTimeSeriesGroup(String oldGroupId, TimeSeriesGroup group) { ); } - public void unassignAllTs(TimeSeriesGroup group) { + public void unassignAllTs(TimeSeriesGroup group, String officeId) { connection(dsl, c -> CWMS_TS_PACKAGE.call_UNASSIGN_TS_GROUP( - getDslContext(c,group.getOfficeId()).configuration(), + getDslContext(c,officeId).configuration(), group.getTimeSeriesCategory().getId(), group.getId(), - null, "T", group.getOfficeId()) + null, "T", officeId) ); } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/location/kind/OutletDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/location/kind/OutletDao.java index a3f418723..9c8d49e53 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/location/kind/OutletDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/location/kind/OutletDao.java @@ -20,7 +20,10 @@ package cwms.cda.data.dao.location.kind; +import static cwms.cda.data.dao.location.kind.LocationUtil.getLocationRef; + import cwms.cda.api.enums.UnitSystem; +import cwms.cda.api.errors.DeleteConflictException; import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dao.DeleteRule; import cwms.cda.data.dao.JooqDao; @@ -37,6 +40,7 @@ import cwms.cda.data.dto.location.kind.VirtualOutlet; import cwms.cda.data.dto.location.kind.VirtualOutletRecord; import java.math.BigInteger; +import java.sql.SQLException; import java.sql.Timestamp; import java.time.Instant; import java.util.ArrayList; @@ -45,6 +49,7 @@ import java.util.stream.Collectors; import org.jooq.Configuration; import org.jooq.DSLContext; +import org.jooq.exception.IntegrityConstraintViolationException; import org.jooq.impl.DSL; import usace.cwms.db.jooq.codegen.packages.CWMS_OUTLET_PACKAGE; import usace.cwms.db.jooq.codegen.udt.records.GATE_CHANGE_OBJ_T; @@ -55,7 +60,7 @@ import usace.cwms.db.jooq.codegen.udt.records.PROJECT_STRUCTURE_OBJ_T; import usace.cwms.db.jooq.codegen.udt.records.STR_TAB_T; import usace.cwms.db.jooq.codegen.udt.records.STR_TAB_TAB_T; -import static cwms.cda.data.dao.location.kind.LocationUtil.getLocationRef; + public class OutletDao extends JooqDao { @@ -114,8 +119,14 @@ public void storeOutlet(Outlet outlet, boolean failIfExists) { public void deleteOutlet(String officeId, String locationId, DeleteRule deleteRule) { connection(dsl, conn -> { setOffice(conn, officeId); - CWMS_OUTLET_PACKAGE.call_DELETE_OUTLET(DSL.using(conn).configuration(), locationId, deleteRule.getRule(), - officeId); + try { + CWMS_OUTLET_PACKAGE.call_DELETE_OUTLET(DSL.using(conn).configuration(), locationId, + deleteRule.getRule(), officeId); + } catch (IntegrityConstraintViolationException e) { + SQLException cause = e.getCause(SQLException.class); + throw new DeleteConflictException("Cannot delete outlet " + locationId + " because of an integrity" + + " constraint violation.", cause); + } }); } @@ -257,8 +268,8 @@ public void renameOutlet(String officeId, String oldOutletId, String newOutletId } public List retrieveOperationalChanges(CwmsId projectId, Instant startTime, Instant endTime, - boolean startInclusive, boolean endInclusive, UnitSystem unitSystem, - long rowLimit) { + boolean startInclusive, boolean endInclusive, + UnitSystem unitSystem, long rowLimit) { return connectionResult(dsl, conn -> { setOffice(conn, projectId.getOfficeId()); @@ -267,11 +278,12 @@ public List retrieveOperationalChanges(CwmsId projectId, Instant sta Timestamp endTimestamp = Timestamp.from(endTime); BigInteger rowLimitBig = BigInteger.valueOf(rowLimit); GATE_CHANGE_TAB_T changeTab = CWMS_OUTLET_PACKAGE.call_RETRIEVE_GATE_CHANGES( - DSL.using(conn).configuration(), locationRef, startTimestamp, endTimestamp, "UTC", unitSystem.getValue(), - formatBool(startInclusive), formatBool(endInclusive), rowLimitBig); + DSL.using(conn).configuration(), locationRef, startTimestamp, endTimestamp, "UTC", + unitSystem.getValue(), formatBool(startInclusive), formatBool(endInclusive), rowLimitBig); if (changeTab == null) { - throw new NotFoundException("No changes found for " + projectId.getOfficeId() + "." + projectId.getName() + throw new NotFoundException("No changes found for " + projectId.getOfficeId() + "." + + projectId.getName() + "\nStart time: " + startTime + "\nEnd time: " + endTime + "\nStart inclusive: " + startInclusive diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeries.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeries.java index 2ba41ed1b..da2af8cbd 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeries.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeries.java @@ -232,7 +232,12 @@ private List getColumnDescriptor() { ), arraySchema = @Schema( type="array", - example = "[1509654000000, 54.3, 0]" + example = "[1509654000000, 54.3, 0]", + description = "Time is Milliseconds since the UNIX Epoch. Value is Double (for missing data you " + + "can use null, or -Float.MAX_VALUE (-340282346638528859811704183484516925440), " + + "quality is an integer.) If you are using missing data set the quality to 5." + + "Failure to do this may result in silently ignoring that value on not storing a " + + "placeholder which can be important in irregular and psuedo regular timeseries." ) ) public static class Record { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/GateChange.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/GateChange.java index f10b30c86..b333cff3d 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/GateChange.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/GateChange.java @@ -20,9 +20,12 @@ package cwms.cda.data.dto.location.kind; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; @@ -30,15 +33,21 @@ import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; import cwms.cda.formatters.json.JsonV1; -import java.util.Objects; + @FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON}) @JsonDeserialize(builder = GateChange.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeName("gate-change") +@JsonIgnoreProperties(ignoreUnknown = true) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) -@JsonPropertyOrder({"project-id", "change-date", "reference-elevation", "pool-elevation", "protected", "discharge-computation-type", "reason-type", "notes"}) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true) +@JsonPropertyOrder({"project-id", "change-date", "reference-elevation", "pool-elevation", "protected", + "discharge-computation-type", "reason-type", "notes", "type"}) public class GateChange extends PhysicalStructureChange { + @JsonProperty("reference-elevation") private final Double referenceElevation; + private static final String type = "gate-change"; GateChange(Builder builder) { super(builder); @@ -49,13 +58,22 @@ public Double getReferenceElevation() { return referenceElevation; } + public String getType() { + return type; + } + @Override protected void validateInternal(CwmsDTOValidator validator) { validator.required(getPoolElevation(), "pool-elevation"); super.validateInternal(validator); } + public int compareTo(GateChange other) { + return this.getChangeDate().compareTo(other.getChangeDate()); + } + public static final class Builder extends PhysicalStructureChange.Builder { + @JsonProperty("reference-elevation") private Double referenceElevation; public Builder() { @@ -72,6 +90,10 @@ public Builder referenceElevation(Double referenceElevation) { return this; } + public Builder withType(String type) { + return this; + } + @Override GateChange.Builder self() { return this; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/GateSetting.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/GateSetting.java index 49e0f3ade..17cf084fe 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/GateSetting.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/GateSetting.java @@ -22,6 +22,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -33,7 +35,9 @@ @FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON}) @JsonDeserialize(builder = GateSetting.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +@JsonSubTypes({@JsonSubTypes.Type(value = GateChange.class, name = "gate-change")}) @JsonTypeName("gate-setting") public final class GateSetting extends Setting { @JsonProperty(required = true) @@ -44,6 +48,7 @@ public final class GateSetting extends Setting { private final String openingUnits; @JsonProperty(required = true) private final Double invertElevation; + private final String type; private GateSetting(Builder builder) { super(builder); @@ -51,6 +56,7 @@ private GateSetting(Builder builder) { openingParameter = builder.openingParameter; openingUnits = builder.openingUnits; invertElevation = builder.invertElevation; + type = builder.type; } public Double getInvertElevation() { @@ -69,11 +75,16 @@ public String getOpeningUnits() { return openingUnits; } + public String getType() { + return type; + } + public static final class Builder extends Setting.Builder { private Double opening; private String openingParameter; private String openingUnits; private Double invertElevation; + private String type; @Override protected Builder self() { @@ -100,6 +111,11 @@ public Builder withInvertElevation(Double invertElevation) { return self(); } + public Builder withType(String type) { + this.type = type; + return self(); + } + @Override public GateSetting build() { return new GateSetting(this); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/Setting.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/Setting.java index a94cb75ee..42fde8d6a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/Setting.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/Setting.java @@ -31,7 +31,8 @@ import cwms.cda.data.dto.CwmsId; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({@JsonSubTypes.Type(value = TurbineSetting.class, name = "turbine-setting")}) +@JsonSubTypes({@JsonSubTypes.Type(value = TurbineSetting.class, name = "turbine-setting"), + @JsonSubTypes.Type(value = GateSetting.class, name = "gate-setting")}) public abstract class Setting extends CwmsDTOBase { @JsonProperty(required = true) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/Measurement.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/Measurement.java index 1df9a1675..491b035b9 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/Measurement.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/Measurement.java @@ -23,6 +23,7 @@ */ package cwms.cda.data.dto.measurement; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -128,6 +129,7 @@ public CwmsId getId() { return id; } + @JsonFormat(shape = JsonFormat.Shape.STRING) public Instant getInstant() { return instant; } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/StreamflowMeasurement.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/StreamflowMeasurement.java index 4f1fa90b7..84de60d4d 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/StreamflowMeasurement.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/StreamflowMeasurement.java @@ -59,6 +59,7 @@ public String getQuality() { return quality; } + @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) public static final class Builder { private Double gageHeight; private Double flow; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/SupplementalStreamflowMeasurement.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/SupplementalStreamflowMeasurement.java index abdd08d65..e41a65cbe 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/SupplementalStreamflowMeasurement.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/SupplementalStreamflowMeasurement.java @@ -120,6 +120,7 @@ public Double getOverbankArea() { return overbankArea; } + @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) public static final class Builder { private Double channelFlow; private Double overbankFlow; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/UsgsMeasurement.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/UsgsMeasurement.java index eb60afc85..12bcf5317 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/UsgsMeasurement.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/UsgsMeasurement.java @@ -114,6 +114,7 @@ public Double getWaterTemp() return waterTemp; } + @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) public static final class Builder { private String remarks; diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/Formats.java b/cwms-data-api/src/main/java/cwms/cda/formatters/Formats.java index a47a9a6d7..ac460b60e 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/Formats.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/Formats.java @@ -280,6 +280,30 @@ public static ContentType parseHeaderAndQueryParm(String header, String queryPar } } + /** + * For endpoints that still allow either for transition, favors the query parameter as that's the likely user + * expectation since machine systems wouldn't said both. + * @param header content type from a header + * @param queryParam content type from a query parameter + * @param klass DTO to find a matching formatter for. + * @return ContentType appropriate to the given selection. + * @throws UnsupportedFormatException if there is no matching content type for the given class + */ + public static ContentType parseQueryOrHeaderParam(String headerParam, String queryParam, Class klass) { + ContentType ct = null; + if (!(queryParam == null || queryParam.isEmpty())) { + ct = parseQueryParam(queryParam, klass); + } else if (headerParam != null) { + ct = parseHeader(headerParam, klass); + } else { + ct = parseHeader(DEFAULT, klass); + } + if (ct == null) { + throw new UnsupportedFormatException("Content-Type " + (headerParam == null ? queryParam : headerParam) + " is not available."); + } + return ct; + } + public static ContentType parseQueryParam(String queryParam, Class klass) { ContentTypeAliasMap aliasMap = ContentTypeAliasMap.empty(); diff --git a/cwms-data-api/src/main/java/cwms/cda/security/MissingRolesException.java b/cwms-data-api/src/main/java/cwms/cda/security/MissingRolesException.java new file mode 100644 index 000000000..d5e714fb5 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/security/MissingRolesException.java @@ -0,0 +1,19 @@ +package cwms.cda.security; + +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +public class MissingRolesException extends CwmsAuthException { + private List missingRoles; + + public MissingRolesException(List missingRoles) { + super("Missing Roles", HttpServletResponse.SC_FORBIDDEN); + this.missingRoles = missingRoles; + } + + @Override + public String getMessage() { + return "Missing roles {" + String.join(",",missingRoles) + "}"; + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/ControllersTest.java b/cwms-data-api/src/test/java/cwms/cda/api/ControllersTest.java index 2b07b7a2c..39a0c65f9 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/ControllersTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/ControllersTest.java @@ -413,4 +413,25 @@ void testRequiredParamAs() { assertThrows(RequiredQueryParameterException.class, () -> Controllers.requiredParamAs(ctx, Controllers.OFFICE, String.class)); } + + @Test + void testQueryParamAsDouble() { + final HttpServletRequest request = mock(HttpServletRequest.class); + final HttpServletResponse response = mock(HttpServletResponse.class); + Map urlParams = new LinkedHashMap<>(); + urlParams.put("a_double", "1.0"); + urlParams.put("an_int", "1"); + String paramStr = ControllerTest.buildParamStr(urlParams); + when(request.getQueryString()).thenReturn(paramStr); + Context ctx = new Context(request, response, new LinkedHashMap()); + + Double retVal = Controllers.queryParamAsDouble(ctx, "a_double"); + assertEquals(1.0, retVal); + + Double retVal2 = Controllers.queryParamAsDouble(ctx, "an_int"); + assertEquals(1.0, retVal2); + + Double retVal3 = Controllers.queryParamAsDouble(ctx, "null"); + assertNull(retVal3); + } } \ No newline at end of file diff --git a/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java index d6e982e42..6c930c1b8 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java @@ -29,6 +29,7 @@ import cwms.cda.data.dto.LocationCategory; import cwms.cda.data.dto.LocationGroup; import fixtures.CwmsDataApiSetupCallback; +import fixtures.IntegrationTestNameGenerator; import fixtures.TestAccounts; import fixtures.users.MockCwmsUserPrincipalImpl; import java.io.File; @@ -44,7 +45,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; import java.util.function.Consumer; @@ -64,6 +64,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.extension.ExtendWith; import usace.cwms.db.jooq.codegen.packages.CWMS_ENV_PACKAGE; @@ -71,17 +72,13 @@ * Helper class to manage cycling tests multiple times against a database. * NOTE: Not thread safe, do not run parallel tests. That may be future work though. */ +@DisplayNameGeneration(IntegrationTestNameGenerator.class) @Tag("integration") @ExtendWith(CwmsDataApiSetupCallback.class) public class DataApiTestIT { private static FluentLogger logger = FluentLogger.forEnclosingClass(); - /** - * List of locations that will need to be deleted when tests are done. - */ - private static ArrayList locationsCreated = new ArrayList<>(); protected static String createLocationQuery = null; - protected static String deleteLocationQuery = null; protected static String createTimeseriesQuery = null; protected static String createTimeseriesOffsetQuery = null; protected final static String registerApiKey = "insert into at_api_keys(userid,key_name,apikey) values(UPPER(?),?,?)"; @@ -140,11 +137,6 @@ public static void load_queries() throws Exception { .getResourceAsStream("cwms/cda/data/sql_templates/create_timeseries_offset.sql"),"UTF-8" ); - deleteLocationQuery = IOUtils.toString( - TimeseriesControllerTestIT.class - .getClassLoader() - .getResourceAsStream("cwms/cda/data/sql_templates/delete_location.sql"),"UTF-8" - ); } /** @@ -196,35 +188,6 @@ public void sessionEvent(SessionEvent event) { } } - /** - * Runs cascade delete on each locations. Location not existing is not an error. - * All other errors will throw a runtime exception and may require manual database - * cleanup. - */ - @AfterAll - public static void remove_data() { - Iterator it = locationsCreated.iterator(); - while(it.hasNext()) { - try { - Location location = it.next(); - CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); - db.connection((c)-> { - try(PreparedStatement stmt = c.prepareStatement(deleteLocationQuery)) { - stmt.setString(1,location.getName()); - stmt.setString(2,location.getOfficeId()); - stmt.execute(); - } catch (SQLException ex) { - if (ex.getErrorCode() != 20025 /*does not exist*/) { - throw new RuntimeException("Unable to remove location",ex); - } - } - },"cwms_20"); - it.remove(); - } catch(SQLException ex) { - throw new RuntimeException(ex); - } - } - } /** * Removes all registered users' API keys from the database. @@ -275,7 +238,7 @@ protected static void createLocation(String location, boolean active, String off office) .withActive(active) .build(); - if (locationsCreated.contains(loc)) { + if (LocationCleanup.locationsCreated.contains(loc)) { return; // we already have this location registered } @@ -290,7 +253,7 @@ protected static void createLocation(String location, boolean active, String off stmt.setString(7,horizontalDatum); stmt.setString(8,kind); stmt.execute(); - locationsCreated.add(loc); + LocationCleanup.locationsCreated.add(loc); } catch (SQLException ex) { throw new RuntimeException("Unable to create location",ex); } 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 579b5da38..0e57f9e9a 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 @@ -35,6 +35,7 @@ import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import org.jooq.DSLContext; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -44,6 +45,7 @@ import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.util.ArrayList; import java.util.List; import java.util.NavigableMap; import java.util.TreeMap; @@ -59,6 +61,18 @@ public class LevelsControllerTestIT extends DataApiTestIT { public static final String OFFICE = "SPK"; + private final List levelList = new ArrayList<>(); + + @AfterEach + void cleanup() throws Exception { + CwmsDataApiSetupCallback.getDatabaseLink().connection(c -> { + DSLContext dsl = dslContext(c, OFFICE); + LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); + for (LocationLevel level : levelList) { + dao.deleteLocationLevel(level.getLocationLevelId(), level.getLevelDate(), level.getOfficeId(), false); + } + }); + } @Test void test_location_level() throws Exception { @@ -70,11 +84,12 @@ void test_location_level() throws Exception { .withConstantValue(1.0) .withLevelUnitsId("ac-ft") .build(); - CwmsDataApiSetupCallback.getDatabaseLink().connection(c -> { - DSLContext dsl = dslContext(c, OFFICE); - LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); - dao.storeLocationLevel(level); - }); + levelList.add(level); + CwmsDataApiSetupCallback.getDatabaseLink().connection(c -> { + DSLContext dsl = dslContext(c, OFFICE); + LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); + dao.storeLocationLevel(level); + }); //Read level without unit given() @@ -116,7 +131,110 @@ void test_location_level() throws Exception { .body("constant-value",equalTo(1.0F)); } + @Test + void test_retrieve_time_window() throws Exception { + createLocation("level_get_all_loc_1", true, OFFICE); + String levelId = "level_get_all_loc_1.Flow.Ave.1Day.Regulating"; + ZonedDateTime time = ZonedDateTime.of(2023, 6, 1, 0, 0, 0, 0, ZoneId.of("America/Los_Angeles")); + CwmsDataApiSetupCallback.getDatabaseLink().connection(c -> { + LocationLevel level = new LocationLevel.Builder(levelId, time) + .withOfficeId(OFFICE) + .withConstantValue(1.0) + .withLevelUnitsId("cms") + .build(); + levelList.add(level); + DSLContext dsl = dslContext(c, OFFICE); + LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); + dao.storeLocationLevel(level); + }); + + String locId2 = "level_get_all_loc_2"; + String levelId2 = locId2 + ".Stor.Ave.1Day.Regulating"; + createLocation(locId2, true, OFFICE); + CwmsDataApiSetupCallback.getDatabaseLink().connection(c -> { + + LocationLevel level = new LocationLevel.Builder(levelId2, time) + .withOfficeId(OFFICE) + .withConstantValue(2.0) + .withLevelUnitsId("ac-ft") + .build(); + levelList.add(level); + DSLContext dsl = dslContext(c, OFFICE); + LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); + dao.storeLocationLevel(level); + }); + + //Read level with begin + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(LEVEL_ID_MASK, "level_get_all_loc_*") + .queryParam(BEGIN, "2020-06-01T00:00:00Z") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/levels/") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .extract(); + + assertThat(response.path("levels.size()"),is(2)); + assertEquals(OFFICE, response.path("levels[0].office-id")); + assertEquals(levelId, response.path("levels[0].location-level-id")); + assertEquals("Regulating", response.path("levels[0].specified-level-id")); + assertEquals("Ave", response.path("levels[0].parameter-type-id")); + assertEquals("Flow", response.path("levels[0].parameter-id")); + assertEquals("cms", response.path("levels[0].level-units-id")); + assertEquals("2023-06-01T07:00:00Z", response.path("levels[0].level-date")); + assertEquals("1Day", response.path("levels[0].duration-id")); + assertEquals(OFFICE, response.path("levels[1].office-id")); + assertEquals(levelId2, response.path("levels[1].location-level-id")); + assertEquals("Regulating", response.path("levels[1].specified-level-id")); + assertEquals("Ave", response.path("levels[1].parameter-type-id")); + assertEquals("Stor", response.path("levels[1].parameter-id")); + assertEquals("m3", response.path("levels[1].level-units-id")); + assertEquals("2023-06-01T07:00:00Z", response.path("levels[1].level-date")); + assertEquals("1Day", response.path("levels[1].duration-id")); + + //Read level without begin and end + response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(LEVEL_ID_MASK, "level_get_all_loc_*") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/levels/") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .extract(); + assertThat(response.path("levels.size()"),is(2)); + assertEquals(OFFICE, response.path("levels[0].office-id")); + assertEquals(levelId, response.path("levels[0].location-level-id")); + assertEquals("Regulating", response.path("levels[0].specified-level-id")); + assertEquals("Ave", response.path("levels[0].parameter-type-id")); + assertEquals("Flow", response.path("levels[0].parameter-id")); + assertEquals("cms", response.path("levels[0].level-units-id")); + assertEquals("2023-06-01T07:00:00Z", response.path("levels[0].level-date")); + assertEquals("1Day", response.path("levels[0].duration-id")); + assertEquals(OFFICE, response.path("levels[1].office-id")); + assertEquals(levelId2, response.path("levels[1].location-level-id")); + assertEquals("Regulating", response.path("levels[1].specified-level-id")); + assertEquals("Ave", response.path("levels[1].parameter-type-id")); + assertEquals("Stor", response.path("levels[1].parameter-id")); + assertEquals("m3", response.path("levels[1].level-units-id")); + assertEquals("2023-06-01T07:00:00Z", response.path("levels[1].level-date")); + assertEquals("1Day", response.path("levels[1].duration-id")); + } @Test void test_level_as_timeseries() throws Exception { @@ -132,6 +250,7 @@ void test_level_as_timeseries() throws Exception { .withConstantValue((double) i) .withLevelUnitsId("cfs") .build(); + levelList.add(level); levels.put(level.getLevelDate().toInstant(), level); CwmsDataApiSetupCallback.getDatabaseLink().connection(c -> { DSLContext dsl = dslContext(c, OFFICE); @@ -147,7 +266,7 @@ void test_level_as_timeseries() throws Exception { .accept(Formats.JSONV2) .contentType(Formats.JSONV2) .header("Authorization", user.toHeaderValue()) - .queryParam("office", OFFICE) + .queryParam(Controllers.OFFICE, OFFICE) .queryParam(BEGIN, time.toInstant().toString()) .queryParam(END, time.plusDays(effectiveDateCount).toInstant().toString()) .queryParam(INTERVAL, "1Hour") @@ -269,6 +388,7 @@ void test_get_all_location_level() throws Exception { .withConstantValue(1.0) .withLevelUnitsId("ac-ft") .build(); + levelList.add(level); DSLContext dsl = dslContext(c, OFFICE); LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); dao.storeLocationLevel(level); @@ -284,6 +404,7 @@ void test_get_all_location_level() throws Exception { .withConstantValue(2.0) .withLevelUnitsId("ac-ft") .build(); + levelList.add(level); DSLContext dsl = dslContext(c, OFFICE); LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); dao.storeLocationLevel(level); @@ -292,14 +413,12 @@ void test_get_all_location_level() throws Exception { String startStr = "2023-06-01T00:00:00Z"; String endStr = "2023-06-02T00:00:00Z"; - - //Read level without unit ExtractableResponse response = given() .log().ifValidationFails(LogDetail.ALL, true) .accept(Formats.JSONV2) .contentType(Formats.JSONV2) - .queryParam("office", OFFICE) + .queryParam(Controllers.OFFICE, OFFICE) .queryParam(LEVEL_ID_MASK, "level_get_all.*") .queryParam(BEGIN, startStr) .queryParam(END, endStr) @@ -341,7 +460,7 @@ void test_get_all_location_level() throws Exception { .log().ifValidationFails(LogDetail.ALL, true) .accept(Formats.JSONV2) .contentType(Formats.JSONV2) - .queryParam("office", OFFICE) + .queryParam(Controllers.OFFICE, OFFICE) .queryParam(UNIT, "SI") .queryParam(LEVEL_ID_MASK, "level_get_all.*") .queryParam(BEGIN, startStr) @@ -383,7 +502,7 @@ void test_get_all_location_level() throws Exception { .log().ifValidationFails(LogDetail.ALL, true) .accept(Formats.JSONV2) .contentType(Formats.JSONV2) - .queryParam("office", OFFICE) + .queryParam(Controllers.OFFICE, OFFICE) .queryParam(UNIT, "EN") .queryParam(LEVEL_ID_MASK, "level_get_all.*") .queryParam(BEGIN, startStr) @@ -420,6 +539,125 @@ void test_get_all_location_level() throws Exception { assertThat(response.path("levels[1].constant-value"), floatCloseTo(2.0, 0.01)); } + @Test + void test_get_all_earliest_time() throws Exception { + String locId = "level_get_all_loc1"; + String levelId = locId + ".Stor.Ave.1Day.Regulating"; + createLocation(locId, true, OFFICE); + final ZonedDateTime time = ZonedDateTime.of(2023, 6, 1, 0, 0, 0, 0, ZoneId.of("America" + + "/Los_Angeles")); + CwmsDataApiSetupCallback.getDatabaseLink().connection(c -> { + LocationLevel level = new LocationLevel.Builder(levelId, time) + .withOfficeId(OFFICE) + .withConstantValue(1.0) + .withLevelUnitsId("ac-ft") + .build(); + levelList.add(level); + DSLContext dsl = dslContext(c, OFFICE); + LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); + dao.storeLocationLevel(level); + }); + + String locId2 = "level_get_all_loc2"; + String levelId2 = locId2 + ".Stor.Ave.1Day.Regulating"; + createLocation(locId2, true, OFFICE); + CwmsDataApiSetupCallback.getDatabaseLink().connection(c -> { + + LocationLevel level = new LocationLevel.Builder(levelId2, time) + .withOfficeId(OFFICE) + .withConstantValue(2.0) + .withLevelUnitsId("ac-ft") + .build(); + levelList.add(level); + DSLContext dsl = dslContext(c, OFFICE); + LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); + dao.storeLocationLevel(level); + }); + + // Get all with minimum timestamp accepted by the database + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(LEVEL_ID_MASK, "level_get_all.*") + .queryParam(BEGIN, "-4712-11-25T00:00:00") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/levels/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .extract(); + + assertThat(response.path("levels.size()"),is(2)); + + assertThat(response.path("levels[0].office-id"),equalTo(OFFICE)); + assertThat(response.path("levels[0].location-level-id"),equalTo(levelId)); + assertThat(response.path("levels[0].specified-level-id"),equalTo("Regulating")); + assertThat(response.path("levels[0].parameter-type-id"),equalTo("Ave")); + assertThat(response.path("levels[0].parameter-id"),equalTo("Stor")); + assertThat(response.path("levels[0].level-units-id"),equalTo("m3")); + assertThat(response.path("levels[0].level-date"),equalTo("2023-06-01T07:00:00Z")); + assertThat(response.path("levels[0].duration-id"),equalTo("1Day")); + double actual0 = Float.valueOf((float) response.path("levels[0].constant-value")).doubleValue(); + assertThat(actual0, closeTo(1233.0, 10.0)); + + assertThat(response.path("levels[1].office-id"),equalTo(OFFICE)); + assertThat(response.path("levels[1].location-level-id"),equalTo(levelId2)); + assertThat(response.path("levels[1].specified-level-id"),equalTo("Regulating")); + assertThat(response.path("levels[1].parameter-type-id"),equalTo("Ave")); + assertThat(response.path("levels[1].parameter-id"),equalTo("Stor")); + assertThat(response.path("levels[1].level-units-id"),equalTo("m3")); + assertThat(response.path("levels[1].level-date"),equalTo("2023-06-01T07:00:00Z")); + assertThat(response.path("levels[1].duration-id"),equalTo("1Day")); + double actual1 = Float.valueOf((float) response.path("levels[1].constant-value")).doubleValue(); + assertThat(actual1, closeTo(2466.9636f, 1.0)); + + //Read level without time window + response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(LEVEL_ID_MASK, "level_get_all.*") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/levels/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .extract(); + + assertThat(response.path("levels.size()"),is(2)); + + assertThat(response.path("levels[0].office-id"),equalTo(OFFICE)); + assertThat(response.path("levels[0].location-level-id"),equalTo(levelId)); + assertThat(response.path("levels[0].specified-level-id"),equalTo("Regulating")); + assertThat(response.path("levels[0].parameter-type-id"),equalTo("Ave")); + assertThat(response.path("levels[0].parameter-id"),equalTo("Stor")); + assertThat(response.path("levels[0].level-units-id"),equalTo("m3")); + assertThat(response.path("levels[0].level-date"),equalTo("2023-06-01T07:00:00Z")); + assertThat(response.path("levels[0].duration-id"),equalTo("1Day")); + actual0 = Float.valueOf((float) response.path("levels[0].constant-value")).doubleValue(); + assertThat(actual0, closeTo(1233.0, 10.0)); + + assertThat(response.path("levels[1].office-id"),equalTo(OFFICE)); + assertThat(response.path("levels[1].location-level-id"),equalTo(levelId2)); + assertThat(response.path("levels[1].specified-level-id"),equalTo("Regulating")); + assertThat(response.path("levels[1].parameter-type-id"),equalTo("Ave")); + assertThat(response.path("levels[1].parameter-id"),equalTo("Stor")); + assertThat(response.path("levels[1].level-units-id"),equalTo("m3")); + assertThat(response.path("levels[1].level-date"),equalTo("2023-06-01T07:00:00Z")); + assertThat(response.path("levels[1].duration-id"),equalTo("1Day")); + actual1 = Float.valueOf((float) response.path("levels[1].constant-value")).doubleValue(); + assertThat(actual1, closeTo(2466.9636f, 1.0)); + } + @ParameterizedTest @EnumSource(GetAllTestNewAliases.class) void test_get_all_aliases_new(GetAllTestNewAliases test) @@ -427,7 +665,7 @@ void test_get_all_aliases_new(GetAllTestNewAliases test) given() .log().ifValidationFails(LogDetail.ALL, true) .accept(test._accept) - .queryParam("office", OFFICE) + .queryParam(Controllers.OFFICE, OFFICE) .queryParam(LEVEL_ID_MASK, "level_get_all.*") .when() .redirects().follow(true) @@ -447,7 +685,7 @@ void test_get_all_aliases_legacy(GetAllTestLegacy test) given() .log().ifValidationFails(LogDetail.ALL, true) .queryParam(FORMAT, test._format) - .queryParam("office", OFFICE) + .queryParam(Controllers.OFFICE, OFFICE) .queryParam(LEVEL_ID_MASK, "level_get_all.*") .when() .redirects() diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LocationCleanup.java b/cwms-data-api/src/test/java/cwms/cda/api/LocationCleanup.java new file mode 100644 index 000000000..d0e89be4f --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/LocationCleanup.java @@ -0,0 +1,90 @@ +/* + * 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 cwms.cda.data.dto.Location; +import fixtures.CwmsDataApiSetupCallback; +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; +import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import org.apache.commons.io.IOUtils; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestPlan; + +public class LocationCleanup implements TestExecutionListener { + + /** + * List of locations that will need to be deleted when tests are done. + */ + public static final Set locationsCreated = new LinkedHashSet<>(); + + @Override + public void testPlanExecutionFinished(TestPlan testPlan) { + try { + cleanupLocations(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Runs cascade delete on each location. Location not existing is not an error. + * All other errors will throw a runtime exception and may require manual database + * cleanup. + */ + private static void cleanupLocations() throws IOException { + + String deleteLocationQuery = IOUtils.toString( + CwmsDataApiSetupCallback.class + .getClassLoader() + .getResourceAsStream("cwms/cda/data/sql_templates/delete_location.sql"),"UTF-8" + ); + Iterator it = locationsCreated.iterator(); + while(it.hasNext()) { + try { + Location location = it.next(); + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection((c)-> { + try(PreparedStatement stmt = c.prepareStatement(deleteLocationQuery)) { + stmt.setString(1,location.getName()); + stmt.setString(2,location.getOfficeId()); + stmt.execute(); + } catch (SQLException ex) { + if (ex.getErrorCode() != 20025 /*does not exist*/) { + throw new RuntimeException("Unable to remove location",ex); + } + } + },"cwms_20"); + it.remove(); + } catch(SQLException ex) { + throw new RuntimeException(ex); + } + } + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LocationGroupControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LocationGroupControllerTestIT.java index 1c6bb002a..61b385c59 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LocationGroupControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LocationGroupControllerTestIT.java @@ -24,10 +24,20 @@ package cwms.cda.api; +import cwms.cda.api.errors.NotFoundException; +import cwms.cda.data.dao.LocationCategoryDao; +import cwms.cda.data.dao.LocationGroupDao; +import fixtures.CwmsDataApiSetupCallback; import fixtures.TestAccounts; import io.restassured.filter.log.LogDetail; -import org.junit.jupiter.api.Disabled; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import org.jooq.Configuration; +import org.jooq.impl.DSL; +import org.jooq.util.oracle.OracleDSL; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -36,32 +46,70 @@ import cwms.cda.data.dto.LocationGroup; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; +import usace.cwms.db.jooq.codegen.packages.CWMS_ENV_PACKAGE; import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import static cwms.cda.api.Controllers.*; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; @Tag("integration") -@Disabled() class LocationGroupControllerTestIT extends DataApiTestIT { + private static final Logger LOGGER = Logger.getLogger(LocationGroupControllerTestIT.class.getName()); + private List groupsToCleanup = new ArrayList<>(); + private List categoriesToCleanup = new ArrayList<>(); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + @AfterEach + void tearDown() throws Exception { + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection(c -> { + + Configuration configuration = OracleDSL.using(c).configuration(); + CWMS_ENV_PACKAGE.call_SET_SESSION_OFFICE_ID(DSL.using(c).configuration(), "SPK"); + LocationCategoryDao locationCategoryDao = new LocationCategoryDao(DSL.using(configuration)); + LocationGroupDao locationGroupDao = new LocationGroupDao(DSL.using(configuration)); + for (LocationGroup group : groupsToCleanup) { + try { + locationGroupDao.unassignAllLocs(group, "SPK"); + if (!group.getOfficeId().equalsIgnoreCase(CWMS_OFFICE) || !group.getId().equalsIgnoreCase("Default")) { + locationGroupDao.delete(group.getLocationCategory().getId(), group.getId(), true, group.getOfficeId()); + } + } catch (NotFoundException e) { + LOGGER.log(Level.CONFIG, String.format("Failed to delete location group: %s", group.getId()), e); + } + } + for (LocationCategory category : categoriesToCleanup) { + try { + locationCategoryDao.delete(category.getId(), true, category.getOfficeId()); + } catch (NotFoundException e) { + LOGGER.log(Level.CONFIG, String.format("Failed to delete location category: %s", category.getId()), e); + } + } + }, CwmsDataApiSetupCallback.getWebUser()); + } @Test void test_getall() throws Exception { - String officeId = "SPK"; String locationId = "LocationGroupTest"; + String officeId = user.getOperatingOffice(); createLocation(locationId, true, officeId); - TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + AssignedLocation assignLoc = new AssignedLocation(locationId, officeId, "AliasId", 1, locationId); LocationCategory cat = new LocationCategory(officeId, "TestCategory", "IntegrationTesting"); - LocationGroup group = new LocationGroup(cat, officeId, LocationGroupControllerTestIT.class.getSimpleName(), "IntegrationTesting", - "sharedLocAliasId", locationId, 123); - List assignedLocations = group.getAssignedLocations(); - assignedLocations.add(new AssignedLocation(locationId, officeId, "AliasId", 1, locationId)); + LocationGroup group = new LocationGroup(new LocationGroup(cat, officeId, LocationGroupControllerTestIT.class.getSimpleName(), "IntegrationTesting", + "sharedLocAliasId", locationId, 123), Collections.singletonList(assignLoc)); ContentType contentType = Formats.parseHeader(Formats.JSON, LocationCategory.class); String categoryXml = Formats.format(contentType, cat); String groupXml = Formats.format(contentType, group); + groupsToCleanup.add(group); + categoriesToCleanup.add(cat); //Create Category given() .log().ifValidationFails(LogDetail.ALL,true) @@ -130,18 +178,18 @@ void test_getall() throws Exception { @Test void test_create_read_delete() throws Exception { - String officeId = "SPK"; + String officeId = user.getOperatingOffice(); String locationId = "LocationGroupTest"; createLocation(locationId, true, officeId); - TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; - LocationCategory cat = new LocationCategory(officeId, "TestCategory", "IntegrationTesting"); - LocationGroup group = new LocationGroup(cat, officeId, LocationGroupControllerTestIT.class.getSimpleName(), "IntegrationTesting", - "sharedLocAliasId", locationId, 123); - List assignedLocations = group.getAssignedLocations(); - assignedLocations.add(new AssignedLocation(locationId, officeId, "AliasId", 1, locationId)); + LocationCategory cat = new LocationCategory(officeId, "TestCategory2", "IntegrationTesting"); + AssignedLocation assignLoc = new AssignedLocation(locationId, officeId, "AliasId", 1, locationId); + LocationGroup group = new LocationGroup(new LocationGroup(cat, officeId, LocationGroupControllerTestIT.class.getSimpleName(), "IntegrationTesting", + "sharedLocAliasId", locationId, 123), Collections.singletonList(assignLoc)); ContentType contentType = Formats.parseHeader(Formats.JSON, LocationCategory.class); String categoryXml = Formats.format(contentType, cat); String groupXml = Formats.format(contentType, group); + groupsToCleanup.add(group); + categoriesToCleanup.add(cat); //Create Category given() .log().ifValidationFails(LogDetail.ALL,true) @@ -217,7 +265,8 @@ void test_create_read_delete() throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .accept(Formats.JSON) .contentType(Formats.JSON) - .queryParam("office", officeId) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, group.getLocationCategory().getId()) .when() .redirects().follow(true) .redirects().max(3) @@ -246,17 +295,15 @@ void test_create_read_delete() throws Exception { @Test void test_rename_group() throws Exception { - String officeId = "SPK"; + String officeId = user.getOperatingOffice(); String locationId = "LocationGroupTest"; createLocation(locationId, true, officeId); - TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + AssignedLocation assignLoc = new AssignedLocation(locationId, officeId, "AliasId", 1, locationId); LocationCategory cat = new LocationCategory(officeId, "test_rename_group", "IntegrationTesting"); - LocationGroup group = new LocationGroup(cat, officeId, "test_rename_group", "IntegrationTesting", - "sharedLocAliasId", locationId, 123); - registerCategory(cat); - registerGroup(group); - List assignedLocations = group.getAssignedLocations(); - assignedLocations.add(new AssignedLocation(locationId, officeId, "AliasId", 1, locationId)); + LocationGroup group = new LocationGroup(new LocationGroup(cat, officeId, "test_rename_group", "IntegrationTesting", + "sharedLocAliasId", locationId, 123), Collections.singletonList(assignLoc)); + groupsToCleanup.add(group); + categoriesToCleanup.add(cat); ContentType contentType = Formats.parseHeader(Formats.JSON, LocationCategory.class); String categoryXml = Formats.format(contentType, cat); String groupXml = Formats.format(contentType, group); @@ -292,7 +339,7 @@ void test_rename_group() throws Exception { .statusCode(is(HttpServletResponse.SC_CREATED)); LocationGroup newGroup = new LocationGroup(cat, officeId, "test_rename_group_new", "IntegrationTesting", "sharedLocAliasId", locationId, 123); - registerGroup(newGroup); + groupsToCleanup.add(newGroup); String newGroupXml = Formats.format(contentType, newGroup); //Rename Group given() @@ -302,7 +349,7 @@ void test_rename_group() throws Exception { .body(newGroupXml) .header("Authorization", user.toHeaderValue()) .header(CATEGORY_ID, group.getLocationCategory().getId()) - //.header(OFFICE, group.getOfficeId()) + .queryParam(OFFICE, group.getOfficeId()) .when() .redirects().follow(true) .redirects().max(3) @@ -338,7 +385,7 @@ void test_rename_group() throws Exception { .accept(Formats.JSON) .contentType(Formats.JSON) .header("Authorization", user.toHeaderValue()) - //.queryParam(OFFICE, officeId) + .queryParam(OFFICE, officeId) .queryParam(CASCADE_DELETE, "true") .when() .redirects().follow(true) @@ -352,20 +399,21 @@ void test_rename_group() throws Exception { @Test void test_add_assigned_locs() throws Exception { - String officeId = "SPK"; + String officeId = user.getOperatingOffice(); String locationId = "LocationGroupTest"; createLocation(locationId, true, officeId); String locationId2 = "LocationGroupTest2"; createLocation(locationId2, true, officeId); - TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + AssignedLocation assignLoc = new AssignedLocation(locationId, officeId, "AliasId", 1, locationId); LocationCategory cat = new LocationCategory(officeId, "test_add_assigned_locs", "IntegrationTesting"); - LocationGroup group = new LocationGroup(cat, officeId, "test_add_assigned_locs", "IntegrationTesting", - "sharedLocAliasId", locationId, 123); + LocationGroup group = new LocationGroup(new LocationGroup(cat, officeId, "test_add_assigned_locs", "IntegrationTesting", + "sharedLocAliasId", locationId, 123), Collections.singletonList(assignLoc)); List assignedLocations = group.getAssignedLocations(); - assignedLocations.add(new AssignedLocation(locationId, officeId, "AliasId", 1, locationId)); ContentType contentType = Formats.parseHeader(Formats.JSON, LocationCategory.class); String categoryXml = Formats.format(contentType, cat); String groupXml = Formats.format(contentType, group); + groupsToCleanup.add(group); + categoriesToCleanup.add(cat); //Create Category given() .log().ifValidationFails(LogDetail.ALL,true) @@ -398,13 +446,15 @@ void test_add_assigned_locs() throws Exception { .statusCode(is(HttpServletResponse.SC_CREATED)); assignedLocations.clear(); assignedLocations.add(new AssignedLocation(locationId2, officeId, "AliasId2", 2, locationId2)); - groupXml = Formats.format(contentType, group); + LocationGroup newGroup = new LocationGroup(group, assignedLocations); + groupsToCleanup.add(newGroup); + String newGroupJson = Formats.format(contentType, newGroup); //Add Assigned Locs given() .log().ifValidationFails(LogDetail.ALL,true) .accept(Formats.JSON) .contentType(Formats.JSON) - .body(groupXml) + .body(newGroupJson) .header("Authorization", user.toHeaderValue()) .queryParam(CATEGORY_ID, group.getLocationCategory().getId()) .queryParam(REPLACE_ASSIGNED_LOCS, "true") @@ -445,7 +495,7 @@ void test_add_assigned_locs() throws Exception { .contentType(Formats.JSON) .header("Authorization", user.toHeaderValue()) .queryParam(OFFICE, officeId) - .queryParam(CASCADE_DELETE, "true") + .queryParam(CASCADE_DELETE, true) .when() .redirects().follow(true) .redirects().max(3) @@ -455,4 +505,345 @@ void test_add_assigned_locs() throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); } + + @Test + void test_district_permissions() throws Exception { + String officeId = user.getOperatingOffice(); + String locationId = "LocationGroupTest"; + createLocation(locationId, true, officeId); + + LocationCategory cat = new LocationCategory(officeId, "test_CWMS_permissions", "Loc Group Integration Testing"); + + AssignedLocation assignLoc = new AssignedLocation(locationId, officeId, null, null, null); + LocationGroup group = new LocationGroup(cat, officeId, "test_CWMS_permissions", "Loc Group Integration Testing", + null, locationId, null); + LocationGroup newLocGroup = new LocationGroup(new LocationGroup(cat, officeId, "test_new_CWMS_permissions", "Second Loc Group IT", + null, locationId, null), Collections.singletonList(assignLoc)); + groupsToCleanup.add(group); + categoriesToCleanup.add(cat); + groupsToCleanup.add(newLocGroup); + + String catJson = Formats.format(new ContentType(Formats.JSON), cat); + String groupJson = Formats.format(new ContentType(Formats.JSON), group); + String newGroupJson = Formats.format(new ContentType(Formats.JSON), newLocGroup); + + // create a location category + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(catJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/category/") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // create a location group + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(groupJson) + .header("Authorization", user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/group/") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // patch the location group owned by district office + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(newGroupJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/location/group/" + group.getId()) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_OK)); + + // get the location group and assert that changes were made + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, newLocGroup.getLocationCategory().getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + newLocGroup.getId()) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("office-id", equalTo(newLocGroup.getOfficeId())) + .body("id", equalTo(newLocGroup.getId())) + .body("description", equalTo(newLocGroup.getDescription())) + .body("assigned-locations[0].location-id", equalTo(locationId)) + .body("assigned-locations[0].alias-id", nullValue()) + .body("assigned-locations[0].ref-location-id", nullValue()); + } + + @Test + void test_CWMS_permissions() throws Exception { + String officeId = user.getOperatingOffice(); + String locationId = "LocationGroupTest"; + createLocation(locationId, true, officeId); + + LocationCategory cat = new LocationCategory(CWMS_OFFICE, "Default", "Default"); + + AssignedLocation assignLoc = new AssignedLocation(locationId, officeId, null, null, null); + LocationGroup group = new LocationGroup(cat, CWMS_OFFICE, "Default", "All Locations", + null, null, null); + LocationGroup newLocGroup = new LocationGroup(group, Collections.singletonList(assignLoc)); + + groupsToCleanup.add(group); + + String newGroupJson = Formats.format(new ContentType(Formats.JSON), newLocGroup); + String groupJson = Formats.format(new ContentType(Formats.JSON), group); + + // get the location group and assert that changes were made + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .queryParam(OFFICE, CWMS_OFFICE) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .extract() + ; + int expectedSize = response.body().jsonPath().getInt("assigned-locations.size()"); + + // patch the location group owned by CWMS office + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(newGroupJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(REPLACE_ASSIGNED_LOCS, true) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/location/group/" + newLocGroup.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + // get the location group and assert that changes were made + ExtractableResponse otherResponse = given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .queryParam(OFFICE, CWMS_OFFICE) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + newLocGroup.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .extract(); + + assertEquals(otherResponse.body().jsonPath().getString("office-id"), group.getOfficeId()); + assertEquals(otherResponse.body().jsonPath().getString("id"), group.getId()); + assertEquals(otherResponse.body().jsonPath().getString("description"), newLocGroup.getDescription()); + List assignedLocations = otherResponse.body().jsonPath().getList("assigned-locations", AssignedLocation.class); + boolean found = false; + for (AssignedLocation assignedLocation : assignedLocations) { + if (assignedLocation.getLocationId().equals(locationId)) { + assertEquals(assignedLocation.getLocationId(), locationId); + assertNull(assignedLocation.getAliasId()); + assertNull(assignedLocation.getRefLocationId()); + found = true; + } + } + assertTrue(found); + + // patch the location group owned by CWMS office + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(groupJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(REPLACE_ASSIGNED_LOCS, true) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/location/group/" + newLocGroup.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + // get the location group and assert that there are no assigned locations + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .queryParam(OFFICE, CWMS_OFFICE) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("assigned-locations.size()", equalTo(expectedSize)); + } + + @Test + void test_CWMS_permissions_with_replacement() throws Exception { + String officeId = user.getOperatingOffice(); + String locationId = "LocationGroupTest"; + createLocation(locationId, true, officeId); + + LocationCategory cat = new LocationCategory(CWMS_OFFICE, "Default", "Default"); + + AssignedLocation assignLoc = new AssignedLocation(locationId, officeId, null, null, null); + LocationGroup group = new LocationGroup(cat, CWMS_OFFICE, "Default", "All Locations", + null, null, null); + LocationGroup newLocGroup = new LocationGroup(group, Collections.singletonList(assignLoc)); + + groupsToCleanup.add(group); + + String newGroupJson = Formats.format(new ContentType(Formats.JSON), newLocGroup); + String groupJson = Formats.format(new ContentType(Formats.JSON), group); + + // get the starting size of the assigned locations + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .queryParam(OFFICE, CWMS_OFFICE) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .extract(); + + int expectedSize = response.body().jsonPath().getInt("assigned-locations.size()"); + + // patch the location group owned by CWMS office + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(newGroupJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(REPLACE_ASSIGNED_LOCS, true) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/location/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + // get the location group and assert that changes were made + ExtractableResponse otherResponse = given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .queryParam(OFFICE, CWMS_OFFICE) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .extract(); + + assertEquals(otherResponse.body().jsonPath().getString("office-id"), group.getOfficeId()); + assertEquals(otherResponse.body().jsonPath().getString("id"), group.getId()); + assertEquals(otherResponse.body().jsonPath().getString("description"), newLocGroup.getDescription()); + List assignedLocations = otherResponse.body().jsonPath().getList("assigned-locations", AssignedLocation.class); + boolean found = false; + for (AssignedLocation assignedLocation : assignedLocations) { + if (assignedLocation.getLocationId().equals(locationId)) { + assertEquals(assignedLocation.getLocationId(), locationId); + assertNull(assignedLocation.getAliasId()); + assertNull(assignedLocation.getRefLocationId()); + found = true; + } + } + assertTrue(found); + + // patch the location group owned by CWMS office + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(groupJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(REPLACE_ASSIGNED_LOCS, true) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/location/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + // get the location group and assert that changes were made + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .queryParam(OFFICE, CWMS_OFFICE) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("assigned-locations.size()", equalTo(expectedSize)); + } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/MeasurementControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/MeasurementControllerTestIT.java new file mode 100644 index 000000000..aab51f262 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/MeasurementControllerTestIT.java @@ -0,0 +1,431 @@ +/* + * 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 com.google.common.flogger.FluentLogger; +import cwms.cda.api.enums.UnitSystem; +import static cwms.cda.data.dao.DaoTest.getDslContext; +import cwms.cda.data.dao.DeleteRule; +import cwms.cda.data.dao.MeasurementDao; +import static cwms.cda.data.dao.MeasurementDaoTestIT.MINIMUM_SCHEMA; +import cwms.cda.data.dao.StreamDao; +import cwms.cda.data.dto.CwmsId; +import cwms.cda.data.dto.measurement.Measurement; +import cwms.cda.data.dto.stream.Stream; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import static cwms.cda.security.KeyAccessManager.AUTH_HEADER; +import fixtures.CwmsDataApiSetupCallback; +import fixtures.MinimumSchema; +import fixtures.TestAccounts; +import static io.restassured.RestAssured.given; +import io.restassured.filter.log.LogDetail; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterAll; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import static org.hamcrest.Matchers.*; + +@Tag("integration") +final class MeasurementControllerTestIT extends DataApiTestIT { + + + private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass(); + private static final String OFFICE_ID = TestAccounts.KeyUser.SPK_NORMAL.getOperatingOffice(); + private static final List TEST_STREAMS = new ArrayList<>(); + private static final List TEST_STREAM_LOC_IDS = new ArrayList<>(); + + @BeforeAll + public static void setup() throws SQLException { + String testLoc = "StreamLoc321"; // match the stream location name in the json file + createLocation(testLoc, true, OFFICE_ID, "STREAM_LOCATION"); + TEST_STREAM_LOC_IDS.add(testLoc); + createAndStoreTestStream("ImOnThisStream2"); + } + + static void createAndStoreTestStream(String testLoc) throws SQLException { + createLocation(testLoc, true, OFFICE_ID, "STREAM"); + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection(c -> { + StreamDao streamDao = new StreamDao(getDslContext(c, OFFICE_ID)); + Stream streamToStore = new Stream.Builder() + .withId(new CwmsId.Builder() + .withOfficeId(OFFICE_ID) + .withName(testLoc) + .build()) + .withLength(100.0) + .withLengthUnits("km") + .build(); + TEST_STREAMS.add(streamToStore); + streamDao.storeStream(streamToStore, false); + }, CwmsDataApiSetupCallback.getWebUser()); + } + + @AfterAll + public static void tearDown() { + for (Stream stream : TEST_STREAMS) { + try { + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection(c -> { + StreamDao streamDao = new StreamDao(getDslContext(c, OFFICE_ID)); + try { + streamDao.deleteStream(stream.getId().getOfficeId(), stream.getId().getName(), DeleteRule.DELETE_ALL); + } catch (Exception e) { + LOGGER.atInfo().log("Failed to delete stream: " + stream.getId().getName() + ". Stream likely already deleted"); + } + }, CwmsDataApiSetupCallback.getWebUser()); + } catch (SQLException ex) { + throw new RuntimeException(ex); + } + } + TEST_STREAMS.clear(); + for(String measLoc: TEST_STREAM_LOC_IDS) + { + try { + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection(c -> { + MeasurementDao measDao = new MeasurementDao(getDslContext(c, OFFICE_ID)); + try { + measDao.deleteMeasurements(OFFICE_ID, measLoc, null, null, null, null); + } catch (Exception e) { + LOGGER.atInfo().log("Failed to delete measurements for: " + measLoc + ". Measurement(s) likely already deleted"); + } + }, CwmsDataApiSetupCallback.getWebUser()); + } catch (SQLException ex) { + throw new RuntimeException(ex); + } + } + } + + @Test + @MinimumSchema(MINIMUM_SCHEMA) + void test_create_retrieve_delete_measurement() throws IOException { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/measurement.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(json); + List measurements = Formats.parseContentList(new ContentType(Formats.JSON), json, Measurement.class); + Measurement measurement = measurements.get(0); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Create the Measurement + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/measurements/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + String locationId = measurement.getLocationId(); + String number = measurement.getNumber(); + + // Retrieve the Measurement and assert that it exists + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE_MASK, measurement.getId().getOfficeId()) + .queryParam(Controllers.ID_MASK, measurement.getLocationId()) + .queryParam(Controllers.MIN_NUMBER, number) + .queryParam(Controllers.MAX_NUMBER, number) + .queryParam(Controllers.MIN_HEIGHT, 0.0) + .queryParam(Controllers.MAX_FLOW, 1000) + .queryParam(Controllers.UNIT_SYSTEM, UnitSystem.EN.getValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/measurements/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("[0].height-unit", equalTo(measurement.getHeightUnit())) + .body("[0].flow-unit", equalTo(measurement.getFlowUnit())) + .body("[0].temp-unit", equalTo(measurement.getTempUnit())) + .body("[0].velocity-unit", equalTo(measurement.getVelocityUnit())) + .body("[0].area-unit", equalTo(measurement.getAreaUnit())) + .body("[0].used", equalTo(measurement.isUsed())) + .body("[0].agency", equalTo(measurement.getAgency())) + .body("[0].party", equalTo(measurement.getParty())) + .body("[0].wm-comments", equalTo(measurement.getWmComments())) + .body("[0].instant", equalTo(measurement.getInstant().toString())) + .body("[0].number", equalTo(measurement.getNumber())) + .body("[0].id.name", equalTo(measurement.getLocationId())) + .body("[0].id.office-id", equalTo(measurement.getOfficeId())) + .body("[0].streamflow-measurement.gage-height", equalTo(measurement.getStreamflowMeasurement().getGageHeight().floatValue())) + .body("[0].streamflow-measurement.flow", equalTo(measurement.getStreamflowMeasurement().getFlow().floatValue())) + .body("[0].streamflow-measurement.quality", equalTo(measurement.getStreamflowMeasurement().getQuality())) + .body("[0].supplemental-streamflow-measurement.channel-flow", equalTo(measurement.getSupplementalStreamflowMeasurement().getChannelFlow().floatValue())) + .body("[0].supplemental-streamflow-measurement.overbank-flow", equalTo(measurement.getSupplementalStreamflowMeasurement().getOverbankFlow().floatValue())) + .body("[0].supplemental-streamflow-measurement.overbank-max-depth", equalTo(measurement.getSupplementalStreamflowMeasurement().getOverbankMaxDepth().floatValue())) + .body("[0].supplemental-streamflow-measurement.channel-max-depth", equalTo(measurement.getSupplementalStreamflowMeasurement().getChannelMaxDepth().floatValue())) + .body("[0].supplemental-streamflow-measurement.avg-velocity", equalTo(measurement.getSupplementalStreamflowMeasurement().getAvgVelocity().floatValue())) + .body("[0].supplemental-streamflow-measurement.surface-velocity", equalTo(measurement.getSupplementalStreamflowMeasurement().getSurfaceVelocity().floatValue())) + .body("[0].supplemental-streamflow-measurement.max-velocity", equalTo(measurement.getSupplementalStreamflowMeasurement().getMaxVelocity().floatValue())) + .body("[0].supplemental-streamflow-measurement.effective-flow-area", equalTo(measurement.getSupplementalStreamflowMeasurement().getEffectiveFlowArea().floatValue())) + .body("[0].supplemental-streamflow-measurement.cross-sectional-area", equalTo(measurement.getSupplementalStreamflowMeasurement().getCrossSectionalArea().floatValue())) + .body("[0].supplemental-streamflow-measurement.mean-gage", equalTo(measurement.getSupplementalStreamflowMeasurement().getMeanGage().floatValue())) + .body("[0].supplemental-streamflow-measurement.top-width", equalTo(measurement.getSupplementalStreamflowMeasurement().getTopWidth().floatValue())) + .body("[0].supplemental-streamflow-measurement.main-channel-area", equalTo(measurement.getSupplementalStreamflowMeasurement().getMainChannelArea().floatValue())) + .body("[0].supplemental-streamflow-measurement.overbank-area", equalTo(measurement.getSupplementalStreamflowMeasurement().getOverbankArea().floatValue())) + .body("[0].usgs-measurement.remarks", equalTo(measurement.getUsgsMeasurement().getRemarks())) + .body("[0].usgs-measurement.current-rating", equalTo(measurement.getUsgsMeasurement().getCurrentRating())) + .body("[0].usgs-measurement.control-condition", equalTo(measurement.getUsgsMeasurement().getControlCondition())) + .body("[0].usgs-measurement.flow-adjustment", equalTo(measurement.getUsgsMeasurement().getFlowAdjustment())) + .body("[0].usgs-measurement.shift-used", equalTo(measurement.getUsgsMeasurement().getShiftUsed().floatValue())) + .body("[0].usgs-measurement.percent-difference", equalTo(measurement.getUsgsMeasurement().getPercentDifference().floatValue())) + .body("[0].usgs-measurement.delta-height", equalTo(measurement.getUsgsMeasurement().getDeltaHeight().floatValue())) + .body("[0].usgs-measurement.delta-time", equalTo(measurement.getUsgsMeasurement().getDeltaTime().floatValue())) + .body("[0].usgs-measurement.air-temp", equalTo(measurement.getUsgsMeasurement().getAirTemp().floatValue())) + .body("[0].usgs-measurement.water-temp", equalTo(measurement.getUsgsMeasurement().getWaterTemp().floatValue())); + + // Delete the Measurement + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(Controllers.OFFICE, measurement.getId().getOfficeId()) + .queryParam(Controllers.MIN_NUMBER, number) + .queryParam(Controllers.MAX_NUMBER, number) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/measurements/" + locationId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + + // Retrieve the Measurement and assert that it does not exist + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, measurement.getId().getOfficeId()) + .queryParam(Controllers.ID_MASK, measurement.getLocationId()) + .queryParam(Controllers.MIN_NUMBER, number) + .queryParam(Controllers.MAX_NUMBER, number) + .queryParam(Controllers.UNIT_SYSTEM, UnitSystem.EN.getValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/measurements/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + } + + @Test + @MinimumSchema(MINIMUM_SCHEMA) + void test_create_retrieve_delete_measurement_multiple() throws IOException { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/measurements.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(json); + List measurements = Formats.parseContentList(new ContentType(Formats.JSON), json, Measurement.class); + + Measurement measurement1 = measurements.get(0); + Measurement measurement2 = measurements.get(1); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Create the Measurements + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/measurements/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Retrieve the Measurements and assert that they exists + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE_MASK, measurement1.getId().getOfficeId()) + .queryParam(Controllers.ID_MASK, measurement1.getLocationId()) + .queryParam(Controllers.UNIT_SYSTEM, UnitSystem.EN.getValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/measurements/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("[0].height-unit", equalTo(measurement1.getHeightUnit())) + .body("[0].flow-unit", equalTo(measurement1.getFlowUnit())) + .body("[0].temp-unit", equalTo(measurement1.getTempUnit())) + .body("[0].velocity-unit", equalTo(measurement1.getVelocityUnit())) + .body("[0].area-unit", equalTo(measurement1.getAreaUnit())) + .body("[0].used", equalTo(measurement1.isUsed())) + .body("[0].agency", equalTo(measurement1.getAgency())) + .body("[0].party", equalTo(measurement1.getParty())) + .body("[0].wm-comments", equalTo(measurement1.getWmComments())) + .body("[0].instant", equalTo(measurement1.getInstant().toString())) + .body("[0].number", equalTo(measurement1.getNumber())) + .body("[0].id.name", equalTo(measurement1.getLocationId())) + .body("[0].id.office-id", equalTo(measurement1.getOfficeId())) + .body("[0].streamflow-measurement.gage-height", equalTo(measurement1.getStreamflowMeasurement().getGageHeight().floatValue())) + .body("[0].streamflow-measurement.flow", equalTo(measurement1.getStreamflowMeasurement().getFlow().floatValue())) + .body("[0].streamflow-measurement.quality", equalTo(measurement1.getStreamflowMeasurement().getQuality())) + .body("[0].supplemental-streamflow-measurement.channel-flow", equalTo(measurement1.getSupplementalStreamflowMeasurement().getChannelFlow().floatValue())) + .body("[0].supplemental-streamflow-measurement.overbank-flow", equalTo(measurement1.getSupplementalStreamflowMeasurement().getOverbankFlow().floatValue())) + .body("[0].supplemental-streamflow-measurement.overbank-max-depth", equalTo(measurement1.getSupplementalStreamflowMeasurement().getOverbankMaxDepth().floatValue())) + .body("[0].supplemental-streamflow-measurement.channel-max-depth", equalTo(measurement1.getSupplementalStreamflowMeasurement().getChannelMaxDepth().floatValue())) + .body("[0].supplemental-streamflow-measurement.avg-velocity", equalTo(measurement1.getSupplementalStreamflowMeasurement().getAvgVelocity().floatValue())) + .body("[0].supplemental-streamflow-measurement.surface-velocity", equalTo(measurement1.getSupplementalStreamflowMeasurement().getSurfaceVelocity().floatValue())) + .body("[0].supplemental-streamflow-measurement.max-velocity", equalTo(measurement1.getSupplementalStreamflowMeasurement().getMaxVelocity().floatValue())) + .body("[0].supplemental-streamflow-measurement.effective-flow-area", equalTo(measurement1.getSupplementalStreamflowMeasurement().getEffectiveFlowArea().floatValue())) + .body("[0].supplemental-streamflow-measurement.cross-sectional-area", equalTo(measurement1.getSupplementalStreamflowMeasurement().getCrossSectionalArea().floatValue())) + .body("[0].supplemental-streamflow-measurement.mean-gage", equalTo(measurement1.getSupplementalStreamflowMeasurement().getMeanGage().floatValue())) + .body("[0].supplemental-streamflow-measurement.top-width", equalTo(measurement1.getSupplementalStreamflowMeasurement().getTopWidth().floatValue())) + .body("[0].supplemental-streamflow-measurement.main-channel-area", equalTo(measurement1.getSupplementalStreamflowMeasurement().getMainChannelArea().floatValue())) + .body("[0].supplemental-streamflow-measurement.overbank-area", equalTo(measurement1.getSupplementalStreamflowMeasurement().getOverbankArea().floatValue())) + .body("[0].usgs-measurement.remarks", equalTo(measurement1.getUsgsMeasurement().getRemarks())) + .body("[0].usgs-measurement.current-rating", equalTo(measurement1.getUsgsMeasurement().getCurrentRating())) + .body("[0].usgs-measurement.control-condition", equalTo(measurement1.getUsgsMeasurement().getControlCondition())) + .body("[0].usgs-measurement.flow-adjustment", equalTo(measurement1.getUsgsMeasurement().getFlowAdjustment())) + .body("[0].usgs-measurement.shift-used", equalTo(measurement1.getUsgsMeasurement().getShiftUsed().floatValue())) + .body("[0].usgs-measurement.percent-difference", equalTo(measurement1.getUsgsMeasurement().getPercentDifference().floatValue())) + .body("[0].usgs-measurement.delta-height", equalTo(measurement1.getUsgsMeasurement().getDeltaHeight().floatValue())) + .body("[0].usgs-measurement.delta-time", equalTo(measurement1.getUsgsMeasurement().getDeltaTime().floatValue())) + .body("[0].usgs-measurement.air-temp", equalTo(measurement1.getUsgsMeasurement().getAirTemp().floatValue())) + .body("[0].usgs-measurement.water-temp", equalTo(measurement1.getUsgsMeasurement().getWaterTemp().floatValue())) + .body("[1].height-unit", equalTo(measurement2.getHeightUnit())) + .body("[1].flow-unit", equalTo(measurement2.getFlowUnit())) + .body("[1].temp-unit", equalTo(measurement2.getTempUnit())) + .body("[1].velocity-unit", equalTo(measurement2.getVelocityUnit())) + .body("[1].area-unit", equalTo(measurement2.getAreaUnit())) + .body("[1].used", equalTo(measurement2.isUsed())) + .body("[1].agency", equalTo(measurement2.getAgency())) + .body("[1].party", equalTo(measurement2.getParty())) + .body("[1].wm-comments", equalTo(measurement2.getWmComments())) + .body("[1].instant", equalTo(measurement2.getInstant().toString())) + .body("[1].number", equalTo(measurement2.getNumber())) + .body("[1].id.name", equalTo(measurement2.getLocationId())) + .body("[1].id.office-id", equalTo(measurement2.getOfficeId())) + .body("[1].streamflow-measurement.gage-height", equalTo(measurement2.getStreamflowMeasurement().getGageHeight().floatValue())) + .body("[1].streamflow-measurement.flow", equalTo(measurement2.getStreamflowMeasurement().getFlow().floatValue())) + .body("[1].streamflow-measurement.quality", equalTo(measurement2.getStreamflowMeasurement().getQuality())) + .body("[1].supplemental-streamflow-measurement.channel-flow", equalTo(measurement2.getSupplementalStreamflowMeasurement().getChannelFlow().floatValue())) + .body("[1].supplemental-streamflow-measurement.overbank-flow", equalTo(measurement2.getSupplementalStreamflowMeasurement().getOverbankFlow().floatValue())) + .body("[1].supplemental-streamflow-measurement.overbank-max-depth", equalTo(measurement2.getSupplementalStreamflowMeasurement().getOverbankMaxDepth().floatValue())) + .body("[1].supplemental-streamflow-measurement.channel-max-depth", equalTo(measurement2.getSupplementalStreamflowMeasurement().getChannelMaxDepth().floatValue())) + .body("[1].supplemental-streamflow-measurement.avg-velocity", equalTo(measurement2.getSupplementalStreamflowMeasurement().getAvgVelocity().floatValue())) + .body("[1].supplemental-streamflow-measurement.surface-velocity", equalTo(measurement2.getSupplementalStreamflowMeasurement().getSurfaceVelocity().floatValue())) + .body("[1].supplemental-streamflow-measurement.max-velocity", equalTo(measurement2.getSupplementalStreamflowMeasurement().getMaxVelocity().floatValue())) + .body("[1].supplemental-streamflow-measurement.effective-flow-area", equalTo(measurement2.getSupplementalStreamflowMeasurement().getEffectiveFlowArea().floatValue())) + .body("[1].supplemental-streamflow-measurement.cross-sectional-area", equalTo(measurement2.getSupplementalStreamflowMeasurement().getCrossSectionalArea().floatValue())) + .body("[1].supplemental-streamflow-measurement.mean-gage", equalTo(measurement2.getSupplementalStreamflowMeasurement().getMeanGage().floatValue())) + .body("[1].supplemental-streamflow-measurement.top-width", equalTo(measurement2.getSupplementalStreamflowMeasurement().getTopWidth().floatValue())) + .body("[1].supplemental-streamflow-measurement.main-channel-area", equalTo(measurement2.getSupplementalStreamflowMeasurement().getMainChannelArea().floatValue())) + .body("[1].supplemental-streamflow-measurement.overbank-area", equalTo(measurement2.getSupplementalStreamflowMeasurement().getOverbankArea().floatValue())) + .body("[1].usgs-measurement.remarks", equalTo(measurement2.getUsgsMeasurement().getRemarks())) + .body("[1].usgs-measurement.current-rating", equalTo(measurement2.getUsgsMeasurement().getCurrentRating())) + .body("[1].usgs-measurement.control-condition", equalTo(measurement2.getUsgsMeasurement().getControlCondition())) + .body("[1].usgs-measurement.flow-adjustment", equalTo(measurement2.getUsgsMeasurement().getFlowAdjustment())) + .body("[1].usgs-measurement.shift-used", equalTo(measurement2.getUsgsMeasurement().getShiftUsed().floatValue())) + .body("[1].usgs-measurement.percent-difference", equalTo(measurement2.getUsgsMeasurement().getPercentDifference().floatValue())) + .body("[1].usgs-measurement.delta-height", equalTo(measurement2.getUsgsMeasurement().getDeltaHeight().floatValue())) + .body("[1].usgs-measurement.delta-time", equalTo(measurement2.getUsgsMeasurement().getDeltaTime().floatValue())) + .body("[1].usgs-measurement.air-temp", equalTo(measurement2.getUsgsMeasurement().getAirTemp().floatValue())) + .body("[1].usgs-measurement.water-temp", equalTo(measurement2.getUsgsMeasurement().getWaterTemp().floatValue())); + + // Delete the Measurements + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(Controllers.OFFICE, OFFICE_ID) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/measurements/" + measurement1.getLocationId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + + // Retrieve the Measurements and assert that they do not exist + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, OFFICE_ID) + .queryParam(Controllers.ID_MASK, measurement1.getLocationId()) + .queryParam(Controllers.UNIT_SYSTEM, UnitSystem.EN.getValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/measurements/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + } + + @Test + @MinimumSchema(MINIMUM_SCHEMA) + void test_delete_does_not_exist() { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + // Delete a Measurement + given() + .log().ifValidationFails(LogDetail.ALL,true) + .queryParam(Controllers.OFFICE, user.getOperatingOffice()) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("measurements/" + Instant.now().toEpochMilli()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + } + +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/PropertyControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/PropertyControllerIT.java index c4174e4a8..7415f6b19 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/PropertyControllerIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/PropertyControllerIT.java @@ -87,6 +87,7 @@ void test_get_create_delete() throws IOException { .accept(Formats.JSON) .queryParam(Controllers.OFFICE, office) .queryParam(Controllers.CATEGORY_ID, property.getCategory()) + .header(AUTH_HEADER, user.toHeaderValue()) .when() .redirects().follow(true) .redirects().max(3) @@ -125,6 +126,7 @@ void test_get_create_delete() throws IOException { .accept(Formats.JSON) .queryParam(Controllers.OFFICE, office) .queryParam(Controllers.CATEGORY_ID, property.getCategory()) + .header(AUTH_HEADER, user.toHeaderValue()) .when() .redirects().follow(true) .redirects().max(3) @@ -223,6 +225,7 @@ void test_get_all() throws IOException { .queryParam(Controllers.OFFICE_MASK, office) .queryParam(Controllers.CATEGORY_ID_MASK, property.getCategory()) .queryParam(Controllers.NAME_MASK, property.getName()) + .header(AUTH_HEADER, user.toHeaderValue()) .when() .redirects().follow(true) .redirects().max(3) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesGroupControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesGroupControllerTestIT.java index 3e584fea9..3304137a3 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesGroupControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesGroupControllerTestIT.java @@ -24,17 +24,27 @@ package cwms.cda.api; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import cwms.cda.api.errors.NotFoundException; +import cwms.cda.data.dao.TimeSeriesCategoryDao; +import cwms.cda.data.dao.TimeSeriesDaoImpl; +import cwms.cda.data.dao.TimeSeriesGroupDao; +import cwms.cda.data.dto.TimeSeries; import fixtures.CwmsDataApiSetupCallback; import fixtures.TestAccounts; import io.restassured.filter.log.LogDetail; import io.restassured.path.json.JsonPath; import io.restassured.response.Response; import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import org.apache.commons.io.IOUtils; import org.hamcrest.Matchers; import org.jooq.Configuration; +import org.jooq.impl.DSL; import org.jooq.util.oracle.OracleDSL; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -47,11 +57,19 @@ import usace.cwms.db.jooq.codegen.packages.CWMS_UTIL_PACKAGE; import javax.servlet.http.HttpServletResponse; +import java.io.InputStream; import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import static cwms.cda.api.Controllers.*; +import static cwms.cda.data.dao.JooqDao.formatBool; import static io.restassured.RestAssured.given; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; @@ -59,10 +77,13 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; @Tag("integration") -@Disabled() // not clearing groups correctly. class TimeSeriesGroupControllerTestIT extends DataApiTestIT { - public static final String CWMS_OFFICE = "CWMS"; + private List categoriesToCleanup = new ArrayList<>(); + private List groupsToCleanup = new ArrayList<>(); + private List timeSeriesToCleanup = new ArrayList<>(); + private static final Logger LOGGER = Logger.getLogger(TimeSeriesGroupControllerTestIT.class.getName()); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; @BeforeAll public static void load_data() throws Exception { @@ -79,6 +100,53 @@ public static void load_data() throws Exception { loadSqlDataFromResource("cwms/cda/data/sql/spk_aliases_and_groups.sql"); } + @AfterAll + public static void tear_down() throws Exception { + loadSqlDataFromResource("cwms/cda/data/sql/delete_mixed_ts_group.sql"); + loadSqlDataFromResource("cwms/cda/data/sql/delete_spk_aliases_and_groups.sql"); + } + + @AfterEach + public void clear_data() throws Exception { + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection(c -> { + Configuration configuration = DSL.using(c).configuration(); + TimeSeriesGroupDao groupDao = new TimeSeriesGroupDao(configuration.dsl()); + TimeSeriesCategoryDao categoryDao = new TimeSeriesCategoryDao(configuration.dsl()); + TimeSeriesDaoImpl timeSeriesDao = new TimeSeriesDaoImpl(configuration.dsl()); + + for (TimeSeriesGroup group : groupsToCleanup) { + try { + groupDao.unassignAllTs(group, "SPK"); + if (!group.getOfficeId().equalsIgnoreCase(CWMS_OFFICE)) { + groupDao.delete(group.getTimeSeriesCategory().getId(), group.getId(), group.getOfficeId()); + } + } catch (NotFoundException e) { + LOGGER.log(Level.CONFIG, "Group not found", e); + } + } + for (TimeSeriesCategory category : categoriesToCleanup) { + try { + categoryDao.delete(category.getId(), true, category.getOfficeId()); + } catch (NotFoundException e) { + LOGGER.log(Level.CONFIG, "Category not found", e); + } + } + for (TimeSeries ts : timeSeriesToCleanup) { + try { + timeSeriesDao.delete(ts.getOfficeId(), ts.getName(), new TimeSeriesDaoImpl.DeleteOptions.Builder() + .withStartTimeInclusive(true).withEndTimeInclusive(true).withMaxVersion(false) + .withOverrideProtection(formatBool(true)).build()); + } catch (NotFoundException e) { + LOGGER.log(Level.CONFIG, "Time Series not found", e); + } + } + groupsToCleanup.clear(); + categoriesToCleanup.clear(); + timeSeriesToCleanup.clear(); + }, CwmsDataApiSetupCallback.getWebUser()); + } + @Test void test_group_SPK() { @@ -86,7 +154,7 @@ void test_group_SPK() { given() .log().ifValidationFails(LogDetail.ALL,true) .accept("application/json") - .queryParam("office", "SPK") + .queryParam("office", user.getOperatingOffice()) .when() .get("/timeseries/group") .then() @@ -94,8 +162,8 @@ void test_group_SPK() { .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(200)) .body("$.size()", is(1), - "[0].time-series-category.office-id", is("SPK"), - "[0].office-id", is("SPK")) + "[0].time-series-category.office-id", is(user.getOperatingOffice()), + "[0].office-id", is(user.getOperatingOffice())) .extract() .response(); @@ -132,7 +200,7 @@ void test_group_CWMS() { int itemIndex = ids.indexOf(testGroupId); - assertThat(jsonPathEval.get("[" + itemIndex + "].time-series-category.office-id"), Matchers.is("CWMS")); + assertThat(jsonPathEval.get("[" + itemIndex + "].time-series-category.office-id"), Matchers.is(CWMS_OFFICE)); List tsIds = jsonPathEval.get("[" + itemIndex + "].assigned-time-series.timeseries-id"); assertNotNull(tsIds); @@ -149,11 +217,10 @@ void test_group_CWMS() { @Test void test_create_read_delete() throws Exception { - String officeId = "SPK"; + String officeId = user.getOperatingOffice(); String timeSeriesId = "Alder Springs.Precip-Cumulative.Inst.15Minutes.0.raw-cda"; createLocation(timeSeriesId.split("\\.")[0],true,officeId); createTimeseries(officeId,timeSeriesId); - TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; TimeSeriesCategory cat = new TimeSeriesCategory(officeId, "test_create_read_delete", "IntegrationTesting"); TimeSeriesGroup group = new TimeSeriesGroup(cat, officeId, "test_create_read_delete", "IntegrationTesting", "sharedTsAliasId", timeSeriesId); @@ -172,7 +239,7 @@ void test_create_read_delete() throws Exception { .body(categoryXml) .header("Authorization", user.toHeaderValue()) .queryParam(OFFICE, officeId) - .queryParam(FAIL_IF_EXISTS, "false") + .queryParam(FAIL_IF_EXISTS, false) .when() .redirects().follow(true) .redirects().max(3) @@ -188,7 +255,7 @@ void test_create_read_delete() throws Exception { .contentType(Formats.JSON) .body(groupXml) .header("Authorization", user.toHeaderValue()) - .queryParam(FAIL_IF_EXISTS, "false") + .queryParam(FAIL_IF_EXISTS, false) .when() .redirects().follow(true) .redirects().max(3) @@ -260,7 +327,7 @@ void test_create_read_delete() throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .accept(Formats.JSON) .contentType(Formats.JSON) - .queryParam("office", officeId) + .queryParam(OFFICE, officeId) .when() .redirects().follow(true) .redirects().max(3) @@ -288,7 +355,7 @@ void test_create_read_delete() throws Exception { private static BigDecimal getTsCode(String officeId, String timeSeriesId) throws SQLException { CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); - return db.connection((c) -> { + return db.connection(c -> { Configuration configuration = OracleDSL.using(c).configuration(); BigDecimal officeCode = CWMS_UTIL_PACKAGE.call_GET_OFFICE_CODE(configuration, officeId); return CWMS_TS_PACKAGE.call_GET_TS_CODE(configuration, timeSeriesId, officeCode); @@ -297,11 +364,11 @@ private static BigDecimal getTsCode(String officeId, String timeSeriesId) throws @Test void test_rename_group() throws Exception { - String officeId = "SPK"; + String officeId = user.getOperatingOffice(); String timeSeriesId = "Alder Springs.Precip-Cumulative.Inst.15Minutes.0.raw-cda"; createLocation(timeSeriesId.split("\\.")[0],true,officeId); createTimeseries(officeId, timeSeriesId); - TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + TimeSeriesCategory cat = new TimeSeriesCategory(officeId, "test_rename_group_cat", "IntegrationTesting"); TimeSeriesGroup group = new TimeSeriesGroup(cat, officeId, "test_rename_group", "IntegrationTesting", "sharedTsAliasId", timeSeriesId); @@ -356,7 +423,7 @@ void test_rename_group() throws Exception { .body(newGroupXml) .header("Authorization", user.toHeaderValue()) .header(CATEGORY_ID, group.getTimeSeriesCategory().getId()) - .header(OFFICE, group.getOfficeId()) + .queryParam(OFFICE, group.getOfficeId()) .when() .redirects().follow(true) .redirects().max(3) @@ -396,7 +463,7 @@ void test_rename_group() throws Exception { .body(newGroupXml) .header("Authorization", user.toHeaderValue()) .queryParam(CATEGORY_ID, newGroup.getTimeSeriesCategory().getId()) - .queryParam(REPLACE_ASSIGNED_TS, "true") + .queryParam(REPLACE_ASSIGNED_TS, true) .queryParam(OFFICE, newGroup.getOfficeId()) .when() .redirects().follow(true) @@ -441,9 +508,8 @@ void test_rename_group() throws Exception { @Test void test_add_assigned_locs() throws Exception { - String officeId = "SPK"; + String officeId = user.getOperatingOffice(); String timeSeriesId = "Alder Springs.Precip-Cumulative.Inst.15Minutes.0.raw-cda"; - TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; TimeSeriesCategory cat = new TimeSeriesCategory(officeId, "test_add_assigned_locs", "IntegrationTesting"); TimeSeriesGroup group = new TimeSeriesGroup(cat, officeId, "test_add_assigned_locs", "IntegrationTesting", "sharedTsAliasId", timeSeriesId); @@ -461,7 +527,7 @@ void test_add_assigned_locs() throws Exception { .contentType(Formats.JSON) .body(categoryXml) .header("Authorization", user.toHeaderValue()) - .queryParam(FAIL_IF_EXISTS, "false") + .queryParam(FAIL_IF_EXISTS, false) .queryParam(OFFICE, officeId) .when() .redirects().follow(true) @@ -478,7 +544,7 @@ void test_add_assigned_locs() throws Exception { .contentType(Formats.JSON) .body(groupXml) .header("Authorization", user.toHeaderValue()) - .queryParam(FAIL_IF_EXISTS, "false") + .queryParam(FAIL_IF_EXISTS, false) .when() .redirects().follow(true) .redirects().max(3) @@ -500,7 +566,7 @@ void test_add_assigned_locs() throws Exception { .body(groupXml) .header("Authorization", user.toHeaderValue()) .queryParam(CATEGORY_ID, group.getTimeSeriesCategory().getId()) - .queryParam(REPLACE_ASSIGNED_LOCS, "true") + .queryParam(REPLACE_ASSIGNED_LOCS, true) .queryParam(OFFICE, group.getOfficeId()) .when() .redirects().follow(true) @@ -541,7 +607,7 @@ void test_add_assigned_locs() throws Exception { .body(groupXml) .header("Authorization", user.toHeaderValue()) .queryParam(CATEGORY_ID, group.getTimeSeriesCategory().getId()) - .queryParam(REPLACE_ASSIGNED_TS, "true") + .queryParam(REPLACE_ASSIGNED_TS, true) .queryParam(OFFICE, group.getOfficeId()) .when() .redirects().follow(true) @@ -583,4 +649,308 @@ void test_add_assigned_locs() throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); } + + @Test + void test_patch_permissions_CWMS() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + InputStream resource = this.getClass().getResourceAsStream( + "/cwms/cda/api/timeseries_create_SPK.json"); + assertNotNull(resource); + String tsData = IOUtils.toString(resource, StandardCharsets.UTF_8) + .replace("ZACK.Stage.Inst.5Minutes.0.ZSTORE_TS_TEST", "ZACK.Stage.Inst.5Minutes.0.ZSTORE_TS_TEST4"); + + TimeSeries deserialize = Formats.parseContent(new ContentType(Formats.JSON), tsData, TimeSeries.class); + timeSeriesToCleanup.add(deserialize); + + JsonNode ts = mapper.readTree(tsData); + String location = ts.get("name").asText().split("\\.")[0]; + String officeId = ts.get("office-id").asText(); + createLocation(location, true, officeId); + + // inserting the time series + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .body(tsData) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + String categoryName = "Default"; + String groupId = "Default"; + String tsId = ts.get("name").asText(); + TimeSeriesCategory category = new TimeSeriesCategory(CWMS_OFFICE, categoryName, "Default"); + TimeSeriesGroup group = new TimeSeriesGroup(category, CWMS_OFFICE, groupId, "All Time Series", null, null); + AssignedTimeSeries assignedTimeSeries = new AssignedTimeSeries(officeId, tsId, null, null, null, null); + TimeSeriesGroup newGroup = new TimeSeriesGroup(group, Collections.singletonList(assignedTimeSeries)); + + String newGroupJson = Formats.format(new ContentType(Formats.JSONV1), newGroup); + + groupsToCleanup.add(newGroup); + + // Retrieve the group and assert it's empty + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .contentType(Formats.JSONV1) + .queryParam(OFFICE, CWMS_OFFICE) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("description", equalTo("All Time Series")) + .body("assigned-time-series.size()", equalTo(0)); + + // Attempt a patch on TS owned by CWMS + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .contentType(Formats.JSONV1) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .body(newGroupJson) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/timeseries/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + // Retrieve the group and assert the changes + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .contentType(Formats.JSONV1) + .queryParam(OFFICE, CWMS_OFFICE) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("description", equalTo("All Time Series")) + .body("assigned-time-series.size()", equalTo(1)) + .body("assigned-time-series[0].timeseries-id", equalTo(tsId)); + } + + @Test + void test_patch_permissions_CWMS_with_replacement() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + InputStream resource = this.getClass().getResourceAsStream( + "/cwms/cda/api/timeseries_create_SPK.json"); + assertNotNull(resource); + String tsData = IOUtils.toString(resource, StandardCharsets.UTF_8); + String tsData2 = tsData + .replace("ZACK.Stage.Inst.5Minutes.0.ZSTORE_TS_TEST", "ZACK.Stage.Inst.5Minutes.0.ZSTORE_TS_TEST2"); + + TimeSeries deserialize = Formats.parseContent(new ContentType(Formats.JSON), tsData, TimeSeries.class); + TimeSeries deserialize2 = Formats.parseContent(new ContentType(Formats.JSON), tsData2, TimeSeries.class); + timeSeriesToCleanup.add(deserialize2); + timeSeriesToCleanup.add(deserialize); + + JsonNode ts = mapper.readTree(tsData); + JsonNode ts2 = mapper.readTree(tsData2); + String location = ts.get("name").asText().split("\\.")[0]; + String officeId = ts.get("office-id").asText(); + createLocation(location, true, officeId); + + // inserting the time series + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .body(tsData) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + // inserting the time series + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .body(tsData2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + String categoryName = "Default"; + String groupId = "Default"; + String tsId = ts.get("name").asText(); + String tsId2 = ts2.get("name").asText(); + TimeSeriesCategory category = new TimeSeriesCategory(CWMS_OFFICE, categoryName, "Default"); + TimeSeriesGroup group = new TimeSeriesGroup(category, CWMS_OFFICE, groupId, "All Time Series", null, null); + AssignedTimeSeries assignedTimeSeries = new AssignedTimeSeries(officeId, tsId, null, null, null, null); + AssignedTimeSeries assignedTimeSeries2 = new AssignedTimeSeries(officeId, tsId2, null, null, null, null); + TimeSeriesGroup newGroup = new TimeSeriesGroup(group, Arrays.asList(assignedTimeSeries2, assignedTimeSeries)); + + String newGroupJson2 = Formats.format(new ContentType(Formats.JSONV1), newGroup); + + groupsToCleanup.add(newGroup); + + // Attempt a patch on TS owned by CWMS with replacement + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .contentType(Formats.JSONV1) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(REPLACE_ASSIGNED_TS, true) + .body(newGroupJson2) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/timeseries/group/" + newGroup.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + // Retrieve the group and assert the changes + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .contentType(Formats.JSONV1) + .queryParam(OFFICE, CWMS_OFFICE) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + newGroup.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("description", equalTo("All Time Series")) + .body("assigned-time-series.size()", equalTo(2)); + } + + @Test + void test_patch_district_permission() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + InputStream resource = this.getClass().getResourceAsStream( + "/cwms/cda/api/timeseries_create_SPK.json"); + assertNotNull(resource); + String tsData = IOUtils.toString(resource, StandardCharsets.UTF_8) + .replace("ZACK.Stage.Inst.5Minutes.0.ZSTORE_TS_TEST", "ZACK.Stage.Inst.5Minutes.0.ZSTORE_TS_TEST3"); + + TimeSeries deserialize = Formats.parseContent(new ContentType(Formats.JSON), tsData, TimeSeries.class); + timeSeriesToCleanup.add(deserialize); + + JsonNode ts = mapper.readTree(tsData); + String location = ts.get("name").asText().split("\\.")[0]; + String officeId = ts.get("office-id").asText(); + createLocation(location, true, officeId); + String tsId = ts.get("name").asText(); + + TimeSeriesCategory category = new TimeSeriesCategory(CWMS_OFFICE, "Default", "Default"); + TimeSeriesGroup districtGroup = new TimeSeriesGroup(category, CWMS_OFFICE, "Default", "All Time Series", null, null); + AssignedTimeSeries assignedTimeSeries = new AssignedTimeSeries(officeId, tsId, null, null, null, null); + TimeSeriesGroup newDistrictGroup = new TimeSeriesGroup(districtGroup, Collections.singletonList(assignedTimeSeries)); + groupsToCleanup.add(newDistrictGroup); + + String newDistrictGroupJson = Formats.format(new ContentType(Formats.JSONV1), newDistrictGroup); + + // inserting the time series + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .body(tsData) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + // Verify the group is empty + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .contentType(Formats.JSONV1) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, CWMS_OFFICE) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + newDistrictGroup.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("id", equalTo("Default")) + .body("assigned-time-series.size()", equalTo(0)); + + // Attempt a patch on TS Group of assigned TS owned by SPK + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .contentType(Formats.JSONV1) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .body(newDistrictGroupJson) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/timeseries/group/" + districtGroup.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + // Retrieve the group and assert the changes + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .contentType(Formats.JSONV1) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, CWMS_OFFICE) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + newDistrictGroup.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("id", equalTo("Default")) + .body("assigned-time-series.size()", equalTo(1)) + .body("assigned-time-series[0].timeseries-id", equalTo(tsId)); + } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesRecentControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesRecentControllerIT.java new file mode 100644 index 000000000..4de1262df --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesRecentControllerIT.java @@ -0,0 +1,255 @@ +/* + * + * 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.KeyAccessManager.AUTH_HEADER; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import cwms.cda.api.enums.VersionType; +import cwms.cda.api.errors.NotFoundException; +import cwms.cda.data.dao.TimeSeriesCategoryDao; +import cwms.cda.data.dao.TimeSeriesDaoImpl; +import cwms.cda.data.dao.TimeSeriesGroupDao; +import cwms.cda.data.dto.AssignedTimeSeries; +import cwms.cda.data.dto.TimeSeries; +import cwms.cda.data.dto.TimeSeriesCategory; +import cwms.cda.data.dto.TimeSeriesGroup; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.json.JsonV1; +import fixtures.CwmsDataApiSetupCallback; +import fixtures.TestAccounts; +import io.restassured.filter.log.LogDetail; +import javax.servlet.http.HttpServletResponse; +import java.sql.Timestamp; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + + +@Tag("integration") +class TimeSeriesRecentControllerIT extends DataApiTestIT { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + private static final Logger LOGGER = Logger.getLogger(TimeSeriesRecentControllerIT.class.getName()); + private static final String OFFICE_ID = "SPK"; + private static final String LOCATION = "Sacramento River Delta"; + private static final String TS_ID = LOCATION + ".Depth.Inst.15Minutes.0.OBS-Raw"; + private static final String CATEGORY_ID = "USACE Data Acquisition"; + private static final String GROUP_ID = "USACE Include List"; + private static final ZonedDateTime END = ZonedDateTime.now(); + private static final ZonedDateTime START = END.minusHours(1); + private static final ZonedDateTime VERSION_DATE = ZonedDateTime.parse("2024-09-21T09:00:00-07:00[PST8PDT]"); + private static TimeSeriesGroup group; + + @BeforeAll + static void setup() throws Exception { + createLocation(LOCATION, true, OFFICE_ID); + } + + @AfterAll + static void cleanup() throws Exception { + CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); + databaseLink.connection(c -> { + DSLContext ctx = getDslContext(c, OFFICE_ID); + TimeSeriesDaoImpl tsDao = new TimeSeriesDaoImpl(ctx); + TimeSeriesCategoryDao tsCategoryDao = new TimeSeriesCategoryDao(ctx); + TimeSeriesGroupDao tsGroupDao = new TimeSeriesGroupDao(ctx); + try { + tsDao.delete(OFFICE_ID, TS_ID, new TimeSeriesDaoImpl.DeleteOptions.Builder() + .withVersionDate(Date.from(VERSION_DATE.toInstant())).withMaxVersion(false) + .withOverrideProtection("F").withEndTimeInclusive(true).withStartTimeInclusive(true).build()); + } catch (NotFoundException e) { + LOGGER.log(Level.CONFIG, "TimeSeries not found"); + } + try { + tsGroupDao.unassignAllTs(group, OFFICE_ID); + tsGroupDao.delete(CATEGORY_ID, GROUP_ID, OFFICE_ID); + } catch (NotFoundException e) { + LOGGER.log(Level.CONFIG, "Group not found"); + } + try { + tsCategoryDao.delete(CATEGORY_ID, true, OFFICE_ID); + } catch (NotFoundException e) { + LOGGER.log(Level.CONFIG, "Category not found"); + } + }); + } + + @Test + void test_retrieving_recent_ts_data() throws Exception { + TimeSeries ts = buildTimeSeries(OFFICE_ID, TS_ID); + ContentType contentType = Formats.parseHeader(Formats.JSONV2, TimeSeries.class); + String json = Formats.format(contentType, ts); + + // create timeseries + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV2) + .accept(Formats.JSONV2) + .header(AUTH_HEADER, user.toHeaderValue()) + .body(json) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + ; + + TimeSeriesCategory category = new TimeSeriesCategory(OFFICE_ID, CATEGORY_ID, "Data Acquisition category"); + json = JsonV1.buildObjectMapper().writeValueAsString(category); + + // add timeseries to category + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .accept(Formats.JSONV1) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .body(json) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/category") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)) + ; + + group = new TimeSeriesGroup(category, OFFICE_ID, GROUP_ID, "USACE Include group", null, TS_ID); + List tsList = Collections + .singletonList(new AssignedTimeSeries(OFFICE_ID, TS_ID, null, null, TS_ID, 0)); + group = new TimeSeriesGroup(group, tsList); + json = JsonV1.buildObjectMapper().writeValueAsString(group); + + // add timeseries to group + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .accept(Formats.JSONV1) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .body(json) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/group") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)) + ; + + // get recent data using category and group + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .accept(Formats.JSONV1) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(OFFICE, OFFICE_ID) + .queryParam(Controllers.CATEGORY_ID, CATEGORY_ID) + .queryParam(Controllers.GROUP_ID, GROUP_ID) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/recent/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("size()", is(1)) + ; + + // get recent data using timeseries id + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .accept(Formats.JSONV1) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(OFFICE, OFFICE_ID) + .queryParam(TS_IDS, TS_ID) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/recent/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("size()", is(1)) + ; + } + + @NotNull + private TimeSeries buildTimeSeries(String officeId, String tsId) { + long diff = END.toEpochSecond() - START.toEpochSecond(); + assertEquals(3600, diff); // just to make sure I've got the date parsing thing right. + + int minutes = 15; + int count = 60/15 ; // do I need a +1? ie should this be 12 or 13? + // Also, should end be the last point or the next interval? + + TimeSeries ts = new TimeSeries(null, + -1, + 0, + tsId, + officeId, + START, + END, + "m", + Duration.ofMinutes(minutes), + null, + VERSION_DATE, + VersionType.SINGLE_VERSION); + + ZonedDateTime next = START; + for(int i = 0; i < count; i++) { + Timestamp dateTime = Timestamp.from(next.toInstant()); + ts.addValue(dateTime, (double) i, 0); + next = next.plusMinutes(minutes); + } + return ts; + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/TimeseriesControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/TimeseriesControllerTestIT.java index 06a9a0f98..cfd95daa0 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/TimeseriesControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/TimeseriesControllerTestIT.java @@ -9,6 +9,7 @@ import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.isNull; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -89,9 +90,12 @@ void test_lrl_timeseries_psuedo_reg1hour() throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .assertThat() .statusCode(is(HttpServletResponse.SC_OK)) - .body("values[1][1]",closeTo(600.0,0.0001)) - .body("values[0][1]",closeTo(500.0,0.0001)) - + .body("values[0][1]", closeTo(500.0,0.0001)) + .body("values[1][1]", nullValue()) + .body("values[1][2]", is(5)) + .body("values[2][1]", nullValue()) + .body("values[2][2]", is(5)) + .body("values[3][1]", closeTo(600.0,0.0001)) ; } catch (SQLException ex) { throw new RuntimeException("Unable to create location for TS", ex); diff --git a/cwms-data-api/src/test/java/cwms/cda/api/WaterPumpDisassociateControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/WaterPumpDisassociateControllerTestIT.java index bc5c03f3a..b396f1e41 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/WaterPumpDisassociateControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/WaterPumpDisassociateControllerTestIT.java @@ -243,6 +243,7 @@ void test_remove_from_contract() throws Exception { given() .log().ifValidationFails(LogDetail.ALL, true) .accept(Formats.JSONV1) + .header(AUTH_HEADER, user.toHeaderValue()) .when() .redirects().follow(true) .redirects().max(3) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/auth/ApiKeyControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/auth/ApiKeyControllerTestIT.java index ebe72f6f4..d49503cc8 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/auth/ApiKeyControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/auth/ApiKeyControllerTestIT.java @@ -10,6 +10,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; +import cwms.cda.ApiServlet; import cwms.cda.api.DataApiTestIT; import cwms.cda.api.LocationController; import cwms.cda.data.dao.AuthDao; @@ -284,9 +285,32 @@ public void test_key_usage() throws Exception { .statusCode(is(HttpCode.UNAUTHORIZED.getStatus())); } + @Order(6) + @ParameterizedTest + @ArgumentsSource(UserSpecSource.class) + @AuthType(user = TestAccounts.KeyUser.SPK_NORMAL) + public void test_api_key_cannot_create_new_key(String authType, TestAccounts.KeyUser theUser, RequestSpecification authSpec) { + final String KEY_NAME = "KeyFromKey"; + + // This doesn't need to be a user in the database, the check is done before it gets there + final ApiKey key = new ApiKey(theUser.getName(),KEY_NAME,null,null,ZonedDateTime.now()); + + given() + .log().ifValidationFails(LogDetail.ALL,true) + .header("Authorization", "apikey " + realKeys.get(0).getApiKey()) + .contentType("application/json") + .body(key) + .when() + .post("/auth/keys") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpCode.FORBIDDEN.getStatus())) + .body("message",is("Missing roles {Role{name='" + ApiServlet.CAC_USER + "'}}")); + } + // delete api keys // List API keys - @Order(6) + @Order(7) @ParameterizedTest @ArgumentsSource(UserSpecSource.class) @AuthType(user = TestAccounts.KeyUser.SPK_NORMAL) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/location/kind/GateChangeControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/location/kind/GateChangeControllerTestIT.java index d091ff86c..dc1d44f5e 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/location/kind/GateChangeControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/location/kind/GateChangeControllerTestIT.java @@ -35,6 +35,8 @@ import fixtures.CwmsDataApiSetupCallback; import io.restassured.filter.log.LogDetail; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.ZonedDateTime; import java.util.Arrays; @@ -42,16 +44,22 @@ import java.util.List; 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.Tag; import org.junit.jupiter.api.Test; +import usace.cwms.db.jooq.codegen.packages.CWMS_PROJECT_PACKAGE; + import static cwms.cda.api.Controllers.*; import static cwms.cda.data.dao.DaoTest.getDslContext; import static cwms.cda.security.KeyAccessManager.AUTH_HEADER; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNotNull; +@Tag("integration") class GateChangeControllerTestIT extends BaseOutletDaoIT { private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass(); @@ -131,11 +139,8 @@ public static void tearDown() throws Exception { } catch (RuntimeException ex) { LOGGER.atFinest().withCause(ex).log("We don't care about this..."); } - deleteLocation(context, CONDUIT_GATE_1.getOfficeId(), CONDUIT_GATE_1.getName()); - deleteLocation(context, CONDUIT_GATE_2.getOfficeId(), CONDUIT_GATE_2.getName()); deleteLocationGroup(context, CONDUIT_GATE_1_OUTLET); }, CwmsDataApiSetupCallback.getWebUser()); - tearDownProject(); } @Test @@ -269,6 +274,66 @@ void test_changes_crud() { .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); } + @Test + void test_changes_create_from_file() throws Exception { + InputStream is = this.getClass().getResourceAsStream("/cwms/cda/api/gate_change.json"); + assertNotNull(is); + String json = IOUtils.toString(is, StandardCharsets.UTF_8); + List change = Formats.parseContentList(Formats.parseHeader(Formats.JSONV1, GateChange.class), json, GateChange.class); + String office = change.get(0).getProjectId().getOfficeId(); + String project = change.get(0).getProjectId().getName(); + String location = change.get(0).getSettings().stream().findFirst().get().getLocationId().getName(); + Location projectLocation = buildProjectLocation(project); + createLocation(project, true, office, "PROJECT"); + createLocation(project + "-" + location, false, office); + String outletRatingSpecId = project + ".Opening-ConduitGate,Elev;Flow-ConduitGate.Standard.Production"; + CwmsId outletRatingGroup = new CwmsId.Builder().withName( + "Rating-" + project + "-ConduitGate").withOfficeId(office).build(); + Outlet outlet = buildTestOutlet(buildProjectStructureLocation(location, OUTLET_KIND), projectLocation, + outletRatingGroup, outletRatingSpecId); + + CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); + databaseLink.connection(c -> { + DSLContext context = getDslContext(c, office); + OutletDao outletDao = new OutletDao(context); + CWMS_PROJECT_PACKAGE.call_STORE_PROJECT(context.configuration(), buildProject(projectLocation), "F"); + outletDao.storeOutlet(outlet, false); + }, CwmsDataApiSetupCallback.getWebUser()); + + //Create the gate changes + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .body(json) + .header(AUTH_HEADER, USER.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("projects/gate-changes") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .header(AUTH_HEADER, USER.toHeaderValue()) + .queryParam(BEGIN, JAN_FIRST.toString()) + .queryParam(END, JAN_SECOND.toString()) + .queryParam(OVERRIDE_PROTECTION, "true") + .queryParam(FAIL_IF_EXISTS, "false") + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("projects/" + change.get(0).getProjectId().getOfficeId() + + "/" + change.get(0).getProjectId().getName() + "/gate-changes") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } + private boolean isSimilar(GateChange left, GateChange right) { boolean output = false; diff --git a/cwms-data-api/src/test/java/cwms/cda/api/location/kind/OutletControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/location/kind/OutletControllerTestIT.java index b2980001e..8e703bfd4 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/location/kind/OutletControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/location/kind/OutletControllerTestIT.java @@ -20,7 +20,6 @@ package cwms.cda.api.location.kind; -import com.google.common.flogger.FluentLogger; import cwms.cda.api.Controllers; import cwms.cda.data.dao.DeleteRule; import cwms.cda.data.dao.location.kind.BaseOutletDaoIT; @@ -39,6 +38,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import static cwms.cda.api.Controllers.*; import static cwms.cda.data.dao.DaoTest.getDslContext; @@ -46,8 +46,8 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.*; +@Tag("integration") class OutletControllerTestIT extends BaseOutletDaoIT { - private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass(); private static final CwmsId CONDUIT_GATE_RATING_GROUP = new CwmsId.Builder().withName( "Rating-" + PROJECT_1_ID.getName() + "-ConduitGate").withOfficeId(OFFICE_ID).build(); private static final CwmsId MODIFIED_CONDUIT_GATE_RATING_GROUP = new CwmsId.Builder().withName( @@ -126,16 +126,7 @@ public static void tearDown() throws Exception { deleteLocationGroup(context, EXISTING_CONDUIT_GATE_OUTLET); deleteLocationGroup(context, NEW_CONDUIT_GATE_1_OUTLET); deleteLocationGroup(context, NEW_CONDUIT_GATE_2_OUTLET); - deleteLocation(context, NEW_CONDUIT_GATE_1.getOfficeId(), NEW_CONDUIT_GATE_1.getName()); - deleteLocation(context, NEW_CONDUIT_GATE_2.getOfficeId(), NEW_CONDUIT_GATE_2.getName()); - deleteLocation(context, RENAMED_CONDUIT_GATE.getOfficeId(), RENAMED_CONDUIT_GATE.getName()); - deleteLocation(context, EXISTING_CONDUIT_GATE.getOfficeId(), EXISTING_CONDUIT_GATE.getName()); - deleteLocation(context, RATED_OUTLET_LOCATION_CONTROLLED.getOfficeId(), - RATED_OUTLET_LOCATION_CONTROLLED.getName()); - deleteLocation(context, RATED_OUTLET_LOCATION_UNCONTROLLED.getOfficeId(), - RATED_OUTLET_LOCATION_UNCONTROLLED.getName()); }, CwmsDataApiSetupCallback.getWebUser()); - tearDownProject(); } @Disabled("Disabled due to a DB issue. See https://jira.hecdev.net/browse/CWDB-296") diff --git a/cwms-data-api/src/test/java/cwms/cda/api/location/kind/VirtualOutletControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/location/kind/VirtualOutletControllerTestIT.java index 9373edfa2..9b239fca2 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/location/kind/VirtualOutletControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/location/kind/VirtualOutletControllerTestIT.java @@ -40,6 +40,7 @@ import org.jooq.DSLContext; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import static cwms.cda.api.Controllers.FAIL_IF_EXISTS; import static cwms.cda.api.Controllers.METHOD; @@ -48,6 +49,7 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.*; +@Tag("integration") class VirtualOutletControllerTestIT extends ProjectStructureIT { private static final String OUTLET_KIND = "OUTLET"; private static final CwmsId VIRTUAL_OUTLET_RATING_GROUP = new CwmsId.Builder().withName("Rating-" + PROJECT_LOC2.getName() + "-VirtualOutlet") @@ -169,19 +171,7 @@ static void tearDown() throws Exception { outletDao.deleteOutlet(CO3_I2.getOfficeId(), CO3_I2.getName(), DeleteRule.DELETE_ALL); outletDao.deleteOutlet(CO3_I3.getOfficeId(), CO3_I3.getName(), DeleteRule.DELETE_ALL); outletDao.deleteOutlet(CO3_CONDUIT.getOfficeId(), CO3_CONDUIT.getName(), DeleteRule.DELETE_ALL); - - deleteLocation(context, CO1_I25.getOfficeId(), CO1_I25.getName()); - deleteLocation(context, CO1_I53.getOfficeId(), CO1_I53.getName()); - deleteLocation(context, CO1_LOW_FLOW.getOfficeId(), CO1_LOW_FLOW.getName()); - deleteLocation(context, CO2_CONDUIT.getOfficeId(), CO2_CONDUIT.getName()); - deleteLocation(context, CO2_INTAKE.getOfficeId(), CO2_INTAKE.getName()); - deleteLocation(context, CO2_WEIR.getOfficeId(), CO2_WEIR.getName()); - deleteLocation(context, CO3_I1.getOfficeId(), CO3_I1.getName()); - deleteLocation(context, CO3_I2.getOfficeId(), CO3_I2.getName()); - deleteLocation(context, CO3_I3.getOfficeId(), CO3_I3.getName()); - deleteLocation(context, CO3_CONDUIT.getOfficeId(), CO3_CONDUIT.getName()); }, CwmsDataApiSetupCallback.getWebUser()); - tearDownProject(); } @Test diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTest.java new file mode 100644 index 000000000..0007a3b0b --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTest.java @@ -0,0 +1,324 @@ +package cwms.cda.data.dao; + +import cwms.cda.data.dto.CwmsId; +import cwms.cda.data.dto.measurement.Measurement; +import cwms.cda.data.dto.measurement.StreamflowMeasurement; +import cwms.cda.data.dto.measurement.SupplementalStreamflowMeasurement; +import cwms.cda.data.dto.measurement.UsgsMeasurement; +import cwms.cda.helpers.DTOMatch; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.io.IOUtils; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import usace.cwms.db.jooq.codegen.udt.records.LOCATION_REF_T; +import usace.cwms.db.jooq.codegen.udt.records.STREAMFLOW_MEAS2_T; +import usace.cwms.db.jooq.codegen.udt.records.SUPP_STREAMFLOW_MEAS_T; + +final class MeasurementDaoTest { + private static final double DELTA = 0.0001; + @Test + void testFromJooqMeasurementRecord() { + Instant instant = Instant.parse("2024-01-01T00:00:00Z"); + STREAMFLOW_MEAS2_T record = mock(STREAMFLOW_MEAS2_T.class); + when(record.getLOCATION()).thenReturn(mockLocation()); + when(record.getMEAS_NUMBER()).thenReturn("12345"); + when(record.getAGENCY_ID()).thenReturn("USGS"); + when(record.getPARTY()).thenReturn("SomeParty"); + when(record.getDATE_TIME()).thenReturn(Timestamp.from(instant)); + when(record.getFLOW()).thenReturn(100.0); + when(record.getGAGE_HEIGHT()).thenReturn(2.0); + when(record.getQUALITY()).thenReturn("good"); + when(record.getWM_COMMENTS()).thenReturn("Test comment"); + when(record.getAREA_UNIT()).thenReturn("ft2"); + when(record.getFLOW_UNIT()).thenReturn("cfs"); + when(record.getHEIGHT_UNIT()).thenReturn("ft"); + when(record.getVELOCITY_UNIT()).thenReturn("fps"); + when(record.getTEMP_UNIT()).thenReturn("F"); + when(record.getUSED()).thenReturn(JooqDao.formatBool(true)); + when(record.getAIR_TEMP()).thenReturn(25.0); + when(record.getCUR_RATING_NUM()).thenReturn("1"); + when(record.getCTRL_COND_ID()).thenReturn("UNSPECIFIED"); + when(record.getFLOW_ADJ_ID()).thenReturn("UNKNOWN"); + when(record.getDELTA_HEIGHT()).thenReturn(0.5); + when(record.getDELTA_TIME()).thenReturn(60.0); + when(record.getPCT_DIFF()).thenReturn(10.0); + when(record.getREMARKS()).thenReturn("Some remarks"); + when(record.getSHIFT_USED()).thenReturn(11.0); + when(record.getWATER_TEMP()).thenReturn(15.0); + when(record.getSUPP_STREAMFLOW_MEAS()).thenReturn(mockSupplementalStreamflowMeasurement()); + + Measurement measurement = MeasurementDao.fromJooqMeasurementRecord(record); + + assertNotNull(measurement); + assertEquals("12345", measurement.getNumber()); + assertEquals("USGS", measurement.getAgency()); + assertEquals("SomeParty", measurement.getParty()); + assertEquals(instant, measurement.getInstant()); + assertEquals("Test comment", measurement.getWmComments()); + assertEquals("ft2", measurement.getAreaUnit()); + assertEquals("cfs", measurement.getFlowUnit()); + assertEquals("ft", measurement.getHeightUnit()); + assertEquals("fps", measurement.getVelocityUnit()); + assertEquals("F", measurement.getTempUnit()); + assertTrue(measurement.isUsed()); + + //Assertions for Streamflow meas fields + assertEquals(100.0, measurement.getStreamflowMeasurement().getFlow(), DELTA); + assertEquals(2.0, measurement.getStreamflowMeasurement().getGageHeight(), DELTA); + assertEquals("good", measurement.getStreamflowMeasurement().getQuality()); + + // Assertions for UsgsMeasurement fields + assertEquals(25.0, measurement.getUsgsMeasurement().getAirTemp(), DELTA); + assertEquals("1", measurement.getUsgsMeasurement().getCurrentRating()); + assertEquals("UNSPECIFIED", measurement.getUsgsMeasurement().getControlCondition()); + assertEquals("UNKNOWN", measurement.getUsgsMeasurement().getFlowAdjustment()); + assertEquals(0.5, measurement.getUsgsMeasurement().getDeltaHeight(), DELTA); + assertEquals(60.0, measurement.getUsgsMeasurement().getDeltaTime()); + assertEquals(10.0, measurement.getUsgsMeasurement().getPercentDifference(), DELTA); + assertEquals("Some remarks", measurement.getUsgsMeasurement().getRemarks()); + assertEquals(11.0, measurement.getUsgsMeasurement().getShiftUsed(), DELTA); + assertEquals(15.0, measurement.getUsgsMeasurement().getWaterTemp(), DELTA); + + // Assertions for SupplementalStreamflowMeasurement fields + assertNotNull(measurement.getSupplementalStreamflowMeasurement()); + assertEquals(1.5, measurement.getSupplementalStreamflowMeasurement().getAvgVelocity(), DELTA); + assertEquals(100.0, measurement.getSupplementalStreamflowMeasurement().getChannelFlow(), DELTA); + assertEquals(3.0, measurement.getSupplementalStreamflowMeasurement().getMeanGage(), DELTA); + assertEquals(2.0, measurement.getSupplementalStreamflowMeasurement().getMaxVelocity(), DELTA); + assertEquals(50.0, measurement.getSupplementalStreamflowMeasurement().getOverbankFlow(), DELTA); + assertEquals(200.0, measurement.getSupplementalStreamflowMeasurement().getOverbankArea(), DELTA); + assertEquals(10.0, measurement.getSupplementalStreamflowMeasurement().getTopWidth(), DELTA); + assertEquals(1.0, measurement.getSupplementalStreamflowMeasurement().getSurfaceVelocity(), DELTA); + assertEquals(5.0, measurement.getSupplementalStreamflowMeasurement().getChannelMaxDepth(), DELTA); + assertEquals(150.0, measurement.getSupplementalStreamflowMeasurement().getMainChannelArea(), DELTA); + assertEquals(2.0, measurement.getSupplementalStreamflowMeasurement().getOverbankMaxDepth(), DELTA); + assertEquals(75.0, measurement.getSupplementalStreamflowMeasurement().getEffectiveFlowArea(), DELTA); + assertEquals(60.0, measurement.getSupplementalStreamflowMeasurement().getCrossSectionalArea(), DELTA); + } + + @Test + void testConvertToXmlMeasurementDto() + { + Measurement meas = buildTestMeasurement(); + + MeasurementDao.MeasurementXmlDto xmlDto = MeasurementDao.convertMeasurementToXmlDto(meas); + assertMatch(meas, xmlDto); + } + + @Test + void testConvertToMeasurementsXmlDto() { + Measurement meas1 = buildTestMeasurement(); + Measurement meas2 = buildMeasurement2(); + List measurements = new ArrayList<>(); + measurements.add(meas1); + measurements.add(meas2); + MeasurementDao.MeasurementsXmlDto xmlDto = MeasurementDao.convertMeasurementsToXmlDto(measurements); + assertEquals(2, xmlDto.getMeasurements().size()); + assertMatch(meas1, xmlDto.getMeasurements().get(0)); + assertMatch(meas2, xmlDto.getMeasurements().get(1)); + } + + @Test + void testToDbXmlMultiple() throws Exception { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dao/dbMeasurements.xml"); + assertNotNull(resource); + String expectedXml = IOUtils.toString(resource, StandardCharsets.UTF_8); + MeasurementDao.MeasurementsXmlDto expectedXmlDto = MeasurementDao.XML_MAPPER.readValue(expectedXml, MeasurementDao.MeasurementsXmlDto.class); + + Measurement meas = buildTestMeasurement(); + Measurement meas2 = buildMeasurement2(); + List measurements = new ArrayList<>(); + measurements.add(meas); + measurements.add(meas2); + String xml = MeasurementDao.toDbXml(measurements); + assertNotNull(xml); + assertFalse(xml.isEmpty()); + MeasurementDao.MeasurementsXmlDto actualXmlDto = MeasurementDao.XML_MAPPER.readValue(xml, MeasurementDao.MeasurementsXmlDto.class); + + assertMatch(expectedXmlDto, actualXmlDto); + } + + private Measurement buildTestMeasurement() { + return new Measurement.Builder() + .withNumber("12345") + .withAgency("USGS") + .withParty("SomeParty") + .withInstant(Instant.parse("2024-01-01T00:00:00Z")) + .withWmComments("Test comment") + .withAreaUnit("ft2") + .withFlowUnit("cfs") + .withHeightUnit("ft") + .withVelocityUnit("fps") + .withTempUnit("F") + .withUsed(true) + .withId(new CwmsId.Builder() + .withName("Walnut_Ck") + .withOfficeId("SPK") + .build()) + .withStreamflowMeasurement(new StreamflowMeasurement.Builder() + .withFlow(100.0) + .withGageHeight(2.0) + .withQuality("good") + .build()) + .withUsgsMeasurement(new UsgsMeasurement.Builder() + .withAirTemp(25.0) + .withCurrentRating("1") + .withControlCondition("UNSPECIFIED") + .withFlowAdjustment("UNKNOWN") + .withDeltaHeight(0.5) + .withDeltaTime(60.0) + .withPercentDifference(10.0) + .withRemarks("Some remarks") + .withShiftUsed(11.0) + .withWaterTemp(15.0) + .build()) + .withSupplementalStreamflowMeasurement(new SupplementalStreamflowMeasurement.Builder() + .withAvgVelocity(1.5) + .withChannelFlow(100.0) + .withMeanGage(3.0) + .withMaxVelocity(2.0) + .withOverbankFlow(50.0) + .withOverbankArea(200.0) + .withTopWidth(10.0) + .withSurfaceVelocity(1.0) + .withChannelMaxDepth(5.0) + .withMainChannelArea(150.0) + .withOverbankMaxDepth(2.0) + .withEffectiveFlowArea(75.0) + .withCrossSectionalArea(60.0) + .build()) + .build(); + } + + private Measurement buildMeasurement2() { + return new Measurement.Builder() + .withNumber("123456") + .withAgency("USGS") + .withParty("SomeParty2") + .withInstant(Instant.parse("2024-02-01T00:00:00Z")) + .withWmComments("Test comment2") + .withAreaUnit("ft2") + .withFlowUnit("cfs") + .withHeightUnit("ft") + .withVelocityUnit("fps") + .withTempUnit("F") + .withUsed(true) + .withId(new CwmsId.Builder() + .withName("Walnut_Ck") + .withOfficeId("SPK") + .build()) + .withStreamflowMeasurement(new StreamflowMeasurement.Builder() + .withFlow(200.0) + .withGageHeight(2.4) + .withQuality("G") + .build()) + .withUsgsMeasurement(new UsgsMeasurement.Builder() + .withAirTemp(35.0) + .withCurrentRating("2") + .withControlCondition("UNSPECIFIED") + .withFlowAdjustment("OTHR") + .withDeltaHeight(0.6) + .withDeltaTime(70.0) + .withPercentDifference(11.0) + .withRemarks("Some remarks2") + .withShiftUsed(12.0) + .withWaterTemp(16.0) + .build()) + .withSupplementalStreamflowMeasurement(new SupplementalStreamflowMeasurement.Builder() + .withAvgVelocity(1.6) + .withChannelFlow(101.0) + .withMeanGage(3.1) + .withMaxVelocity(2.1) + .withOverbankFlow(51.0) + .withOverbankArea(200.1) + .withTopWidth(10.1) + .withSurfaceVelocity(1.1) + .withChannelMaxDepth(5.1) + .withMainChannelArea(150.1) + .withOverbankMaxDepth(2.1) + .withEffectiveFlowArea(75.1) + .withCrossSectionalArea(60.1) + .build()) + .build(); + } + + private LOCATION_REF_T mockLocation() { + LOCATION_REF_T location = new LOCATION_REF_T(); + location.setBASE_LOCATION_ID("Walnut_Ck"); + location.setSUB_LOCATION_ID(null); + location.setOFFICE_ID("SPK"); + return location; + } + + private SUPP_STREAMFLOW_MEAS_T mockSupplementalStreamflowMeasurement() { + SUPP_STREAMFLOW_MEAS_T supplemental = new SUPP_STREAMFLOW_MEAS_T(); + supplemental.setAVG_VELOCITY(1.5); + supplemental.setCHANNEL_FLOW(100.0); + supplemental.setMEAN_GAGE(3.0); + supplemental.setMAX_VELOCITY(2.0); + supplemental.setOVERBANK_FLOW(50.0); + supplemental.setOVERBANK_AREA(200.0); + supplemental.setTOP_WIDTH(10.0); + supplemental.setSURFACE_VELOCITY(1.0); + supplemental.setCHANNEL_MAX_DEPTH(5.0); + supplemental.setMAIN_CHANNEL_AREA(150.0); + supplemental.setOVERBANK_MAX_DEPTH(2.0); + supplemental.setEFFECTIVE_FLOW_AREA(75.0); + supplemental.setCROSS_SECTIONAL_AREA(60.0); + return supplemental; + } + + private static void assertMatch(Measurement meas, MeasurementDao.MeasurementXmlDto xmlDto) { + assertEquals(meas.getNumber(), xmlDto.getNumber()); + assertEquals(meas.getAgency(), xmlDto.getAgency()); + assertEquals(meas.getParty(), xmlDto.getParty()); + assertEquals(meas.getInstant(), xmlDto.getInstant()); + assertEquals(meas.getWmComments(), xmlDto.getWmComments()); + assertEquals(meas.getAreaUnit(), xmlDto.getAreaUnit()); + assertEquals(meas.getFlowUnit(), xmlDto.getFlowUnit()); + assertEquals(meas.getHeightUnit(), xmlDto.getHeightUnit()); + assertEquals(meas.getVelocityUnit(), xmlDto.getVelocityUnit()); + assertEquals(meas.getTempUnit(), xmlDto.getTempUnit()); + assertEquals(meas.isUsed(), xmlDto.isUsed()); + assertEquals(meas.getLocationId(), xmlDto.getLocationId()); + assertEquals(meas.getOfficeId(), xmlDto.getOfficeId()); + DTOMatch.assertMatch(meas.getStreamflowMeasurement(), xmlDto.getStreamflowMeasurement()); + DTOMatch.assertMatch(meas.getUsgsMeasurement(), xmlDto.getUsgsMeasurement()); + DTOMatch.assertMatch(meas.getSupplementalStreamflowMeasurement(), xmlDto.getSupplementalStreamflowMeasurement()); + } + + private static void assertMatch(MeasurementDao.MeasurementXmlDto expectedXmlDto, MeasurementDao.MeasurementXmlDto actualXmlDto) { + assertEquals(expectedXmlDto.getAgency(), actualXmlDto.getAgency()); + assertEquals(expectedXmlDto.getAreaUnit(), actualXmlDto.getAreaUnit()); + assertEquals(expectedXmlDto.getFlowUnit(), actualXmlDto.getFlowUnit()); + assertEquals(expectedXmlDto.getHeightUnit(), actualXmlDto.getHeightUnit()); + assertEquals(expectedXmlDto.getTempUnit(), actualXmlDto.getTempUnit()); + assertEquals(expectedXmlDto.getInstant(), actualXmlDto.getInstant()); + assertEquals(expectedXmlDto.getNumber(), actualXmlDto.getNumber()); + assertEquals(expectedXmlDto.getParty(), actualXmlDto.getParty()); + assertEquals(expectedXmlDto.getVelocityUnit(), actualXmlDto.getVelocityUnit()); + assertEquals(expectedXmlDto.getWmComments(), actualXmlDto.getWmComments()); + assertEquals(expectedXmlDto.getLocationId(), actualXmlDto.getLocationId()); + assertEquals(expectedXmlDto.getOfficeId(), actualXmlDto.getOfficeId()); + DTOMatch.assertMatch(expectedXmlDto.getStreamflowMeasurement(), actualXmlDto.getStreamflowMeasurement()); + DTOMatch.assertMatch(expectedXmlDto.getUsgsMeasurement(), actualXmlDto.getUsgsMeasurement()); + DTOMatch.assertMatch(expectedXmlDto.getSupplementalStreamflowMeasurement(), actualXmlDto.getSupplementalStreamflowMeasurement()); + } + + private static void assertMatch(MeasurementDao.MeasurementsXmlDto expectedXmlDto, MeasurementDao.MeasurementsXmlDto actualXmlDto) { + assertEquals(expectedXmlDto.getMeasurements().size(), actualXmlDto.getMeasurements().size()); + for (int i = 0; i < expectedXmlDto.getMeasurements().size(); i++) { + assertMatch(expectedXmlDto.getMeasurements().get(i), actualXmlDto.getMeasurements().get(i)); + } + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTestIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTestIT.java new file mode 100644 index 000000000..30331c1c7 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTestIT.java @@ -0,0 +1,344 @@ +package cwms.cda.data.dao; + +import cwms.cda.api.DataApiTestIT; +import cwms.cda.api.enums.UnitSystem; +import cwms.cda.api.errors.NotFoundException; +import static cwms.cda.data.dao.DaoTest.getDslContext; +import cwms.cda.data.dto.CwmsId; +import cwms.cda.data.dto.measurement.Measurement; +import cwms.cda.data.dto.measurement.StreamflowMeasurement; +import cwms.cda.data.dto.measurement.SupplementalStreamflowMeasurement; +import cwms.cda.data.dto.measurement.UsgsMeasurement; +import cwms.cda.data.dto.stream.Bank; +import cwms.cda.data.dto.stream.Stream; +import cwms.cda.data.dto.stream.StreamLocation; +import cwms.cda.helpers.DTOMatch; +import fixtures.CwmsDataApiSetupCallback; +import fixtures.MinimumSchema; +import fixtures.TestAccounts; +import java.sql.SQLException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import org.jooq.DSLContext; +import org.junit.jupiter.api.AfterAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("integration") +public final class MeasurementDaoTestIT extends DataApiTestIT { + + private static final String OFFICE_ID = TestAccounts.KeyUser.SPK_NORMAL.getOperatingOffice(); + private static final List STREAM_LOC_IDS = new ArrayList<>(); + private static final List STREAMS_CREATED = new ArrayList<>(); + public static final int MINIMUM_SCHEMA = 999999; + + @BeforeAll + public static void setup() { + for (int i = 0; i < 2; i++) { + String testLoc = "STREAM_LOC" + i; + STREAM_LOC_IDS.add(testLoc); + try { + createLocation(testLoc, true, OFFICE_ID, "STREAM_LOCATION"); + } catch (Exception e) { + //ignore if already exists + } + try { + StreamLocationDaoTestIT.createAndStoreTestStream("TEST_STREAM_123", OFFICE_ID); + } catch (Exception e) { + //ignore if already exists + } + } + } + + @AfterAll + public static void tearDown() { + for (Stream stream : STREAMS_CREATED) { + try { + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + String webUser = CwmsDataApiSetupCallback.getWebUser(); + db.connection(c -> { + try { + StreamDao streamDao = new StreamDao(getDslContext(c, OFFICE_ID)); + streamDao.deleteStream(stream.getId().getOfficeId(), stream.getId().getName(), DeleteRule.DELETE_ALL); + } catch (Exception e) { + //ignore + } + }, webUser); + } catch (SQLException ex) { + throw new RuntimeException(ex); + } + } + STREAMS_CREATED.clear(); + STREAM_LOC_IDS.clear(); + } + + @Test + @MinimumSchema(MINIMUM_SCHEMA) + void testRoundTripStore() throws Exception { + CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); + String webUser = CwmsDataApiSetupCallback.getWebUser(); + databaseLink.connection(c -> { + DSLContext context = getDslContext(c, databaseLink.getOfficeId()); + StreamLocationDao streamLocationDao = new StreamLocationDao(context); + //build stream locations + String streamLocId = STREAM_LOC_IDS.get(0); + StreamLocation streamLocation = StreamLocationDaoTestIT.buildTestStreamLocation("TEST_STREAM_123", streamLocId, OFFICE_ID,10.0, Bank.LEFT); + String streamLocId2 = STREAM_LOC_IDS.get(1); + StreamLocation streamLocation2 = StreamLocationDaoTestIT.buildTestStreamLocation("TEST_STREAM_123", streamLocId2, OFFICE_ID,11.0, Bank.RIGHT); + + try { + //store stream locations + streamLocationDao.storeStreamLocation(streamLocation, false); + streamLocationDao.storeStreamLocation(streamLocation2, false); + + Measurement meas1 = buildMeasurement1(streamLocId); + Measurement meas1B = buildMeasurement2(streamLocId); + + Measurement meas2 = buildMeasurement1(streamLocId2); + + MeasurementDao measurementDao = new MeasurementDao(context); + List measurements = new ArrayList<>(); + measurements.add(meas1); + measurements.add(meas1B); + measurements.add(meas2); + measurementDao.storeMeasurements(measurements, false); + + List retrievedMeasurements = measurementDao.retrieveMeasurements(OFFICE_ID, streamLocId, null, null, UnitSystem.EN.getValue(), + null, null, null, null, null, null, null, null); + assertEquals(2, retrievedMeasurements.size()); + + DTOMatch.assertMatch(meas1, retrievedMeasurements.get(0)); + DTOMatch.assertMatch(meas1B, retrievedMeasurements.get(1)); + + List measurementsAll = measurementDao.retrieveMeasurements(OFFICE_ID, null, null, null, UnitSystem.EN.getValue(), + null, null, null, null, null, null, null, null); + List meas1List = measurementsAll.stream() + .filter(m -> m.getLocationId().equals(streamLocId)) + .collect(Collectors.toList()); + assertEquals(2, meas1List.size()); + DTOMatch.assertMatch(meas1, meas1List.get(0)); + DTOMatch.assertMatch(meas1B, meas1List.get(1)); + + Measurement meas2Found = measurementsAll.stream() + .filter(m -> m.getLocationId().equals(streamLocId2)) + .findFirst() + .orElse(null); + assertNotNull(meas2Found); + DTOMatch.assertMatch(meas2, meas2Found); + + retrievedMeasurements = measurementDao.retrieveMeasurements(OFFICE_ID, streamLocId, null, null, UnitSystem.EN.getValue(), + null, null, null, null, null, null, null, null); + + Measurement finalMeas1 = meas1; + Measurement finalMeas1B = meas1B; + Measurement retrievedMeas1 = retrievedMeasurements.stream() + .filter(m -> m.getNumber().equals(finalMeas1.getNumber())) + .findFirst() + .orElse(null); + assertNotNull(retrievedMeas1); + DTOMatch.assertMatch(meas1, retrievedMeas1); + Measurement retrievedMeas1B = retrievedMeasurements.stream() + .filter(m -> m.getNumber().equals(finalMeas1B.getNumber())) + .findFirst() + .orElse(null); + assertNotNull(retrievedMeas1B); + DTOMatch.assertMatch(meas1B, retrievedMeas1B); + + //delete measurements + measurementDao.deleteMeasurements(meas1.getId().getOfficeId(), meas1.getId().getName(), null, null, null, null); + measurementDao.deleteMeasurements(meas2.getId().getOfficeId(), meas2.getId().getName(), null, null, null, null); + + final Measurement meas1F = meas1; + final Measurement meas2F = meas2; + assertThrows(NotFoundException.class, () -> measurementDao.retrieveMeasurements(meas1F.getId().getOfficeId(), meas1F.getId().getName(), + null, null, UnitSystem.EN.getValue(), null, null, null, null, null, null, null, null)); + assertThrows(NotFoundException.class, () -> measurementDao.retrieveMeasurements(meas2F.getId().getOfficeId(), meas2F.getId().getName(), + null, null, UnitSystem.EN.getValue(), null, null, null, null, null, null, null, null)); + } finally { + //delete stream locations + streamLocationDao.deleteStreamLocation( + streamLocation.getStreamLocationNode().getId().getOfficeId(), + streamLocation.getStreamLocationNode().getStreamNode().getStreamId().getName(), + streamLocation.getStreamLocationNode().getId().getName() + ); + streamLocationDao.deleteStreamLocation( + streamLocation2.getStreamLocationNode().getId().getOfficeId(), + streamLocation2.getStreamLocationNode().getStreamNode().getStreamId().getName(), + streamLocation2.getStreamLocationNode().getId().getName() + ); + } + }, webUser); + } + + private Measurement buildMeasurement1(String streamLocId) { + return buildMeasurement1(streamLocId, 100.0); + } + + private Measurement buildMeasurement1(String streamLocId, double flow) { + return new Measurement.Builder() + .withNumber("12345") + .withAgency("USGS") + .withParty("SomeParty") + .withInstant(Instant.parse("2024-01-01T00:00:00Z")) + .withWmComments("Test comment") + .withAreaUnit("ft2") + .withFlowUnit("cfs") + .withHeightUnit("ft") + .withVelocityUnit("fps") + .withTempUnit("F") + .withUsed(true) + .withId(new CwmsId.Builder() + .withName(streamLocId) + .withOfficeId(OFFICE_ID) + .build()) + .withStreamflowMeasurement(new StreamflowMeasurement.Builder() + .withFlow(flow) + .withGageHeight(2.0) + .withQuality("G") + .build()) + .withUsgsMeasurement(new UsgsMeasurement.Builder() + .withAirTemp(11.0) + .withCurrentRating("1") + .withControlCondition("FILL") + .withFlowAdjustment("OTHR") + .withDeltaHeight(0.5) + .withDeltaTime(60.0) + .withPercentDifference(10.0) + .withRemarks("Some remarks") + .withShiftUsed(11.0) + .withWaterTemp(15.0) + .build()) + .withSupplementalStreamflowMeasurement(new SupplementalStreamflowMeasurement.Builder() + .withAvgVelocity(1.5) + .withChannelFlow(100.0) + .withMeanGage(3.0) + .withMaxVelocity(2.0) + .withOverbankFlow(50.0) + .withOverbankArea(200.0) + .withTopWidth(10.0) + .withSurfaceVelocity(1.0) + .withChannelMaxDepth(5.0) + .withMainChannelArea(150.0) + .withOverbankMaxDepth(2.0) + .withEffectiveFlowArea(75.0) + .withCrossSectionalArea(60.0) + .build()) + .build(); + } + + private Measurement buildMeasurement2(String streamLocId) { + return buildMeasurement2(streamLocId, 200.0); + } + + private Measurement buildMeasurement2(String streamLocId, double flow) { + //same as buildMeasurement but with different values (same office) + return new Measurement.Builder() + .withNumber("54321") + .withAgency("USGS") + .withParty("SomeParty2") + .withInstant(Instant.parse("2024-02-01T00:00:00Z")) + .withWmComments("Test comment2") + .withAreaUnit("ft2") + .withFlowUnit("cfs") + .withHeightUnit("ft") + .withVelocityUnit("fps") + .withTempUnit("F") + .withUsed(true) + .withId(new CwmsId.Builder() + .withName(streamLocId) + .withOfficeId(OFFICE_ID) + .build()) + .withStreamflowMeasurement(new StreamflowMeasurement.Builder() + .withFlow(flow) + .withGageHeight(4.0) + .withQuality("G") + .build()) + .withUsgsMeasurement(new UsgsMeasurement.Builder() + .withAirTemp(26.0) + .withCurrentRating("2") + .withControlCondition("FILL") + .withFlowAdjustment("OTHR") + .withDeltaHeight(0.6) + .withDeltaTime(61.0) + .withPercentDifference(11.0) + .withRemarks("Some remarks") + .withShiftUsed(12.0) + .withWaterTemp(16.0) + .build()) + .withSupplementalStreamflowMeasurement(new SupplementalStreamflowMeasurement.Builder() + .withAvgVelocity(1.6) + .withChannelFlow(101.0) + .withMeanGage(3.1) + .withMaxVelocity(2.1) + .withOverbankFlow(50.1) + .withOverbankArea(201.0) + .withTopWidth(11.0) + .withSurfaceVelocity(1.1) + .withChannelMaxDepth(5.1) + .withMainChannelArea(150.1) + .withOverbankMaxDepth(2.1) + .withEffectiveFlowArea(75.1) + .withCrossSectionalArea(60.1) + .build()) + .build(); + } + + private Measurement buildMeasurementDoesntExist(String streamLocId) { + return new Measurement.Builder() + .withNumber("0981273") + .withAgency("USGS") + .withParty("SomeParty") + .withInstant(Instant.parse("2024-01-01T00:00:00Z")) + .withWmComments("Test comment") + .withAreaUnit("ft2") + .withFlowUnit("cfs") + .withHeightUnit("ft") + .withVelocityUnit("fps") + .withTempUnit("F") + .withUsed(true) + .withId(new CwmsId.Builder() + .withName(streamLocId) + .withOfficeId(OFFICE_ID) + .build()) + .withStreamflowMeasurement(new StreamflowMeasurement.Builder() + .withFlow(100.0) + .withGageHeight(2.0) + .withQuality("G") + .build()) + .withUsgsMeasurement(new UsgsMeasurement.Builder() + .withAirTemp(25.0) + .withCurrentRating("1") + .withControlCondition("FILL") + .withFlowAdjustment("OTHR") + .withDeltaHeight(0.5) + .withDeltaTime(60.0) + .withPercentDifference(10.0) + .withRemarks("Some remarks") + .withShiftUsed(11.0) + .withWaterTemp(15.0) + .build()) + .withSupplementalStreamflowMeasurement(new SupplementalStreamflowMeasurement.Builder() + .withAvgVelocity(1.5) + .withChannelFlow(100.0) + .withMeanGage(3.0) + .withMaxVelocity(2.0) + .withOverbankFlow(50.0) + .withOverbankArea(200.0) + .withTopWidth(10.0) + .withSurfaceVelocity(1.0) + .withChannelMaxDepth(5.0) + .withMainChannelArea(150.0) + .withOverbankMaxDepth(2.0) + .withEffectiveFlowArea(75.0) + .withCrossSectionalArea(60.0) + .build()) + .build(); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/StreamDaoTestIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/StreamDaoTestIT.java index 198fdf9c4..f3d70725e 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/StreamDaoTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/StreamDaoTestIT.java @@ -152,7 +152,7 @@ void testRename() throws Exception { }, CwmsDataApiSetupCallback.getWebUser()); } - private static Stream buildTestStream(String streamId, StreamNode flowsIntoNode, StreamNode divertsFromNode) { + static Stream buildTestStream(String streamId, StreamNode flowsIntoNode, StreamNode divertsFromNode) { return new Stream.Builder() .withStartsDownstream(true) .withId(new CwmsId.Builder() @@ -169,7 +169,7 @@ private static Stream buildTestStream(String streamId, StreamNode flowsIntoNode, .build(); } - private static StreamNode buildStreamNode(String streamId, double station, Bank bank) { + static StreamNode buildStreamNode(String streamId, double station, Bank bank) { return new StreamNode.Builder() .withStreamId(new CwmsId.Builder() .withName(streamId) diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/StreamLocationDaoTestIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/StreamLocationDaoTestIT.java index 8a452b774..100c17549 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/StreamLocationDaoTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/StreamLocationDaoTestIT.java @@ -67,22 +67,22 @@ public static void setup() throws SQLException { //ignore if already exists } try { - createAndStoreTestStream("TEST_STREAM_123"); + createAndStoreTestStream("TEST_STREAM_123", OFFICE_ID); } catch (Exception e) { //ignore if already exists } } } - private static void createAndStoreTestStream(String testLoc) throws SQLException { - createLocation(testLoc, true, OFFICE_ID, "STREAM"); + static void createAndStoreTestStream(String testLoc, String officeId) throws SQLException { + createLocation(testLoc, true, officeId, "STREAM"); CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); String webUser = CwmsDataApiSetupCallback.getWebUser(); db.connection(c-> { - StreamDao streamDao = new StreamDao(getDslContext(c, OFFICE_ID)); + StreamDao streamDao = new StreamDao(getDslContext(c, officeId)); Stream streamToStore = new Stream.Builder() .withId(new CwmsId.Builder() - .withOfficeId(OFFICE_ID) + .withOfficeId(officeId) .withName(testLoc) .build()) .withLength(100.0) @@ -126,9 +126,9 @@ void testRoundTrip() throws Exception { StreamLocationDao streamLocationDao = new StreamLocationDao(context); //build stream locations String streamLocId = STREAM_LOC_IDS.get(0); - StreamLocation streamLocation = buildTestStreamLocation("TEST_STREAM_123", streamLocId, 10.0, Bank.LEFT); + StreamLocation streamLocation = buildTestStreamLocation("TEST_STREAM_123", streamLocId, OFFICE_ID, 10.0, Bank.LEFT); String streamLocId2 = STREAM_LOC_IDS.get(1); - StreamLocation streamLocation2 = buildTestStreamLocation("TEST_STREAM_123", streamLocId2, 11.0, Bank.RIGHT); + StreamLocation streamLocation2 = buildTestStreamLocation("TEST_STREAM_123", streamLocId2, OFFICE_ID, 11.0, Bank.RIGHT); //store stream locations streamLocationDao.storeStreamLocation(streamLocation, false); @@ -245,16 +245,16 @@ void testRoundTrip() throws Exception { }, webUser); } - private static StreamLocation buildTestStreamLocation(String streamId, String locationId, double station, Bank bank) { + static StreamLocation buildTestStreamLocation(String streamId, String locationId, String officeId, double station, Bank bank) { StreamLocationNode streamLocationNode = new StreamLocationNode.Builder() .withId(new CwmsId.Builder() .withName(locationId) - .withOfficeId(OFFICE_ID) + .withOfficeId(officeId) .build()) .withStreamNode(new StreamNode.Builder() .withStreamId(new CwmsId.Builder() .withName(streamId) - .withOfficeId(OFFICE_ID) + .withOfficeId(officeId) .build()) .withStation(station) .withBank(bank) diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/EmbankmentDaoIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/EmbankmentDaoIT.java index 83fcae945..90d19a402 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/EmbankmentDaoIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/EmbankmentDaoIT.java @@ -78,7 +78,6 @@ public void setup() throws Exception { @AfterAll public void tearDown() throws Exception { - tearDownProject(); CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); databaseLink.connection(c -> { DSLContext context = getDslContext(c, OFFICE_ID); diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoChangeIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoChangeIT.java index c63322481..87cd3f079 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoChangeIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoChangeIT.java @@ -126,10 +126,7 @@ static void tearDown() throws Exception { outletDao.deleteOutlet(TAINTER_GATE_20_LOC.getOfficeId(), TAINTER_GATE_20_LOC.getName(), DeleteRule.DELETE_ALL); deleteLocationGroup(context, TAINTER_GATE_10_OUTLET); - deleteLocation(context, TAINTER_GATE_10_LOC.getOfficeId(), TAINTER_GATE_10_LOC.getName()); - deleteLocation(context, TAINTER_GATE_20_LOC.getOfficeId(), TAINTER_GATE_20_LOC.getName()); }, CwmsDataApiSetupCallback.getWebUser()); - tearDownProject(); } @Test diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoIT.java index 43c956aea..0fd88b054 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoIT.java @@ -100,12 +100,7 @@ static void tearDown() throws Exception { outletDao.deleteOutlet(TAINTER_GATE_2_LOC.getOfficeId(), TAINTER_GATE_2_LOC.getName(), DeleteRule.DELETE_ALL); outletDao.deleteOutlet(BOX_CULVERT_1_LOC.getOfficeId(), BOX_CULVERT_1_LOC.getName(), DeleteRule.DELETE_ALL); - deleteLocation(context, TAINTER_GATE_1_LOC.getOfficeId(), TAINTER_GATE_1_LOC.getName()); - deleteLocation(context, TAINTER_GATE_2_LOC.getOfficeId(), TAINTER_GATE_2_LOC.getName()); - deleteLocation(context, TAINTER_GATE_3_LOC.getOfficeId(), TAINTER_GATE_3_LOC.getName()); - deleteLocation(context, BOX_CULVERT_1_LOC.getOfficeId(), BOX_CULVERT_1_LOC.getName()); }, CwmsDataApiSetupCallback.getWebUser()); - tearDownProject(); } @Test diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoRatingIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoRatingIT.java index 85914995b..56d3c36dc 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoRatingIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoRatingIT.java @@ -69,11 +69,6 @@ static void setup() throws Exception { setupProject(); } - @AfterAll - static void cleanup() throws Exception { - tearDownProject(); - } - @Test void test_uncontrolled_outlet() throws SQLException { CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); @@ -90,8 +85,8 @@ void test_uncontrolled_outlet() throws SQLException { group.getId(), group.getDescription(), SPILLWAY_CURVE_ID, group.getSharedRefLocationId(), group.getLocGroupAttribute()); LocationGroup newGroup = new LocationGroup(modifiedGroup, group.getAssignedLocations()); - locationGroupDao.unassignAllLocs(group); - locationGroupDao.assignLocs(newGroup); + locationGroupDao.unassignAllLocs(group, PROJECT_1_ID.getOfficeId()); + locationGroupDao.assignLocs(newGroup, PROJECT_1_ID.getOfficeId()); Outlet retrievedOutlet = outletDao.retrieveOutlet(PROJECT_SPILLWAY_LOC.getOfficeId(), PROJECT_SPILLWAY_LOC.getName()); assertNotNull(retrievedOutlet); @@ -119,8 +114,8 @@ void test_controlled_outlet() throws SQLException { group.getId(), group.getDescription(), LOW_FLOW_CURVE_ID, group.getSharedRefLocationId(), group.getLocGroupAttribute()); LocationGroup newGroup = new LocationGroup(modifiedGroup, group.getAssignedLocations()); - locationGroupDao.unassignAllLocs(group); - locationGroupDao.assignLocs(newGroup); + locationGroupDao.unassignAllLocs(group, PROJECT_1_ID.getOfficeId()); + locationGroupDao.assignLocs(newGroup, PROJECT_1_ID.getOfficeId()); Outlet retrievedOutlet = outletDao.retrieveOutlet(PROJECT_LOW_FLOW_LOC.getOfficeId(), PROJECT_LOW_FLOW_LOC.getName()); assertNotNull(retrievedOutlet); diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoVirtualIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoVirtualIT.java index 167b7e2bc..5ae4b70b4 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoVirtualIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/OutletDaoVirtualIT.java @@ -170,19 +170,7 @@ static void teardown() throws Exception { outletDao.deleteOutlet(CO3_I2.getOfficeId(), CO3_I2.getName(), DeleteRule.DELETE_ALL); outletDao.deleteOutlet(CO3_I3.getOfficeId(), CO3_I3.getName(), DeleteRule.DELETE_ALL); outletDao.deleteOutlet(CO3_CONDUIT.getOfficeId(), CO3_CONDUIT.getName(), DeleteRule.DELETE_ALL); - - deleteLocation(context, CO1_I25.getOfficeId(), CO1_I25.getName()); - deleteLocation(context, CO1_I53.getOfficeId(), CO1_I53.getName()); - deleteLocation(context, CO1_LOW_FLOW.getOfficeId(), CO1_LOW_FLOW.getName()); - deleteLocation(context, CO2_CONDUIT.getOfficeId(), CO2_CONDUIT.getName()); - deleteLocation(context, CO2_INTAKE.getOfficeId(), CO2_INTAKE.getName()); - deleteLocation(context, CO2_WEIR.getOfficeId(), CO2_WEIR.getName()); - deleteLocation(context, CO3_I1.getOfficeId(), CO3_I1.getName()); - deleteLocation(context, CO3_I2.getOfficeId(), CO3_I2.getName()); - deleteLocation(context, CO3_I3.getOfficeId(), CO3_I3.getName()); - deleteLocation(context, CO3_CONDUIT.getOfficeId(), CO3_CONDUIT.getName()); }, CwmsDataApiSetupCallback.getWebUser()); - tearDownProject(); } @Test diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/ProjectStructureIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/ProjectStructureIT.java index 079e37946..582b3867a 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/ProjectStructureIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/ProjectStructureIT.java @@ -9,9 +9,9 @@ import com.google.common.flogger.FluentLogger; import cwms.cda.api.DataApiTestIT; +import cwms.cda.api.LocationCleanup; import cwms.cda.api.enums.Nation; import cwms.cda.api.errors.NotFoundException; -import cwms.cda.data.dao.DeleteRule; import cwms.cda.data.dao.LocationsDaoImpl; import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.Location; @@ -46,24 +46,10 @@ public static void setupProject() throws Exception { CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); databaseLink.connection(c -> { DSLContext context = getDslContext(c, OFFICE_ID); - CWMS_PROJECT_PACKAGE.call_STORE_PROJECT(context.configuration(), buildProject(PROJECT_LOC), "T"); - CWMS_PROJECT_PACKAGE.call_STORE_PROJECT(context.configuration(), buildProject(PROJECT_LOC2), "T"); - }, CwmsDataApiSetupCallback.getWebUser()); - } - - public static void tearDownProject() throws Exception - { - //Don't tag this as a @AfterAll - JUnit can't guarantee this occurs first. - CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); - databaseLink.connection(c -> { - DSLContext context = getDslContext(c, OFFICE_ID); - LocationsDaoImpl locationsDao = new LocationsDaoImpl(context); - CWMS_PROJECT_PACKAGE.call_DELETE_PROJECT(context.configuration(), PROJECT_LOC.getName(), - DeleteRule.DELETE_ALL.getRule(), OFFICE_ID); - CWMS_PROJECT_PACKAGE.call_DELETE_PROJECT(context.configuration(), PROJECT_LOC2.getName(), - DeleteRule.DELETE_ALL.getRule(), OFFICE_ID); - locationsDao.deleteLocation(PROJECT_LOC.getName(), OFFICE_ID, true); - locationsDao.deleteLocation(PROJECT_LOC2.getName(), OFFICE_ID, true); + CWMS_PROJECT_PACKAGE.call_STORE_PROJECT(context.configuration(), buildProject(PROJECT_LOC), "F"); + LocationCleanup.locationsCreated.add(PROJECT_LOC); + CWMS_PROJECT_PACKAGE.call_STORE_PROJECT(context.configuration(), buildProject(PROJECT_LOC2), "F"); + LocationCleanup.locationsCreated.add(PROJECT_LOC2); }, CwmsDataApiSetupCallback.getWebUser()); } @@ -134,6 +120,7 @@ public static void storeLocation(DSLContext context, Location loc) throws IOExce LocationsDaoImpl locationsDao = new LocationsDaoImpl(context); deleteLocation(context, loc.getOfficeId(), loc.getName()); locationsDao.storeLocation(loc); + LocationCleanup.locationsCreated.add(loc); } public static void deleteLocation(DSLContext context, String officeId, String locId) { diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/TurbineDaoIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/TurbineDaoIT.java index 90d5fdfc3..3a22c29ec 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/TurbineDaoIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/location/kind/TurbineDaoIT.java @@ -207,7 +207,7 @@ void testRename() throws Exception { Turbine retrievedTurbine = turbineDao.retrieveTurbine(newId, office); assertEquals(newId, retrievedTurbine.getLocation().getName()); turbineDao.deleteTurbine(newId, office, DeleteRule.DELETE_ALL); - }); + }, CwmsDataApiSetupCallback.getWebUser()); } @Test @@ -241,8 +241,7 @@ void testTurbineChangesRoundTrip() throws Exception { turbineDao.deleteTurbine(TURBINE_LOC1.getName(), TURBINE_LOC1.getOfficeId(), DeleteRule.DELETE_ALL); turbineDao.deleteTurbine(TURBINE_LOC2.getName(), TURBINE_LOC2.getOfficeId(), DeleteRule.DELETE_ALL); turbineDao.deleteTurbine(TURBINE_LOC3.getName(), TURBINE_LOC3.getOfficeId(), DeleteRule.DELETE_ALL); - }); - + }, CwmsDataApiSetupCallback.getWebUser()); } private static Location buildProjectLocation(String projectId) { diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/CwmsDTOValidatorTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/CwmsDTOValidatorTest.java index 691b2e72a..a38eb46ea 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/CwmsDTOValidatorTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/CwmsDTOValidatorTest.java @@ -65,7 +65,7 @@ void testLargeCwmsDTOValidationNineRequiredFieldInvalid() { .collect(toList()); CwmsDTOHolder cwmsDTOHolder = new CwmsDTOHolder(collect); - assertTimeout(Duration.ofMillis(150L), () -> assertThrows(FieldException.class, cwmsDTOHolder::validate)); + assertTimeout(Duration.ofMillis(300L), () -> assertThrows(FieldException.class, cwmsDTOHolder::validate)); } private static final class OneField extends CwmsDTOBase { diff --git a/cwms-data-api/src/test/java/cwms/cda/formatters/FormatsTest.java b/cwms-data-api/src/test/java/cwms/cda/formatters/FormatsTest.java index 1b44bdb9e..ea6feb1f4 100644 --- a/cwms-data-api/src/test/java/cwms/cda/formatters/FormatsTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/formatters/FormatsTest.java @@ -145,6 +145,15 @@ void testParseHeader() { } + + @ParameterizedTest + @EnumSource(ParseQueryOrParamTest.class) + void test_header_or_query_parm(ParseQueryOrParamTest test) throws Exception { + ContentType ct = Formats.parseQueryOrHeaderParam(test.header, test.query, test.dto); + System.out.println(ct.toString()); + assertTrue(ContentType.equivalent(ct.toString(), test.type), "In correct content type returned."); + } + @EnumSource(ParseQueryParamTest.class) @ParameterizedTest void testParseQueryParam(ParseQueryParamTest test) { @@ -250,4 +259,27 @@ enum ParseQueryParamTest { } } + + enum ParseQueryOrParamTest { + BOTH(Formats.XML, Formats.CSV_LEGACY, Office.class, Formats.CSV), + HEADER(Formats.JSON, null, Office.class, Formats.JSONV2), + QUERY(null, Formats.TAB_LEGACY, Office.class, Formats.TAB) + ; + + final String header; + final String query; + final Class dto; + final String type; + + + ParseQueryOrParamTest(String header, String query, Class dto, String type) + { + this.header = header; + this.query = query; + this.dto = dto; + this.type = type; + } + + + } } \ No newline at end of file diff --git a/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java b/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java index a40cf0f30..e218d14d0 100644 --- a/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java +++ b/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java @@ -405,27 +405,27 @@ public static void assertMatch(Measurement first, Measurement second) { public static void assertMatch(StreamflowMeasurement first, StreamflowMeasurement second) { assertAll( - () -> assertEquals(first.getGageHeight(), second.getGageHeight(), "Gage height does not match"), - () -> assertEquals(first.getFlow(), second.getFlow(), "Flow does not match"), + () -> assertEquals(first.getGageHeight(), second.getGageHeight(), DEFAULT_DELTA,"Gage height does not match"), + () -> assertEquals(first.getFlow(), second.getFlow(), DEFAULT_DELTA, "Flow does not match"), () -> assertEquals(first.getQuality(), second.getQuality(), "Quality does not match") ); } public static void assertMatch(SupplementalStreamflowMeasurement first, SupplementalStreamflowMeasurement second) { assertAll( - () -> assertEquals(first.getChannelFlow(), second.getChannelFlow(), "Channel flow does not match"), - () -> assertEquals(first.getOverbankFlow(), second.getOverbankFlow(), "Overbank flow does not match"), - () -> assertEquals(first.getOverbankMaxDepth(), second.getOverbankMaxDepth(), "Overbank max depth does not match"), - () -> assertEquals(first.getChannelMaxDepth(), second.getChannelMaxDepth(), "Channel max depth does not match"), - () -> assertEquals(first.getAvgVelocity(), second.getAvgVelocity(), "Average velocity does not match"), - () -> assertEquals(first.getSurfaceVelocity(), second.getSurfaceVelocity(), "Surface velocity does not match"), - () -> assertEquals(first.getMaxVelocity(), second.getMaxVelocity(), "Max velocity does not match"), - () -> assertEquals(first.getEffectiveFlowArea(), second.getEffectiveFlowArea(), "Effective flow area does not match"), - () -> assertEquals(first.getCrossSectionalArea(), second.getCrossSectionalArea(), "Cross sectional area does not match"), - () -> assertEquals(first.getMeanGage(), second.getMeanGage(), "Mean gage does not match"), - () -> assertEquals(first.getTopWidth(), second.getTopWidth(), "Top width does not match"), - () -> assertEquals(first.getMainChannelArea(), second.getMainChannelArea(), "Main channel area does not match"), - () -> assertEquals(first.getOverbankArea(), second.getOverbankArea(), "Overbank area does not match") + () -> assertEquals(first.getChannelFlow(), second.getChannelFlow(), DEFAULT_DELTA, "Channel flow does not match"), + () -> assertEquals(first.getOverbankFlow(), second.getOverbankFlow(), DEFAULT_DELTA, "Overbank flow does not match"), + () -> assertEquals(first.getOverbankMaxDepth(), second.getOverbankMaxDepth(), DEFAULT_DELTA, "Overbank max depth does not match"), + () -> assertEquals(first.getChannelMaxDepth(), second.getChannelMaxDepth(), DEFAULT_DELTA, "Channel max depth does not match"), + () -> assertEquals(first.getAvgVelocity(), second.getAvgVelocity(), DEFAULT_DELTA, "Average velocity does not match"), + () -> assertEquals(first.getSurfaceVelocity(), second.getSurfaceVelocity(), DEFAULT_DELTA, "Surface velocity does not match"), + () -> assertEquals(first.getMaxVelocity(), second.getMaxVelocity(), DEFAULT_DELTA, "Max velocity does not match"), + () -> assertEquals(first.getEffectiveFlowArea(), second.getEffectiveFlowArea(), DEFAULT_DELTA, "Effective flow area does not match"), + () -> assertEquals(first.getCrossSectionalArea(), second.getCrossSectionalArea(), DEFAULT_DELTA, "Cross sectional area does not match"), + () -> assertEquals(first.getMeanGage(), second.getMeanGage(), DEFAULT_DELTA, "Mean gage does not match"), + () -> assertEquals(first.getTopWidth(), second.getTopWidth(), DEFAULT_DELTA, "Top width does not match"), + () -> assertEquals(first.getMainChannelArea(), second.getMainChannelArea(), DEFAULT_DELTA, "Main channel area does not match"), + () -> assertEquals(first.getOverbankArea(), second.getOverbankArea(), DEFAULT_DELTA, "Overbank area does not match") ); } @@ -434,13 +434,13 @@ public static void assertMatch(UsgsMeasurement first, UsgsMeasurement second) { () -> assertEquals(first.getRemarks(), second.getRemarks(), "Remarks do not match"), () -> assertEquals(first.getCurrentRating(), second.getCurrentRating(), "Current rating does not match"), () -> assertEquals(first.getControlCondition(), second.getControlCondition(), "Control condition does not match"), - () -> assertEquals(first.getShiftUsed(), second.getShiftUsed(), "Shift used does not match"), - () -> assertEquals(first.getPercentDifference(), second.getPercentDifference(), "Percent difference does not match"), + () -> assertEquals(first.getShiftUsed(), second.getShiftUsed(), DEFAULT_DELTA, "Shift used does not match"), + () -> assertEquals(first.getPercentDifference(), second.getPercentDifference(), DEFAULT_DELTA, "Percent difference does not match"), () -> assertEquals(first.getFlowAdjustment(), second.getFlowAdjustment(), "Flow adjustment does not match"), - () -> assertEquals(first.getDeltaHeight(), second.getDeltaHeight(), "Delta height does not match"), + () -> assertEquals(first.getDeltaHeight(), second.getDeltaHeight(), DEFAULT_DELTA, "Delta height does not match"), () -> assertEquals(first.getDeltaTime(), second.getDeltaTime(), "Delta time does not match"), - () -> assertEquals(first.getAirTemp(), second.getAirTemp(), "Air temperature does not match"), - () -> assertEquals(first.getWaterTemp(), second.getWaterTemp(), "Water temperature does not match") + () -> assertEquals(first.getAirTemp(), second.getAirTemp(), DEFAULT_DELTA, "Air temperature does not match"), + () -> assertEquals(first.getWaterTemp(), second.getWaterTemp(), DEFAULT_DELTA, "Water temperature does not match") ); } diff --git a/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java b/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java index f7e1b940f..519eac93f 100644 --- a/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java +++ b/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java @@ -25,7 +25,9 @@ import helpers.TsRandomSampler; import io.restassured.RestAssured; import io.restassured.config.JsonConfig; +import io.restassured.filter.log.LogDetail; import io.restassured.path.json.config.JsonPathConfig; +import io.restassured.response.ValidatableResponse; import javax.servlet.http.HttpServletResponse; @@ -43,7 +45,7 @@ public class CwmsDataApiSetupCallback implements BeforeAllCallback,AfterAllCallb private static final String ORACLE_IMAGE = System.getProperty("CDA.oracle.database.image",System.getProperty("RADAR.oracle.database.image", CwmsDatabaseContainer.ORACLE_19C)); private static final String ORACLE_VOLUME = System.getProperty("CDA.oracle.database.volume",System.getProperty("RADAR.oracle.database.volume", "cwmsdb_data_api_volume")); - private static final String CWMS_DB_IMAGE = System.getProperty("CDA.cwms.database.image",System.getProperty("RADAR.cwms.database.image", "registry.hecdev.net/cwms/schema_installer:99.99.99.2-CDA_STAGING")); + static final String CWMS_DB_IMAGE = System.getProperty("CDA.cwms.database.image",System.getProperty("RADAR.cwms.database.image", "registry.hecdev.net/cwms/schema_installer:99.99.99.2-CDA_STAGING")); private static String webUser = null; @@ -91,7 +93,7 @@ public void beforeAll(ExtensionContext context) throws Exception { logger.atInfo().log("Tomcat Listing on " + cdaInstance.getPort()); RestAssured.baseURI=CwmsDataApiSetupCallback.httpUrl(); RestAssured.port = CwmsDataApiSetupCallback.httpPort(); - RestAssured.basePath = "/cwms-data"; + RestAssured.basePath = System.getProperty("warContext"); // we only use doubles RestAssured.config() .jsonConfig( @@ -110,6 +112,7 @@ private static void healthCheck() throws InterruptedException { .when() .get("/offices/SPK") .then() + .log().ifValidationFails(LogDetail.ALL) .assertThat() .statusCode(is(HttpServletResponse.SC_OK)); logger.atInfo().log("Server is up!"); diff --git a/cwms-data-api/src/test/java/fixtures/IntegrationTestNameGenerator.java b/cwms-data-api/src/test/java/fixtures/IntegrationTestNameGenerator.java new file mode 100644 index 000000000..6b2048559 --- /dev/null +++ b/cwms-data-api/src/test/java/fixtures/IntegrationTestNameGenerator.java @@ -0,0 +1,53 @@ +/* + * 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 fixtures; + +import java.lang.reflect.Method; +import org.junit.jupiter.api.DisplayNameGenerator; + +public final class IntegrationTestNameGenerator implements DisplayNameGenerator { + private static String schemaVersion; + + static { + // Fetch the schema version from a system property + String[] split = CwmsDataApiSetupCallback.CWMS_DB_IMAGE.split(":"); + schemaVersion = split[split.length - 1]; + } + + @Override + public String generateDisplayNameForClass(Class testClass) { + return testClass.getSimpleName() + " (schema: " + schemaVersion + ")"; + } + + @Override + public String generateDisplayNameForNestedClass(Class nestedClass) { + return nestedClass.getSimpleName() + " (schema: " + schemaVersion + ")"; + } + + @Override + public String generateDisplayNameForMethod(Class testClass, Method testMethod) { + return testClass.getSimpleName() + "." + testMethod.getName() + " (schema: " + schemaVersion + ")"; + } +} diff --git a/cwms-data-api/src/test/java/fixtures/MinimumSchema.java b/cwms-data-api/src/test/java/fixtures/MinimumSchema.java new file mode 100644 index 000000000..10fea8308 --- /dev/null +++ b/cwms-data-api/src/test/java/fixtures/MinimumSchema.java @@ -0,0 +1,38 @@ +/* + * 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 fixtures; + +import org.junit.jupiter.api.extension.ExtendWith; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(SchemaVersionCondition.class) +public @interface MinimumSchema { + int value(); +} diff --git a/cwms-data-api/src/test/java/fixtures/SchemaVersionCondition.java b/cwms-data-api/src/test/java/fixtures/SchemaVersionCondition.java new file mode 100644 index 000000000..5abc675b0 --- /dev/null +++ b/cwms-data-api/src/test/java/fixtures/SchemaVersionCondition.java @@ -0,0 +1,67 @@ +/* + * 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 fixtures; + +import cwms.cda.data.dao.AuthDao; +import java.sql.SQLException; +import java.util.Objects; +import org.jooq.impl.DSL; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class SchemaVersionCondition implements ExecutionCondition { + + private static final ConditionEvaluationResult ENABLED = ConditionEvaluationResult + .enabled("@MinimumSchemaVersion is not present"); + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + return context.getElement() + .map(el -> el.getAnnotation(MinimumSchema.class)) + .filter(Objects::nonNull) + .map(annotation -> { + int version = annotation.value(); + int currentVersion = getCurrentSchemaVersion(); + if (currentVersion < version) { + return ConditionEvaluationResult.disabled("Test disabled because schema version " + + currentVersion + " is less than " + version); + } + return ConditionEvaluationResult.enabled("Test enabled because schema version " + + currentVersion + " is at least " + version); + }) + .orElse(ENABLED); + } + + private int getCurrentSchemaVersion() { + try { + return CwmsDataApiSetupCallback.getDatabaseLink().connection(c->{ + return AuthDao.getInstance(DSL.using(c)).getDbVersion(); + }, CwmsDataApiSetupCallback.getWebUser()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/cwms-data-api/src/test/java/fixtures/TestAccounts.java b/cwms-data-api/src/test/java/fixtures/TestAccounts.java index 5f2ad3b0f..44b941bfe 100644 --- a/cwms-data-api/src/test/java/fixtures/TestAccounts.java +++ b/cwms-data-api/src/test/java/fixtures/TestAccounts.java @@ -26,15 +26,17 @@ import java.util.Arrays; +import cwms.cda.ApiServlet; + public class TestAccounts { public enum KeyUser { NONE("none",null,null,null,null, null), // Used for annotations GUEST("guest",null,null,null, null, null), // USED as marker label for tests - SPK_NORMAL("l2hectest","l2hectest","1234567890","l2userkey","ATotallyRandomStringL2hectest","SPK", "CWMS Users", "cac_user"), - SPK_NORMAL2("l2hectest_vt","l2hectestvt","2345678901","l2userkey2","DiffrntStringL2hectest_vt","SPK", "CWMS Users", "cac_user"), - SWT_NORMAL("m5hectest","swt99db","1234567890","testkey2","ATotallyRandomStringM5hectest","SWT", "CWMS Users", "cac_user"), + SPK_NORMAL("l2hectest","l2hectest","1234567890","l2userkey","ATotallyRandomStringL2hectest","SPK", "CWMS Users", ApiServlet.CAC_USER), + SPK_NORMAL2("l2hectest_vt","l2hectestvt","2345678901","l2userkey2","DiffrntStringL2hectest_vt","SPK", "CWMS Users", ApiServlet.CAC_USER), + SWT_NORMAL("m5hectest","swt99db","1234567890","testkey2","ATotallyRandomStringM5hectest","SWT", "CWMS Users", ApiServlet.CAC_USER), SPK_NO_ROLES("user2","user2",null,"User2key","user2SEssion", "SPK"); private final String name; // username diff --git a/cwms-data-api/src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener b/cwms-data-api/src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener new file mode 100644 index 000000000..273cc4490 --- /dev/null +++ b/cwms-data-api/src/test/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener @@ -0,0 +1 @@ +cwms.cda.api.LocationCleanup \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/gate_change.json b/cwms-data-api/src/test/resources/cwms/cda/api/gate_change.json new file mode 100644 index 000000000..90f1cacf6 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/gate_change.json @@ -0,0 +1,39 @@ +[ { + "type" : "gate-change", + "project-id" : { + "office-id" : "SPK", + "name" : "BIGH" + }, + "change-date" : 1704096000000, + "pool-elevation" : 3.0, + "protected" : true, + "discharge-computation-type" : { + "office-id" : "SPK", + "display-value" : "A", + "tooltip" : "Adjusted by an automated method", + "active" : true + }, + "reason-type" : { + "office-id" : "SPK", + "display-value" : "O", + "tooltip" : "Other release", + "active" : true + }, + "notes" : "Test notes", + "new-total-discharge-override" : 1.0, + "old-total-discharge-override" : 2.0, + "discharge-units" : "cfs", + "tailwater-elevation" : 4.0, + "elevation-units" : "ft", + "settings" : [ { + "type" : "gate-setting", + "location-id" : { + "office-id" : "SPK", + "name" : "BIGH-CG100" + }, + "opening" : 10.0, + "opening-parameter" : "Opening", + "opening-units" : "ft", + "invert-elevation" : 20.0 + } ] +} ] \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/lrl/pseudo_reg_1hour.json b/cwms-data-api/src/test/resources/cwms/cda/api/lrl/pseudo_reg_1hour.json index 54abb997b..c44f093b4 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/api/lrl/pseudo_reg_1hour.json +++ b/cwms-data-api/src/test/resources/cwms/cda/api/lrl/pseudo_reg_1hour.json @@ -8,6 +8,16 @@ 500, 0 ], + [ + 1673439400000, + null, + 5 + ], + [ + 1673440400000, + -340282346638528859811704183484516925440, + 5 + ], [ 1673442000000, 600, diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/measurement.json b/cwms-data-api/src/test/resources/cwms/cda/api/measurement.json new file mode 100644 index 000000000..0c23d00c6 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/measurement.json @@ -0,0 +1,51 @@ +[ + { + "height-unit": "ft", + "flow-unit": "cfs", + "temp-unit": "F", + "velocity-unit": "fps", + "area-unit": "ft2", + "used": true, + "agency": "USGS", + "party": "SPK", + "wm-comments": "Measurement made during normal flow conditions.", + "instant": "2024-09-16T00:00:00Z", + "number": "123456", + "id": { + "name": "StreamLoc321", + "office-id": "SPK" + }, + "streamflow-measurement": { + "gage-height": 5.5, + "flow": 250.0, + "quality": "G" + }, + "supplemental-streamflow-measurement": { + "channel-flow": 300.0, + "overbank-flow": 50.0, + "overbank-max-depth": 5.0, + "channel-max-depth": 10.0, + "avg-velocity": 1.5, + "surface-velocity": 2.0, + "max-velocity": 3.0, + "effective-flow-area": 200.0, + "cross-sectional-area": 250.0, + "mean-gage": 20.0, + "top-width": 30.0, + "main-channel-area": 150.0, + "overbank-area": 80.0 + }, + "usgs-measurement": { + "remarks": "Remarks", + "current-rating": "1", + "control-condition": "FILL", + "flow-adjustment": "OTHR", + "shift-used": 0.1, + "percent-difference": 5.0, + "delta-height": 0.05, + "delta-time": 10.0, + "air-temp": 20.0, + "water-temp": 15.0 + } + } +] diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/measurements.json b/cwms-data-api/src/test/resources/cwms/cda/api/measurements.json new file mode 100644 index 000000000..3db3ce30b --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/measurements.json @@ -0,0 +1,100 @@ +[ + { + "height-unit": "ft", + "flow-unit": "cfs", + "temp-unit": "F", + "velocity-unit": "fps", + "area-unit": "ft2", + "used": true, + "agency": "USGS", + "party": "SPK", + "wm-comments": "Measurement made during normal flow conditions.", + "instant": "2024-09-16T00:00:00Z", + "number": "123456", + "id": { + "name": "StreamLoc321", + "office-id": "SPK" + }, + "streamflow-measurement": { + "gage-height": 5.5, + "flow": 250.0, + "quality": "G" + }, + "supplemental-streamflow-measurement": { + "channel-flow": 300.0, + "overbank-flow": 50.0, + "overbank-max-depth": 5.0, + "channel-max-depth": 10.0, + "avg-velocity": 1.5, + "surface-velocity": 2.0, + "max-velocity": 3.0, + "effective-flow-area": 200.0, + "cross-sectional-area": 250.0, + "mean-gage": 20.0, + "top-width": 30.0, + "main-channel-area": 150.0, + "overbank-area": 80.0 + }, + "usgs-measurement": { + "remarks": "Remarks", + "current-rating": "1", + "control-condition": "FILL", + "flow-adjustment": "OTHR", + "shift-used": 0.1, + "percent-difference": 5.0, + "delta-height": 0.05, + "delta-time": 10.0, + "air-temp": 20.0, + "water-temp": 15.0 + } + }, + { + "height-unit": "ft", + "flow-unit": "cfs", + "temp-unit": "F", + "velocity-unit": "fps", + "area-unit": "ft2", + "used": true, + "agency": "USGS", + "party": "SPK", + "wm-comments": "Measurement made after recent rainfall.", + "instant": "2024-09-17T12:00:00Z", + "number": "654321", + "id": { + "name": "StreamLoc321", + "office-id": "SPK" + }, + "streamflow-measurement": { + "gage-height": 6.0, + "flow": 275.0, + "quality": "F" + }, + "supplemental-streamflow-measurement": { + "channel-flow": 320.0, + "overbank-flow": 45.0, + "overbank-max-depth": 4.5, + "channel-max-depth": 9.5, + "avg-velocity": 1.8, + "surface-velocity": 2.5, + "max-velocity": 3.5, + "effective-flow-area": 220.0, + "cross-sectional-area": 260.0, + "mean-gage": 21.0, + "top-width": 32.0, + "main-channel-area": 160.0, + "overbank-area": 85.0 + }, + "usgs-measurement": { + "remarks": "Post-rain conditions.", + "current-rating": "2", + "control-condition": "FILL", + "flow-adjustment": "OTHR", + "shift-used": 0.15, + "percent-difference": 4.5, + "delta-height": 0.1, + "delta-time": 15.0, + "air-temp": 18.0, + "water-temp": 16.0 + } + } +] diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/timeseries_create_SPK.json b/cwms-data-api/src/test/resources/cwms/cda/api/timeseries_create_SPK.json new file mode 100644 index 000000000..ac0717fe2 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/timeseries_create_SPK.json @@ -0,0 +1,50 @@ +{ + "begin": "2021-06-21T08:00:00-0700[PST8PDT]", + "end": "2021-06-21T09:00:00-0700[PST8PDT]", + "interval": "PT15M", + "name": "ZACK.Stage.Inst.5Minutes.0.ZSTORE_TS_TEST", + "office-id": "SPK", + "units": "m", + "page-size": 1000, + "total": 4, + "page": null, + "value-columns": [ + { + "name": "date-time", + "ordinal": 1, + "datatype": "java.sql.Timestamp" + }, + { + "name": "value", + "ordinal": 2, + "datatype": "java.lang.Double" + }, + { + "name": "quality-code", + "ordinal": 3, + "datatype": "int" + } + ], + "values": [ + [ + 1624287600000, + 0.0, + 0 + ], + [ + 1624288500000, + 1.0, + 0 + ], + [ + 1624289400000, + 2.0, + 0 + ], + [ + 1624290300000, + 3.0, + 0 + ] + ] +} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dao/dbMeasurements.xml b/cwms-data-api/src/test/resources/cwms/cda/data/dao/dbMeasurements.xml new file mode 100644 index 000000000..3bd498e73 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dao/dbMeasurements.xml @@ -0,0 +1,82 @@ + + + 12345 + 2024-01-01T00:00:00Z + Walnut_Ck + SomeParty + Test comment + USGS + + 2.0 + 100.0 + good + + + Some remarks + 1 + UNSPECIFIED + 11.0 + 10.0 + UNKNOWN + 0.5 + 60.0 + 25.0 + 15.0 + + + 100.0 + 50.0 + 2.0 + 5.0 + 1.5 + 1.0 + 2.0 + 75.0 + 60.0 + 3.0 + 10.0 + 150.0 + 200.0 + + + + 123456 + 2024-02-01T00:00:00Z + Walnut_Ck + SomeParty2 + Test comment2 + USGS + + 2.4 + 200.0 + G + + + Some remarks2 + 2 + UNSPECIFIED + 12.0 + 11.0 + OTHR + 0.6 + 70.0 + 35.0 + 16.0 + + + 101.0 + 51.0 + 2.1 + 5.1 + 1.6 + 1.1 + 2.1 + 75.1 + 60.1 + 3.1 + 10.1 + 150.1 + 200.1 + + + \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/location/kind/gate-change.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/location/kind/gate-change.json index 9cc7ff0e8..95bd60de1 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/data/dto/location/kind/gate-change.json +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/location/kind/gate-change.json @@ -1,4 +1,5 @@ { + "type": "gate-change", "project-id": { "office-id": "SPK", "name": "BIGH" diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/location/kind/gate-changes.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/location/kind/gate-changes.json index 06cd71a15..b061dbdf9 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/data/dto/location/kind/gate-changes.json +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/location/kind/gate-changes.json @@ -1,5 +1,6 @@ [ { + "type": "gate-change", "project-id": { "office-id": "SPK", "name": "BIGH" @@ -51,6 +52,7 @@ ] }, { + "type": "gate-change", "project-id": { "office-id": "SPK", "name": "BIGH" diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/sql/delete_mixed_ts_group.sql b/cwms-data-api/src/test/resources/cwms/cda/data/sql/delete_mixed_ts_group.sql new file mode 100644 index 000000000..b6e3959f0 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/sql/delete_mixed_ts_group.sql @@ -0,0 +1,35 @@ +begin + + -- unassign a ts to the group + cwms_ts.unassign_ts_group( + p_ts_category_id=>'Test Category2', + p_ts_group_id=>'Test Group2', + p_ts_id=>'Clear Creek.Precip-Cumulative.Inst.15Minutes.0.raw-cda', + p_unassign_all=>'T', + p_db_office_id=>'LRL'); + + + -- unassign a ts to the group + cwms_ts.unassign_ts_group( + p_ts_category_id=>'Test Category2', + p_ts_group_id=>'Test Group2', + p_ts_id=>'Alder Springs.Precip-Cumulative.Inst.15Minutes.0.raw-cda', + p_unassign_all=>'T', + p_db_office_id=>'SPK'); + + -- delete a group at CWMS in the mew category + cwms_ts.delete_ts_group('Test Category2', + 'Test Group2', + 'CWMS'); + + -- delete a category at CWMS + cwms_ts.delete_ts_category('Test Category2', + 'T', + 'CWMS'); + + + + + + +end; \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/sql/delete_spk_aliases_and_groups.sql b/cwms-data-api/src/test/resources/cwms/cda/data/sql/delete_spk_aliases_and_groups.sql new file mode 100644 index 000000000..ffbd20c4d --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/sql/delete_spk_aliases_and_groups.sql @@ -0,0 +1,10 @@ +begin + -- delete a timeseries alias + cwms_ts.unassign_ts_group(p_ts_category_id=>'Test Category', + p_ts_group_id=>'Test Group', + p_ts_id=>'Alder Springs.Precip-Cumulative.Inst.15Minutes.0.raw-cda', + p_unassign_all=>'T', + p_db_office_id=>'SPK'); + cwms_ts.delete_ts_group('Test Category','Test Group','SPK'); + cwms_ts.delete_ts_category('Test Category', 'T', 'SPK'); +end; \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6702174f5..5e8c5f20c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] jaxb-api = "2.3.1" jaxb-impl = "3.0.2" -jooq-codegen = "99.99.99.2-CDA_STAGING" +jooq-codegen = "99.99.99.3-CDA_STAGING" jooq = "3.18.7-jdk8" hec-data-access = "9.8.0" slf4j = "1.7.36" @@ -11,9 +11,9 @@ flogger = "0.7.4" google-findbugs = "3.0.2" error_prone_annotations = "2.15.0" cwms-ratings = "2.0.2" -javalin = "4.6.7" -tomcat = "9.0.91" -swagger-core = "2.2.2" +javalin = "4.6.8" +tomcat = "9.0.93" +swagger-core = "2.2.23" swagger-ui = "5.9.0" jackson = "2.17.1" geojson-jackson = "1.14" @@ -24,7 +24,8 @@ java-ee = "8.0.1" cwms-tomcat-auth = "1.1.0" jjwt = "0.11.5" jstl = "1.2" -junit = "5.8.2" +junit = "5.11.1" +junit-launcher = "1.11.2" testcontainers = "1.17.1" cwms-testcontainers = "1.0.7" oracle-jdbc = "19.3.0.0" @@ -35,6 +36,10 @@ apache-commons-csv = "1.9.0" google-auto-service = "1.0-rc6" freemarker = "2.3.32" +#Overrides +classgraph = { strictly = '4.8.176' } +swagger-parser = { strictly = '2.1.22' } + [libraries] # annotation @@ -92,6 +97,7 @@ junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit-launcher" } testcontainers-base = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers"} @@ -117,10 +123,15 @@ tomcat-jdbc = { module = "org.apache.tomcat:tomcat-jdbc", version.ref = "tomcat" # webjars swagger-ui = { module ="org.webjars:swagger-ui", version.ref = "swagger-ui" } +# Overrides +io-github-classgraph = { module = "io.github.classgraph:classgraph", version.ref = "classgraph"} +io-swagger-parser = { module = "io.swagger.parser.v3:swagger-parser", version.ref = "swagger-parser" } + [bundles] -junit = ["junit-jupiter-api", "junit-jupiter-params", "junit-jupiter-engine"] +junit = ["junit-jupiter-api", "junit-jupiter-params", "junit-jupiter-engine", "junit-platform-launcher"] tomcat-embedded = [ "tomcat-embedded-core", "tomcat-embedded-jasper" ] tomcat-support = [ "tomcat-juli", "tomcat-jdbc" ] testcontainers = [ "testcontainers-base", "testcontainers-database-commons", "testcontainers-jdbc", "testcontainers-junit-jupiter", "testcontainers-cwms"] metrics = ["metrics-core", "metrics-servlets", "metrics-prometheus-client", "metrics-prometheus-servlets" ] -jackson = ["jackson-core", "jackson-dataformat-csv", "jackson-dataformat-xml", "jackson-datatype-jsr310" ] \ No newline at end of file +jackson = ["jackson-core", "jackson-dataformat-csv", "jackson-dataformat-xml", "jackson-datatype-jsr310" ] +overrides = ["io-github.classgraph", "io-swagger-parser"]