From b64b5371004e1327ce475c4c1afcb6d134857eb7 Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Tue, 16 Jan 2024 19:05:08 -0500 Subject: [PATCH 01/14] Add pact consumer library This required upgrading to gradle 7.5 --- build.gradle | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 58694 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 263 ++++++++++++++--------- gradlew.bat | 34 +-- 5 files changed, 173 insertions(+), 127 deletions(-) diff --git a/build.gradle b/build.gradle index 7c0e77daf7..19fba74b62 100644 --- a/build.gradle +++ b/build.gradle @@ -308,6 +308,7 @@ dependencies { testImplementation 'au.com.dius.pact.provider:junit5:4.3.19' testImplementation 'au.com.dius.pact.provider:junit5spring:4.3.19' + testImplementation 'au.com.dius.pact.consumer:junit5:4.6.1' antlr "org.antlr:antlr4:4.8" spotbugs 'com.github.spotbugs:spotbugs:4.2.3' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 490fda8577df6c95960ba7077c43220e5bb2c0d9..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 21909 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 SzbncTHUVZ3=*-w}@BaX!LSYdA delta 19715 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e750102e09..8049c684f0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 2fe81a7d95..a69d9cb6c2 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,78 +17,113 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -105,79 +140,101 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 9109989e3c..53a6b238d4 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,38 +64,26 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 68ee2e17e9f02a36719d8f3defddea9ef318e75a Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Tue, 16 Jan 2024 19:08:03 -0500 Subject: [PATCH 02/14] TPS pact consumer tests - first pass --- build.gradle | 10 ++ .../bio/terra/pact/consumer/TpsPactTest.java | 137 ++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 src/test/java/bio/terra/pact/consumer/TpsPactTest.java diff --git a/build.gradle b/build.gradle index 19fba74b62..3c9c6038f5 100644 --- a/build.gradle +++ b/build.gradle @@ -571,6 +571,16 @@ task testAll(type: Test) { outputs.upToDateWhen { false } } +// PACT + +task pactTests(type: Test) { + useJUnitPlatform { + includeTags "pact-test" + } + environment.put('pact.rootDir', "$buildDir/pacts") + environment.put('pact.provider.version', "$project.version") +} + task verifyPacts(type: Test) { useJUnitPlatform { includeTags 'bio.terra.common.category.Pact' diff --git a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java new file mode 100644 index 0000000000..5b6768a6a4 --- /dev/null +++ b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java @@ -0,0 +1,137 @@ +package bio.terra.pact.consumer; + +import static au.com.dius.pact.consumer.dsl.LambdaDsl.newJsonBody; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import au.com.dius.pact.consumer.MockServer; +import au.com.dius.pact.consumer.dsl.PactDslWithProvider; +import au.com.dius.pact.consumer.junit5.PactConsumerTest; +import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; +import au.com.dius.pact.consumer.junit5.PactTestFor; +import au.com.dius.pact.core.model.PactSpecVersion; +import au.com.dius.pact.core.model.RequestResponsePact; +import au.com.dius.pact.core.model.annotations.Pact; +import bio.terra.app.configuration.PolicyServiceConfiguration; +import bio.terra.policy.api.TpsApi; +import bio.terra.policy.client.ApiException; +import bio.terra.policy.model.TpsComponent; +import bio.terra.policy.model.TpsObjectType; +import bio.terra.policy.model.TpsPaoCreateRequest; +import bio.terra.policy.model.TpsPolicyInput; +import bio.terra.policy.model.TpsPolicyInputs; +import bio.terra.service.policy.PolicyApiService; +import bio.terra.service.policy.PolicyService; +import java.util.Map; +import java.util.UUID; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.test.context.ActiveProfiles; + +@Tag("pact-test") +// @Tag(bio.terra.common.category.Pact.TAG) +@PactConsumerTest +@ActiveProfiles(bio.terra.common.category.Pact.PROFILE) +@ExtendWith(PactConsumerTestExt.class) +@PactTestFor(providerName = "tps", pactVersion = PactSpecVersion.V3) +class TpsPactTest { + + private PolicyServiceConfiguration policyServiceConfiguration; + private PolicyApiService policyApiService; + private final UUID snapshotId = UUID.randomUUID(); + private final TpsPolicyInput protectedDataPolicy = + new TpsPolicyInput() + .namespace(PolicyService.POLICY_NAMESPACE) + .name(PolicyService.PROTECTED_DATA_POLICY_NAME); + private final TpsPolicyInputs policies = new TpsPolicyInputs().addInputsItem(protectedDataPolicy); + + static Map contentTypeJsonHeader = Map.of("Content-Type", "application/json"); + + @Pact(consumer = "datarepo") + RequestResponsePact createPao(PactDslWithProvider builder) { + String snapshotId = UUID.randomUUID().toString(); + return builder + .given("default") + .uponReceiving("create PAO with ID ") + .method("POST") + .path("/api/policy/v1alpha1/pao") + .body( + newJsonBody( + object -> { + object.stringType("objectId", snapshotId); + object.stringType("component", TpsComponent.TDR.getValue()); + object.stringType("objectType", TpsObjectType.SNAPSHOT.getValue()); + object.object( + "attributes", + (attributes) -> + attributes.array( + "inputs", + (inputs) -> + inputs.object( + i -> { + i.stringType( + "namespace", PolicyService.POLICY_NAMESPACE); + i.stringType( + "name", PolicyService.PROTECTED_DATA_POLICY_NAME); + }))); + }) + .build()) + .headers(contentTypeJsonHeader) + .willRespondWith() + .status(204) + .toPact(); + } + + @Pact(consumer = "datarepo") + RequestResponsePact deletePaoThatDoesNotExist(PactDslWithProvider builder) { + // String nonExistentPolicyId = UUID.randomUUID().toString(); + return builder + .given("default") + .uponReceiving("create PAO with ID ") + .method("DELETE") + .path("/api/policy/v1alpha1/pao/" + snapshotId) + .willRespondWith() + .status(404) + .toPact(); + } + + // create snapshot protected data policy - success + @Test + @PactTestFor(pactMethod = "createPao") + void createPaoSuccess(MockServer mockServer) throws ApiException { + // call createPao with the snapshot id + policyServiceConfiguration = new PolicyServiceConfiguration(mockServer.getUrl()); + // when(policyServiceConfiguration.basePath()).thenReturn(mockServer.getUrl()); + policyApiService = new PolicyApiService(policyServiceConfiguration); + TpsApi tps = policyApiService.getPolicyApi(); + tps.createPao( + new TpsPaoCreateRequest() + .objectId(snapshotId) + .component(TpsComponent.TDR) + .objectType(TpsObjectType.SNAPSHOT) + .attributes(policies)); + } + + // create snapshot group policy - success + + // create snapshot policy -- conflict error + + // update existing policy (already protected data, update with group policy) + // state requires policy with this id to already exist + + // delete a policy (exists - requires TPS state to have this policy) + + // delete a policy (does not exist) + @Test + @PactTestFor(pactMethod = "deletePaoThatDoesNotExist") + void deletePaoThatDoesNotExist(MockServer mockServer) { + policyServiceConfiguration = new PolicyServiceConfiguration(mockServer.getUrl()); + // when(policyServiceConfiguration.basePath()).thenReturn(mockServer.getUrl()); + policyApiService = new PolicyApiService(policyServiceConfiguration); + TpsApi tps = policyApiService.getPolicyApi(); + assertThrows( + ApiException.class, + () -> tps.deletePao(snapshotId), + "nonexistent policy should return 404"); + } +} From 938cd9aeb4da8d8c1783c083f7dc847f0be8a757 Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Tue, 16 Jan 2024 19:31:42 -0500 Subject: [PATCH 03/14] Add GH action to publish consumer pact with TPS This action is from terra-workspace-manager: https://github.com/DataBiosphere/terra-workspace-manager/blob/main/.github/workflows/consumer_contract_tests.yml --- .../workflows/consumer_contract_tests.yaml | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 .github/workflows/consumer_contract_tests.yaml diff --git a/.github/workflows/consumer_contract_tests.yaml b/.github/workflows/consumer_contract_tests.yaml new file mode 100644 index 0000000000..68f3faef2e --- /dev/null +++ b/.github/workflows/consumer_contract_tests.yaml @@ -0,0 +1,149 @@ +name: Consumer contract tests +# The purpose of this workflow is to validate the service level contract +# using the Pact framework. +# +# More details on Contract Testing can be found in our handbook +# +# https://broadworkbench.atlassian.net/wiki/spaces/IRT/pages/2660368406/Getting+Started+with+Pact+Contract+Testing +# +# +# +# NOTE: The publish-contracts workflow will use the latest commit of the branch that triggers this workflow to publish the unique consumer contract version to Pact Broker. +on: + pull_request: + branches: [ main, se/DR-3357-tps-consumer-tests ] + paths-ignore: [ '**.md' ] + push: + branches: [ main, se/DR-3357-tps-consumer-tests ] + paths-ignore: [ '**.md' ] + merge_group: + branches: [ main, se/DR-3357-tps-consumer-tests ] + paths-ignore: [ '**.md' ] + +env: + PUBLISH_CONTRACTS_RUN_NAME: 'publish-contracts-${{ github.event.repository.name }}-${{ github.run_id }}-${{ github.run_attempt }}' + CAN_I_DEPLOY_RUN_NAME: 'can-i-deploy-${{ github.event.repository.name }}-${{ github.run_id }}-${{ github.run_attempt }}' + +jobs: + bump-check: + runs-on: ubuntu-latest + outputs: + is-bump: ${{ steps.skiptest.outputs.is-bump }} + steps: + - uses: actions/checkout@v3 + - name: Skip version bump merges + id: skiptest + uses: ./.github/actions/bump-skip + with: + event-name: ${{ github.event_name }} + + # The primary objective of this section is to carefully control the dispatching of tags, + # ensuring it only occurs during the 'Tag, publish, deploy' workflow. + # However, a challenge arises with contract tests, as they require knowledge of the upcoming tag + # before the actual deployment. To address this, we leverage the dry run feature provided by bumper. + # This allows us to obtain the next tag for publishing contracts and verifying consumer pacts without + # triggering the tag dispatch. This approach sidesteps the need for orchestrating multiple workflows, + # simplifying our implementation. + # + # We regulate the tag job to meet the following requirements according to the trigger event type: + # 1. pull_request event (due to opening or updating of PR branch): + # dry-run flag is set to false + # this allows the new semver tag #major.#minor.#patch-#commit to be used to identity pacticipant version for development purpose + # PR has no effect on the value of the latest tag in settings.gradle on disk + # 2. PR merge to main, this triggers a push event on the main branch: + # dry-run flag is set to true + # this allows the new semver tag #major.#minor.#patch to be used to identity pacticipant version, and + # this action will not update the value of the latest tag in settings.gradle on disk + # + # Note: All workflows from the same PR merge should have the same copy of settings.gradle on disk, + # which should be the one from the HEAD of the main branch before the workflow starts running + regulated-tag-job: + needs: [ bump-check ] + if: ${{ needs.bump-check.outputs.is-bump == 'no' }} + uses: ./.github/workflows/tag.yml + with: + # The 'ref' parameter ensures that the consumer version is postfixed with the HEAD commit of the PR branch, + # facilitating cross-referencing of a pact between Pact Broker and GitHub. + ref: ${{ github.head_ref || '' }} + # The 'dry-run' parameter prevents the new tag from being dispatched. + dry-run: true + release-branches: main + secrets: inherit + + init-github-context: + runs-on: ubuntu-latest + needs: [ bump-check ] + if: ${{ needs.bump-check.outputs.is-bump == 'no' }} + outputs: + repo-branch: ${{ steps.extract-branch.outputs.repo-branch }} + repo-version: ${{ steps.extract-branch.outputs.repo-version }} + + steps: + - uses: actions/checkout@v3 + - id: extract-branch + run: | + GITHUB_EVENT_NAME=${{ github.event_name }} + if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then + GITHUB_REF=${{ github.ref }} + GITHUB_SHA=${{ github.sha }} + elif [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then + GITHUB_REF=refs/heads/${{ github.head_ref }} + GITHUB_SHA=${{ github.event.pull_request.head.sha }} + elif [[ "$GITHUB_EVENT_NAME" == "merge_group" ]]; then + GITHUB_REF=refs/heads/${{ github.head_ref }} + else + echo "Failed to extract branch information" + exit 1 + fi + echo "repo-branch=${GITHUB_REF/refs\/heads\//""}" >> $GITHUB_OUTPUT + echo "repo-version=${GITHUB_SHA}" >> $GITHUB_OUTPUT + - name: Echo repo and branch information + run: | + echo "repo-owner=${{ github.repository_owner }}" + echo "repo-name=${{ github.event.repository.name }}" + echo "repo-branch=${{ steps.extract-branch.outputs.repo-branch }}" + echo "repo-version=${{ steps.extract-branch.outputs.repo-version }}" + + tdr-consumer-contract-tests: + runs-on: ubuntu-latest + needs: [ bump-check, init-github-context ] + if: ${{ needs.bump-check.outputs.is-bump == 'no' }} + outputs: + pact-b64: ${{ steps.encode-pact.outputs.pact-b64 }} + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'temurin' + - name: Run consumer tests + run: ./gradlew pactTests + - name: Output consumer contract as non-breaking base64 string + id: encode-pact + run: | + NON_BREAKING_B64=$(cat service/build/pacts/tdr-tps.json | base64 -w 0) + echo "pact-b64=${NON_BREAKING_B64}" >> $GITHUB_OUTPUT + + publish-contracts: + runs-on: ubuntu-latest + needs: [ bump-check, init-github-context, tdr-consumer-contract-tests, regulated-tag-job ] + if: ${{ needs.bump-check.outputs.is-bump == 'no' }} + steps: + - name: Dispatch to terra-github-workflows + uses: broadinstitute/workflow-dispatch@v4.0.0 + with: + run-name: "${{ env.PUBLISH_CONTRACTS_RUN_NAME }}" + workflow: .github/workflows/publish-contracts.yaml + repo: broadinstitute/terra-github-workflows + ref: refs/heads/main + token: ${{ secrets.BROADBOT_TOKEN }} # github token for access to kick off a job in the private repo + inputs: '{ + "run-name": "${{ env.PUBLISH_CONTRACTS_RUN_NAME }}", + "pact-b64": "${{ needs.tdr-consumer-contract-tests.outputs.pact-b64 }}", + "repo-owner": "${{ github.repository_owner }}", + "repo-name": "${{ github.event.repository.name }}", + "repo-branch": "${{ needs.init-github-context.outputs.repo-branch }}", + "release-tag": "${{ needs.regulated-tag-job.outputs.new-tag }}" + }' From 18eb5db134ecac28efb175641ba711b078ff4650 Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Tue, 16 Jan 2024 20:01:01 -0500 Subject: [PATCH 04/14] Change PolicyServiceConfiguration to class This could not be mocked as a record --- .../PolicyServiceConfiguration.java | 15 +++++++++++- .../service/policy/PolicyApiService.java | 2 +- .../bio/terra/pact/consumer/TpsPactTest.java | 24 ++++++++++--------- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/main/java/bio/terra/app/configuration/PolicyServiceConfiguration.java b/src/main/java/bio/terra/app/configuration/PolicyServiceConfiguration.java index 9cca5f6570..1cce404b32 100644 --- a/src/main/java/bio/terra/app/configuration/PolicyServiceConfiguration.java +++ b/src/main/java/bio/terra/app/configuration/PolicyServiceConfiguration.java @@ -5,12 +5,25 @@ import java.io.IOException; import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; /** Configuration for managing connection to Terra Policy Service. * */ +@Configuration +@EnableConfigurationProperties @ConfigurationProperties(prefix = "tps") -public record PolicyServiceConfiguration(String basePath) { +public class PolicyServiceConfiguration { private static final List POLICY_SCOPES = List.of("openid", "email", "profile"); + private String basePath; + + public String getBasePath() { + return basePath; + } + + public void setBasePath(String basePath) { + this.basePath = basePath; + } public String getAccessToken() throws IOException { GoogleCredentials credentials = diff --git a/src/main/java/bio/terra/service/policy/PolicyApiService.java b/src/main/java/bio/terra/service/policy/PolicyApiService.java index b7b22de288..34e92123b3 100644 --- a/src/main/java/bio/terra/service/policy/PolicyApiService.java +++ b/src/main/java/bio/terra/service/policy/PolicyApiService.java @@ -25,7 +25,7 @@ public PolicyApiService(PolicyServiceConfiguration policyServiceConfiguration) { private ApiClient getApiClient() { return new ApiClient() .setHttpClient(sharedHttpClient) - .setBasePath(policyServiceConfiguration.basePath()); + .setBasePath(policyServiceConfiguration.getBasePath()); } private ApiClient getApiClient(String accessToken) { diff --git a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java index 5b6768a6a4..b19a19197a 100644 --- a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java +++ b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java @@ -2,6 +2,8 @@ import static au.com.dius.pact.consumer.dsl.LambdaDsl.newJsonBody; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import au.com.dius.pact.consumer.MockServer; import au.com.dius.pact.consumer.dsl.PactDslWithProvider; @@ -23,6 +25,7 @@ import bio.terra.service.policy.PolicyService; import java.util.Map; import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -36,8 +39,7 @@ @PactTestFor(providerName = "tps", pactVersion = PactSpecVersion.V3) class TpsPactTest { - private PolicyServiceConfiguration policyServiceConfiguration; - private PolicyApiService policyApiService; + private TpsApi tps; private final UUID snapshotId = UUID.randomUUID(); private final TpsPolicyInput protectedDataPolicy = new TpsPolicyInput() @@ -47,6 +49,15 @@ class TpsPactTest { static Map contentTypeJsonHeader = Map.of("Content-Type", "application/json"); + @BeforeEach + void setup(MockServer mockServer) throws Exception { + var tpsConfig = mock(PolicyServiceConfiguration.class); + when(tpsConfig.getAccessToken()).thenReturn("dummyToken"); + when(tpsConfig.getBasePath()).thenReturn(mockServer.getUrl()); + PolicyApiService policyApiService = new PolicyApiService(tpsConfig); + tps = policyApiService.getPolicyApi(); + } + @Pact(consumer = "datarepo") RequestResponsePact createPao(PactDslWithProvider builder) { String snapshotId = UUID.randomUUID().toString(); @@ -84,7 +95,6 @@ RequestResponsePact createPao(PactDslWithProvider builder) { @Pact(consumer = "datarepo") RequestResponsePact deletePaoThatDoesNotExist(PactDslWithProvider builder) { - // String nonExistentPolicyId = UUID.randomUUID().toString(); return builder .given("default") .uponReceiving("create PAO with ID ") @@ -100,10 +110,6 @@ RequestResponsePact deletePaoThatDoesNotExist(PactDslWithProvider builder) { @PactTestFor(pactMethod = "createPao") void createPaoSuccess(MockServer mockServer) throws ApiException { // call createPao with the snapshot id - policyServiceConfiguration = new PolicyServiceConfiguration(mockServer.getUrl()); - // when(policyServiceConfiguration.basePath()).thenReturn(mockServer.getUrl()); - policyApiService = new PolicyApiService(policyServiceConfiguration); - TpsApi tps = policyApiService.getPolicyApi(); tps.createPao( new TpsPaoCreateRequest() .objectId(snapshotId) @@ -125,10 +131,6 @@ void createPaoSuccess(MockServer mockServer) throws ApiException { @Test @PactTestFor(pactMethod = "deletePaoThatDoesNotExist") void deletePaoThatDoesNotExist(MockServer mockServer) { - policyServiceConfiguration = new PolicyServiceConfiguration(mockServer.getUrl()); - // when(policyServiceConfiguration.basePath()).thenReturn(mockServer.getUrl()); - policyApiService = new PolicyApiService(policyServiceConfiguration); - TpsApi tps = policyApiService.getPolicyApi(); assertThrows( ApiException.class, () -> tps.deletePao(snapshotId), From 5a8b0688e165cb2f22ab6bb768b5f59a7344751d Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Tue, 16 Jan 2024 20:09:41 -0500 Subject: [PATCH 05/14] Fix pact JSON file name --- .github/workflows/consumer_contract_tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/consumer_contract_tests.yaml b/.github/workflows/consumer_contract_tests.yaml index 68f3faef2e..c745f61762 100644 --- a/.github/workflows/consumer_contract_tests.yaml +++ b/.github/workflows/consumer_contract_tests.yaml @@ -123,7 +123,7 @@ jobs: - name: Output consumer contract as non-breaking base64 string id: encode-pact run: | - NON_BREAKING_B64=$(cat service/build/pacts/tdr-tps.json | base64 -w 0) + NON_BREAKING_B64=$(cat service/build/pacts/datarepo-tps.json | base64 -w 0) echo "pact-b64=${NON_BREAKING_B64}" >> $GITHUB_OUTPUT publish-contracts: From 8066d7d3b11269763f6498adc424b0d76e6b3893 Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Wed, 17 Jan 2024 12:53:50 -0500 Subject: [PATCH 06/14] Fix contract file location --- .github/workflows/consumer_contract_tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/consumer_contract_tests.yaml b/.github/workflows/consumer_contract_tests.yaml index c745f61762..c2b944419e 100644 --- a/.github/workflows/consumer_contract_tests.yaml +++ b/.github/workflows/consumer_contract_tests.yaml @@ -123,7 +123,7 @@ jobs: - name: Output consumer contract as non-breaking base64 string id: encode-pact run: | - NON_BREAKING_B64=$(cat service/build/pacts/datarepo-tps.json | base64 -w 0) + NON_BREAKING_B64=$(cat build/pacts/datarepo-tps.json | base64 -w 0) echo "pact-b64=${NON_BREAKING_B64}" >> $GITHUB_OUTPUT publish-contracts: From 75542bf4f5540b509e125a5c55df0e4e7c4a94f3 Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Wed, 17 Jan 2024 13:58:12 -0500 Subject: [PATCH 07/14] Fix tps pact test descriptions --- src/test/java/bio/terra/pact/consumer/TpsPactTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java index b19a19197a..9cdb716330 100644 --- a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java +++ b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java @@ -63,7 +63,7 @@ RequestResponsePact createPao(PactDslWithProvider builder) { String snapshotId = UUID.randomUUID().toString(); return builder .given("default") - .uponReceiving("create PAO with ID ") + .uponReceiving("create PAO for TDR snapshot") .method("POST") .path("/api/policy/v1alpha1/pao") .body( @@ -97,7 +97,7 @@ RequestResponsePact createPao(PactDslWithProvider builder) { RequestResponsePact deletePaoThatDoesNotExist(PactDslWithProvider builder) { return builder .given("default") - .uponReceiving("create PAO with ID ") + .uponReceiving("delete non-existent PAO") .method("DELETE") .path("/api/policy/v1alpha1/pao/" + snapshotId) .willRespondWith() From 0ae774d31cd9240c745d29fe40692efbb1467832 Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Thu, 18 Jan 2024 17:05:06 -0500 Subject: [PATCH 08/14] Fix version conflicts - Update pact provider version - Update groovy library - required by pact (will this break the logback config?) --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 3c9c6038f5..1bdd9a927e 100644 --- a/build.gradle +++ b/build.gradle @@ -306,8 +306,8 @@ dependencies { exclude group: 'com.sun.jersey', module: 'jersey-server' } - testImplementation 'au.com.dius.pact.provider:junit5:4.3.19' - testImplementation 'au.com.dius.pact.provider:junit5spring:4.3.19' + testImplementation 'au.com.dius.pact.provider:junit5:4.6.1' + testImplementation 'au.com.dius.pact.provider:junit5spring:4.6.1' testImplementation 'au.com.dius.pact.consumer:junit5:4.6.1' antlr "org.antlr:antlr4:4.8" @@ -315,7 +315,7 @@ dependencies { // Need groovy on the class path for the logback config. Could use XML and skip this dependency, // but the groovy config is... well... groovy. - runtimeOnly group: 'org.codehaus.groovy', name: 'groovy', version: '3.0.7' + runtimeOnly group: 'org.apache.groovy', name: 'groovy', version: '4.0.11' // Findbugs annotations, so we can selectively suppress findbugs findings compileOnly 'com.google.code.findbugs:annotations:3.0.1' From b8cdff3fc7d1499c2e3ea059cc93abf319b1ae00 Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Mon, 22 Jan 2024 12:38:37 -0500 Subject: [PATCH 09/14] Add additional TPS contract tests --- .../bio/terra/pact/consumer/TpsPactTest.java | 193 ++++++++++++++---- 1 file changed, 152 insertions(+), 41 deletions(-) diff --git a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java index 9cdb716330..b4b7daf64d 100644 --- a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java +++ b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java @@ -6,6 +6,7 @@ import static org.mockito.Mockito.when; import au.com.dius.pact.consumer.MockServer; +import au.com.dius.pact.consumer.dsl.DslPart; import au.com.dius.pact.consumer.dsl.PactDslWithProvider; import au.com.dius.pact.consumer.junit5.PactConsumerTest; import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; @@ -19,6 +20,7 @@ import bio.terra.policy.model.TpsComponent; import bio.terra.policy.model.TpsObjectType; import bio.terra.policy.model.TpsPaoCreateRequest; +import bio.terra.policy.model.TpsPaoUpdateRequest; import bio.terra.policy.model.TpsPolicyInput; import bio.terra.policy.model.TpsPolicyInputs; import bio.terra.service.policy.PolicyApiService; @@ -32,23 +34,85 @@ import org.springframework.test.context.ActiveProfiles; @Tag("pact-test") -// @Tag(bio.terra.common.category.Pact.TAG) @PactConsumerTest @ActiveProfiles(bio.terra.common.category.Pact.PROFILE) @ExtendWith(PactConsumerTestExt.class) @PactTestFor(providerName = "tps", pactVersion = PactSpecVersion.V3) class TpsPactTest { - private TpsApi tps; private final UUID snapshotId = UUID.randomUUID(); private final TpsPolicyInput protectedDataPolicy = new TpsPolicyInput() .namespace(PolicyService.POLICY_NAMESPACE) .name(PolicyService.PROTECTED_DATA_POLICY_NAME); - private final TpsPolicyInputs policies = new TpsPolicyInputs().addInputsItem(protectedDataPolicy); + + TpsPaoCreateRequest createPAORequest = + new TpsPaoCreateRequest() + .objectId(snapshotId) + .component(TpsComponent.TDR) + .objectType(TpsObjectType.SNAPSHOT) + .attributes(new TpsPolicyInputs().addInputsItem(protectedDataPolicy)); + + private final String groupName = "testGroup"; + private final TpsPolicyInput groupConstraintPolicy = + PolicyService.getGroupConstraintPolicyInput(groupName); + TpsPaoUpdateRequest updatePAORequest = + new TpsPaoUpdateRequest() + .updateMode(PolicyService.UPDATE_MODE) + .addAttributes(new TpsPolicyInputs().addInputsItem(groupConstraintPolicy)); static Map contentTypeJsonHeader = Map.of("Content-Type", "application/json"); + DslPart createPaoJsonBody = + newJsonBody( + object -> { + object.stringType("objectId", snapshotId.toString()); + object.stringType("component", TpsComponent.TDR.getValue()); + object.stringType("objectType", TpsObjectType.SNAPSHOT.getValue()); + object.object( + "attributes", + (attributes) -> + attributes.array( + "inputs", + (inputs) -> + inputs.object( + i -> { + i.stringType("namespace", PolicyService.POLICY_NAMESPACE); + i.stringType( + "name", PolicyService.PROTECTED_DATA_POLICY_NAME); + }))); + }) + .build(); + + DslPart updatePaoJsonBody = + newJsonBody( + object -> { + object.stringType("updateMode", PolicyService.UPDATE_MODE.getValue()); + object.object( + "addAttributes", + (attributes) -> + attributes.array( + "inputs", + (inputs) -> + inputs.object( + i -> { + i.stringType("namespace", PolicyService.POLICY_NAMESPACE); + i.stringType( + "name", PolicyService.GROUP_CONSTRAINT_POLICY_NAME); + i.array( + "additionalData", + (additionalData) -> + additionalData.object( + data -> { + data.stringType( + "key", + PolicyService.GROUP_CONSTRAINT_KEY_NAME); + data.stringType("value", "testGroup"); + })); + }))); + }) + .build(); + @BeforeEach void setup(MockServer mockServer) throws Exception { var tpsConfig = mock(PolicyServiceConfiguration.class); @@ -60,43 +124,77 @@ void setup(MockServer mockServer) throws Exception { @Pact(consumer = "datarepo") RequestResponsePact createPao(PactDslWithProvider builder) { - String snapshotId = UUID.randomUUID().toString(); return builder - .given("default") + .given("a PAO with this id does not exist") .uponReceiving("create PAO for TDR snapshot") .method("POST") .path("/api/policy/v1alpha1/pao") - .body( - newJsonBody( - object -> { - object.stringType("objectId", snapshotId); - object.stringType("component", TpsComponent.TDR.getValue()); - object.stringType("objectType", TpsObjectType.SNAPSHOT.getValue()); - object.object( - "attributes", - (attributes) -> - attributes.array( - "inputs", - (inputs) -> - inputs.object( - i -> { - i.stringType( - "namespace", PolicyService.POLICY_NAMESPACE); - i.stringType( - "name", PolicyService.PROTECTED_DATA_POLICY_NAME); - }))); - }) - .build()) + .body(createPaoJsonBody) .headers(contentTypeJsonHeader) .willRespondWith() .status(204) .toPact(); } + @Pact(consumer = "datarepo") + RequestResponsePact createPaoConflict(PactDslWithProvider builder) { + return builder + .given("a PAO with this id exists") + .uponReceiving("create PAO for TDR snapshot throws conflict error") + .method("POST") + .path("/api/policy/v1alpha1/pao") + .body(createPaoJsonBody) + .headers(contentTypeJsonHeader) + .willRespondWith() + .status(409) + .toPact(); + } + + @Pact(consumer = "datarepo") + RequestResponsePact updatePao(PactDslWithProvider builder) { + return builder + .given("a PAO with a protected-data policy exists for this snapshot") + .uponReceiving("update snapshot PAO with group constraint policy") + .method("PATCH") + .path("/api/policy/v1alpha1/pao/" + snapshotId) + .body(updatePaoJsonBody) + .headers(contentTypeJsonHeader) + .willRespondWith() + .status(200) + .headers(contentTypeJsonHeader) + .toPact(); + } + + @Pact(consumer = "datarepo") + RequestResponsePact updatePaoConflict(PactDslWithProvider builder) { + return builder + .given("a PAO with a group constraint policy exists for this snapshot") + .uponReceiving("update snapshot PAO with duplicate group constraint policy") + .method("PATCH") + .path("/api/policy/v1alpha1/pao/" + snapshotId) + .body(updatePaoJsonBody) + .headers(contentTypeJsonHeader) + .willRespondWith() + .status(409) + .toPact(); + } + + @Pact(consumer = "datarepo") + RequestResponsePact deletePao(PactDslWithProvider builder) { + return builder + .given("a PAO with this id exists") + .uponReceiving("delete PAO") + .method("DELETE") + .path("/api/policy/v1alpha1/pao/" + snapshotId) + .willRespondWith() + .status(200) + .toPact(); + } + @Pact(consumer = "datarepo") RequestResponsePact deletePaoThatDoesNotExist(PactDslWithProvider builder) { return builder - .given("default") + .given("a PAO with this id does not exist") .uponReceiving("delete non-existent PAO") .method("DELETE") .path("/api/policy/v1alpha1/pao/" + snapshotId) @@ -105,29 +203,42 @@ RequestResponsePact deletePaoThatDoesNotExist(PactDslWithProvider builder) { .toPact(); } - // create snapshot protected data policy - success @Test @PactTestFor(pactMethod = "createPao") void createPaoSuccess(MockServer mockServer) throws ApiException { - // call createPao with the snapshot id - tps.createPao( - new TpsPaoCreateRequest() - .objectId(snapshotId) - .component(TpsComponent.TDR) - .objectType(TpsObjectType.SNAPSHOT) - .attributes(policies)); + tps.createPao(createPAORequest); } - // create snapshot group policy - success + @Test + @PactTestFor(pactMethod = "createPaoConflict") + void createPaoConflictError(MockServer mockServer) throws ApiException { + assertThrows( + ApiException.class, + () -> tps.createPao(createPAORequest), + "creating a policy should return 409 if one already exists"); + } - // create snapshot policy -- conflict error + @Test + @PactTestFor(pactMethod = "updatePao") + void updatePaoSuccess(MockServer mockServer) throws ApiException { + tps.updatePao(updatePAORequest, snapshotId); + } - // update existing policy (already protected data, update with group policy) - // state requires policy with this id to already exist + @Test + @PactTestFor(pactMethod = "updatePaoConflict") + void updatePaoWithDuplicatePolicy(MockServer mockServer) { + assertThrows( + ApiException.class, + () -> tps.updatePao(updatePAORequest, snapshotId), + "updating pao with duplicate policy should return 409"); + } - // delete a policy (exists - requires TPS state to have this policy) + @Test + @PactTestFor(pactMethod = "deletePao") + void deletePaoSuccess(MockServer mockServer) throws ApiException { + tps.deletePao(snapshotId); + } - // delete a policy (does not exist) @Test @PactTestFor(pactMethod = "deletePaoThatDoesNotExist") void deletePaoThatDoesNotExist(MockServer mockServer) { From 6d19567eac31ecccc4e6dc9f64c94f0ca43f4323 Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Mon, 22 Jan 2024 13:07:39 -0500 Subject: [PATCH 10/14] Fix spotbugs error --- .../bio/terra/pact/consumer/TpsPactTest.java | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java index b4b7daf64d..8e372e4f24 100644 --- a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java +++ b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java @@ -39,31 +39,27 @@ @ExtendWith(PactConsumerTestExt.class) @PactTestFor(providerName = "tps", pactVersion = PactSpecVersion.V3) class TpsPactTest { + private static final String groupName = "testGroup"; + private static final Map contentTypeJsonHeader = Map.of("Content-Type", "application/json"); private TpsApi tps; private final UUID snapshotId = UUID.randomUUID(); private final TpsPolicyInput protectedDataPolicy = new TpsPolicyInput() .namespace(PolicyService.POLICY_NAMESPACE) .name(PolicyService.PROTECTED_DATA_POLICY_NAME); - - TpsPaoCreateRequest createPAORequest = + private final TpsPaoCreateRequest createPAORequest = new TpsPaoCreateRequest() .objectId(snapshotId) .component(TpsComponent.TDR) .objectType(TpsObjectType.SNAPSHOT) .attributes(new TpsPolicyInputs().addInputsItem(protectedDataPolicy)); - - private final String groupName = "testGroup"; private final TpsPolicyInput groupConstraintPolicy = PolicyService.getGroupConstraintPolicyInput(groupName); - TpsPaoUpdateRequest updatePAORequest = + private final TpsPaoUpdateRequest updatePAORequest = new TpsPaoUpdateRequest() .updateMode(PolicyService.UPDATE_MODE) .addAttributes(new TpsPolicyInputs().addInputsItem(groupConstraintPolicy)); - - static Map contentTypeJsonHeader = Map.of("Content-Type", "application/json"); - - DslPart createPaoJsonBody = + private final DslPart createPaoJsonBody = newJsonBody( object -> { object.stringType("objectId", snapshotId.toString()); @@ -83,8 +79,7 @@ class TpsPactTest { }))); }) .build(); - - DslPart updatePaoJsonBody = + private final DslPart updatePaoJsonBody = newJsonBody( object -> { object.stringType("updateMode", PolicyService.UPDATE_MODE.getValue()); From 4f3e0d824f4a444efc61950ba36e84825553461a Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Mon, 22 Jan 2024 13:48:42 -0500 Subject: [PATCH 11/14] Spotless fixes --- src/test/java/bio/terra/pact/consumer/TpsPactTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java index 8e372e4f24..ada5b3b045 100644 --- a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java +++ b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java @@ -40,7 +40,8 @@ @PactTestFor(providerName = "tps", pactVersion = PactSpecVersion.V3) class TpsPactTest { private static final String groupName = "testGroup"; - private static final Map contentTypeJsonHeader = Map.of("Content-Type", "application/json"); + private static final Map contentTypeJsonHeader = + Map.of("Content-Type", "application/json"); private TpsApi tps; private final UUID snapshotId = UUID.randomUUID(); private final TpsPolicyInput protectedDataPolicy = From 430096009ba419d4a60d22c4c443a429adcf1378 Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Mon, 22 Jan 2024 14:37:55 -0500 Subject: [PATCH 12/14] Add test to create group constraint policy Also add id and policy name parameters --- .../bio/terra/pact/consumer/TpsPactTest.java | 156 ++++++++++-------- 1 file changed, 91 insertions(+), 65 deletions(-) diff --git a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java index ada5b3b045..1afbc207bb 100644 --- a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java +++ b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java @@ -7,6 +7,7 @@ import au.com.dius.pact.consumer.MockServer; import au.com.dius.pact.consumer.dsl.DslPart; +import au.com.dius.pact.consumer.dsl.LambdaDslObject; import au.com.dius.pact.consumer.dsl.PactDslWithProvider; import au.com.dius.pact.consumer.junit5.PactConsumerTest; import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; @@ -48,38 +49,37 @@ class TpsPactTest { new TpsPolicyInput() .namespace(PolicyService.POLICY_NAMESPACE) .name(PolicyService.PROTECTED_DATA_POLICY_NAME); - private final TpsPaoCreateRequest createPAORequest = - new TpsPaoCreateRequest() - .objectId(snapshotId) - .component(TpsComponent.TDR) - .objectType(TpsObjectType.SNAPSHOT) - .attributes(new TpsPolicyInputs().addInputsItem(protectedDataPolicy)); private final TpsPolicyInput groupConstraintPolicy = PolicyService.getGroupConstraintPolicyInput(groupName); private final TpsPaoUpdateRequest updatePAORequest = new TpsPaoUpdateRequest() .updateMode(PolicyService.UPDATE_MODE) .addAttributes(new TpsPolicyInputs().addInputsItem(groupConstraintPolicy)); - private final DslPart createPaoJsonBody = - newJsonBody( - object -> { - object.stringType("objectId", snapshotId.toString()); - object.stringType("component", TpsComponent.TDR.getValue()); - object.stringType("objectType", TpsObjectType.SNAPSHOT.getValue()); - object.object( - "attributes", - (attributes) -> - attributes.array( - "inputs", - (inputs) -> - inputs.object( - i -> { - i.stringType("namespace", PolicyService.POLICY_NAMESPACE); - i.stringType( - "name", PolicyService.PROTECTED_DATA_POLICY_NAME); - }))); - }) - .build(); + + private final DslPart createProtectedDataPaoJsonBody = + createPaoJsonBody( + i -> { + i.stringType("namespace", PolicyService.POLICY_NAMESPACE); + i.stringType("name", PolicyService.PROTECTED_DATA_POLICY_NAME); + }); + + java.util.function.Consumer groupConstraintPolicyDsl = + i -> { + i.stringType("namespace", PolicyService.POLICY_NAMESPACE); + i.stringType("name", PolicyService.GROUP_CONSTRAINT_POLICY_NAME); + i.array( + "additionalData", + (additionalData) -> + additionalData.object( + data -> { + data.stringType("key", PolicyService.GROUP_CONSTRAINT_KEY_NAME); + data.stringType("value", "testGroup"); + })); + }; + + private final DslPart createGroupConstraintPaoJsonBody = + createPaoJsonBody(groupConstraintPolicyDsl); + private final DslPart updatePaoJsonBody = newJsonBody( object -> { @@ -88,24 +88,7 @@ class TpsPactTest { "addAttributes", (attributes) -> attributes.array( - "inputs", - (inputs) -> - inputs.object( - i -> { - i.stringType("namespace", PolicyService.POLICY_NAMESPACE); - i.stringType( - "name", PolicyService.GROUP_CONSTRAINT_POLICY_NAME); - i.array( - "additionalData", - (additionalData) -> - additionalData.object( - data -> { - data.stringType( - "key", - PolicyService.GROUP_CONSTRAINT_KEY_NAME); - data.stringType("value", "testGroup"); - })); - }))); + "inputs", (inputs) -> inputs.object(groupConstraintPolicyDsl))); }) .build(); @@ -119,27 +102,23 @@ void setup(MockServer mockServer) throws Exception { } @Pact(consumer = "datarepo") - RequestResponsePact createPao(PactDslWithProvider builder) { - return builder - .given("a PAO with this id does not exist") - .uponReceiving("create PAO for TDR snapshot") - .method("POST") - .path("/api/policy/v1alpha1/pao") - .body(createPaoJsonBody) - .headers(contentTypeJsonHeader) - .willRespondWith() - .status(204) - .toPact(); + RequestResponsePact createPaoProtectedData(PactDslWithProvider builder) { + return createPaoDslRequest(builder, createProtectedDataPaoJsonBody); + } + + @Pact(consumer = "datarepo") + RequestResponsePact createPaoGroupConstraint(PactDslWithProvider builder) { + return createPaoDslRequest(builder, createGroupConstraintPaoJsonBody); } @Pact(consumer = "datarepo") RequestResponsePact createPaoConflict(PactDslWithProvider builder) { return builder - .given("a PAO with this id exists") + .given("a PAO with this id exists", Map.of("id", snapshotId.toString())) .uponReceiving("create PAO for TDR snapshot throws conflict error") .method("POST") .path("/api/policy/v1alpha1/pao") - .body(createPaoJsonBody) + .body(createProtectedDataPaoJsonBody) .headers(contentTypeJsonHeader) .willRespondWith() .status(409) @@ -149,7 +128,9 @@ RequestResponsePact createPaoConflict(PactDslWithProvider builder) { @Pact(consumer = "datarepo") RequestResponsePact updatePao(PactDslWithProvider builder) { return builder - .given("a PAO with a protected-data policy exists for this snapshot") + .given( + "a PAO with a protected-data policy exists for this snapshot", + Map.of("id", snapshotId.toString())) .uponReceiving("update snapshot PAO with group constraint policy") .method("PATCH") .path("/api/policy/v1alpha1/pao/" + snapshotId) @@ -164,7 +145,9 @@ RequestResponsePact updatePao(PactDslWithProvider builder) { @Pact(consumer = "datarepo") RequestResponsePact updatePaoConflict(PactDslWithProvider builder) { return builder - .given("a PAO with a group constraint policy exists for this snapshot") + .given( + "a PAO with a group constraint policy exists for this snapshot", + Map.of("id", snapshotId.toString())) .uponReceiving("update snapshot PAO with duplicate group constraint policy") .method("PATCH") .path("/api/policy/v1alpha1/pao/" + snapshotId) @@ -178,7 +161,7 @@ RequestResponsePact updatePaoConflict(PactDslWithProvider builder) { @Pact(consumer = "datarepo") RequestResponsePact deletePao(PactDslWithProvider builder) { return builder - .given("a PAO with this id exists") + .given("a PAO with this id exists", Map.of("id", snapshotId.toString())) .uponReceiving("delete PAO") .method("DELETE") .path("/api/policy/v1alpha1/pao/" + snapshotId) @@ -190,7 +173,7 @@ RequestResponsePact deletePao(PactDslWithProvider builder) { @Pact(consumer = "datarepo") RequestResponsePact deletePaoThatDoesNotExist(PactDslWithProvider builder) { return builder - .given("a PAO with this id does not exist") + .given("a PAO with this id does not exist", Map.of("id", snapshotId.toString())) .uponReceiving("delete non-existent PAO") .method("DELETE") .path("/api/policy/v1alpha1/pao/" + snapshotId) @@ -200,9 +183,15 @@ RequestResponsePact deletePaoThatDoesNotExist(PactDslWithProvider builder) { } @Test - @PactTestFor(pactMethod = "createPao") - void createPaoSuccess(MockServer mockServer) throws ApiException { - tps.createPao(createPAORequest); + @PactTestFor(pactMethod = "createPaoProtectedData") + void createPaoProtectedDataSuccess(MockServer mockServer) throws ApiException { + tps.createPao(createPAORequest(protectedDataPolicy)); + } + + @Test + @PactTestFor(pactMethod = "createPaoGroupConstraint") + void createPaoGroupConstraintSuccess(MockServer mockServer) throws ApiException { + tps.createPao(createPAORequest(groupConstraintPolicy)); } @Test @@ -210,7 +199,7 @@ void createPaoSuccess(MockServer mockServer) throws ApiException { void createPaoConflictError(MockServer mockServer) throws ApiException { assertThrows( ApiException.class, - () -> tps.createPao(createPAORequest), + () -> tps.createPao(createPAORequest(protectedDataPolicy)), "creating a policy should return 409 if one already exists"); } @@ -243,4 +232,41 @@ void deletePaoThatDoesNotExist(MockServer mockServer) { () -> tps.deletePao(snapshotId), "nonexistent policy should return 404"); } + + private DslPart createPaoJsonBody(java.util.function.Consumer policyObject) { + return newJsonBody( + object -> { + object.stringType("objectId", snapshotId.toString()); + object.stringType("component", TpsComponent.TDR.getValue()); + object.stringType("objectType", TpsObjectType.SNAPSHOT.getValue()); + object.object( + "attributes", + (attributes) -> + attributes.array("inputs", (inputs) -> inputs.object(policyObject))); + }) + .build(); + } + + private RequestResponsePact createPaoDslRequest(PactDslWithProvider builder, DslPart body) { + return builder + .given( + "a PAO with this id does not exist", + Map.of("id", snapshotId.toString(), "name", PolicyService.PROTECTED_DATA_POLICY_NAME)) + .uponReceiving("create protected-data PAO for TDR snapshot") + .method("POST") + .path("/api/policy/v1alpha1/pao") + .body(body) + .headers(contentTypeJsonHeader) + .willRespondWith() + .status(204) + .toPact(); + } + + private TpsPaoCreateRequest createPAORequest(TpsPolicyInput policyItem) { + return new TpsPaoCreateRequest() + .objectId(snapshotId) + .component(TpsComponent.TDR) + .objectType(TpsObjectType.SNAPSHOT) + .attributes(new TpsPolicyInputs().addInputsItem(policyItem)); + } } From 317dc35cfce3eb334fbf6c63f8b20a6a282db3df Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Tue, 23 Jan 2024 10:00:09 -0500 Subject: [PATCH 13/14] Consolidate code Replace pact dsl with JSON string --- .../bio/terra/pact/consumer/TpsPactTest.java | 92 ++++++------------- 1 file changed, 30 insertions(+), 62 deletions(-) diff --git a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java index 1afbc207bb..ee581b0a6f 100644 --- a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java +++ b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java @@ -1,13 +1,10 @@ package bio.terra.pact.consumer; -import static au.com.dius.pact.consumer.dsl.LambdaDsl.newJsonBody; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import au.com.dius.pact.consumer.MockServer; -import au.com.dius.pact.consumer.dsl.DslPart; -import au.com.dius.pact.consumer.dsl.LambdaDslObject; import au.com.dius.pact.consumer.dsl.PactDslWithProvider; import au.com.dius.pact.consumer.junit5.PactConsumerTest; import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; @@ -26,6 +23,9 @@ import bio.terra.policy.model.TpsPolicyInputs; import bio.terra.service.policy.PolicyApiService; import bio.terra.service.policy.PolicyService; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Map; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; @@ -56,41 +56,11 @@ class TpsPactTest { .updateMode(PolicyService.UPDATE_MODE) .addAttributes(new TpsPolicyInputs().addInputsItem(groupConstraintPolicy)); - private final DslPart createProtectedDataPaoJsonBody = - createPaoJsonBody( - i -> { - i.stringType("namespace", PolicyService.POLICY_NAMESPACE); - i.stringType("name", PolicyService.PROTECTED_DATA_POLICY_NAME); - }); + private String createPaoProtectedDataJson; + private String createPaoGroupConstraintJson; + private String updatePaoJson; - java.util.function.Consumer groupConstraintPolicyDsl = - i -> { - i.stringType("namespace", PolicyService.POLICY_NAMESPACE); - i.stringType("name", PolicyService.GROUP_CONSTRAINT_POLICY_NAME); - i.array( - "additionalData", - (additionalData) -> - additionalData.object( - data -> { - data.stringType("key", PolicyService.GROUP_CONSTRAINT_KEY_NAME); - data.stringType("value", "testGroup"); - })); - }; - - private final DslPart createGroupConstraintPaoJsonBody = - createPaoJsonBody(groupConstraintPolicyDsl); - - private final DslPart updatePaoJsonBody = - newJsonBody( - object -> { - object.stringType("updateMode", PolicyService.UPDATE_MODE.getValue()); - object.object( - "addAttributes", - (attributes) -> - attributes.array( - "inputs", (inputs) -> inputs.object(groupConstraintPolicyDsl))); - }) - .build(); + ObjectMapper mapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL); @BeforeEach void setup(MockServer mockServer) throws Exception { @@ -102,23 +72,32 @@ void setup(MockServer mockServer) throws Exception { } @Pact(consumer = "datarepo") - RequestResponsePact createPaoProtectedData(PactDslWithProvider builder) { - return createPaoDslRequest(builder, createProtectedDataPaoJsonBody); + RequestResponsePact createPaoProtectedData(PactDslWithProvider builder) + throws JsonProcessingException { + String createPaoProtectedDataJson = + mapper.writeValueAsString(createPAORequest(protectedDataPolicy)); + return createPaoDslRequest(builder, createPaoProtectedDataJson); } @Pact(consumer = "datarepo") - RequestResponsePact createPaoGroupConstraint(PactDslWithProvider builder) { - return createPaoDslRequest(builder, createGroupConstraintPaoJsonBody); + RequestResponsePact createPaoGroupConstraint(PactDslWithProvider builder) + throws JsonProcessingException { + String createPaoGroupConstraintJson = + mapper.writeValueAsString(createPAORequest(groupConstraintPolicy)); + return createPaoDslRequest(builder, createPaoGroupConstraintJson); } @Pact(consumer = "datarepo") - RequestResponsePact createPaoConflict(PactDslWithProvider builder) { + RequestResponsePact createPaoConflict(PactDslWithProvider builder) + throws JsonProcessingException { + String createPaoProtectedDataJson = + mapper.writeValueAsString(createPAORequest(protectedDataPolicy)); return builder .given("a PAO with this id exists", Map.of("id", snapshotId.toString())) .uponReceiving("create PAO for TDR snapshot throws conflict error") .method("POST") .path("/api/policy/v1alpha1/pao") - .body(createProtectedDataPaoJsonBody) + .body(createPaoProtectedDataJson) .headers(contentTypeJsonHeader) .willRespondWith() .status(409) @@ -126,7 +105,8 @@ RequestResponsePact createPaoConflict(PactDslWithProvider builder) { } @Pact(consumer = "datarepo") - RequestResponsePact updatePao(PactDslWithProvider builder) { + RequestResponsePact updatePao(PactDslWithProvider builder) throws JsonProcessingException { + String updatePaoJson = mapper.writeValueAsString(updatePAORequest); return builder .given( "a PAO with a protected-data policy exists for this snapshot", @@ -134,7 +114,7 @@ RequestResponsePact updatePao(PactDslWithProvider builder) { .uponReceiving("update snapshot PAO with group constraint policy") .method("PATCH") .path("/api/policy/v1alpha1/pao/" + snapshotId) - .body(updatePaoJsonBody) + .body(updatePaoJson) .headers(contentTypeJsonHeader) .willRespondWith() .status(200) @@ -143,7 +123,9 @@ RequestResponsePact updatePao(PactDslWithProvider builder) { } @Pact(consumer = "datarepo") - RequestResponsePact updatePaoConflict(PactDslWithProvider builder) { + RequestResponsePact updatePaoConflict(PactDslWithProvider builder) + throws JsonProcessingException { + String updatePaoJson = mapper.writeValueAsString(updatePAORequest); return builder .given( "a PAO with a group constraint policy exists for this snapshot", @@ -151,7 +133,7 @@ RequestResponsePact updatePaoConflict(PactDslWithProvider builder) { .uponReceiving("update snapshot PAO with duplicate group constraint policy") .method("PATCH") .path("/api/policy/v1alpha1/pao/" + snapshotId) - .body(updatePaoJsonBody) + .body(updatePaoJson) .headers(contentTypeJsonHeader) .willRespondWith() .status(409) @@ -233,21 +215,7 @@ void deletePaoThatDoesNotExist(MockServer mockServer) { "nonexistent policy should return 404"); } - private DslPart createPaoJsonBody(java.util.function.Consumer policyObject) { - return newJsonBody( - object -> { - object.stringType("objectId", snapshotId.toString()); - object.stringType("component", TpsComponent.TDR.getValue()); - object.stringType("objectType", TpsObjectType.SNAPSHOT.getValue()); - object.object( - "attributes", - (attributes) -> - attributes.array("inputs", (inputs) -> inputs.object(policyObject))); - }) - .build(); - } - - private RequestResponsePact createPaoDslRequest(PactDslWithProvider builder, DslPart body) { + private RequestResponsePact createPaoDslRequest(PactDslWithProvider builder, String body) { return builder .given( "a PAO with this id does not exist", From 0b7cf3b640b40efb074bae9026acdc2485969252 Mon Sep 17 00:00:00 2001 From: Saman Ehsan Date: Tue, 23 Jan 2024 13:54:00 -0500 Subject: [PATCH 14/14] Get snapshotId from provider state --- .../bio/terra/pact/consumer/TpsPactTest.java | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java index ee581b0a6f..c2bcc47f98 100644 --- a/src/test/java/bio/terra/pact/consumer/TpsPactTest.java +++ b/src/test/java/bio/terra/pact/consumer/TpsPactTest.java @@ -56,10 +56,6 @@ class TpsPactTest { .updateMode(PolicyService.UPDATE_MODE) .addAttributes(new TpsPolicyInputs().addInputsItem(groupConstraintPolicy)); - private String createPaoProtectedDataJson; - private String createPaoGroupConstraintJson; - private String updatePaoJson; - ObjectMapper mapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL); @BeforeEach @@ -108,12 +104,11 @@ RequestResponsePact createPaoConflict(PactDslWithProvider builder) RequestResponsePact updatePao(PactDslWithProvider builder) throws JsonProcessingException { String updatePaoJson = mapper.writeValueAsString(updatePAORequest); return builder - .given( - "a PAO with a protected-data policy exists for this snapshot", - Map.of("id", snapshotId.toString())) + .given("a PAO with a protected-data policy exists for this snapshot") .uponReceiving("update snapshot PAO with group constraint policy") .method("PATCH") - .path("/api/policy/v1alpha1/pao/" + snapshotId) + .pathFromProviderState( + "/api/policy/v1alpha1/pao/${snapshotId}", "/api/policy/v1alpha1/pao/" + snapshotId) .body(updatePaoJson) .headers(contentTypeJsonHeader) .willRespondWith() @@ -127,12 +122,11 @@ RequestResponsePact updatePaoConflict(PactDslWithProvider builder) throws JsonProcessingException { String updatePaoJson = mapper.writeValueAsString(updatePAORequest); return builder - .given( - "a PAO with a group constraint policy exists for this snapshot", - Map.of("id", snapshotId.toString())) + .given("a PAO with a group constraint policy exists for this snapshot") .uponReceiving("update snapshot PAO with duplicate group constraint policy") .method("PATCH") - .path("/api/policy/v1alpha1/pao/" + snapshotId) + .pathFromProviderState( + "/api/policy/v1alpha1/pao/${snapshotId}", "/api/policy/v1alpha1/pao/" + snapshotId) .body(updatePaoJson) .headers(contentTypeJsonHeader) .willRespondWith() @@ -143,10 +137,11 @@ RequestResponsePact updatePaoConflict(PactDslWithProvider builder) @Pact(consumer = "datarepo") RequestResponsePact deletePao(PactDslWithProvider builder) { return builder - .given("a PAO with this id exists", Map.of("id", snapshotId.toString())) + .given("a PAO with this id exists") .uponReceiving("delete PAO") .method("DELETE") - .path("/api/policy/v1alpha1/pao/" + snapshotId) + .pathFromProviderState( + "/api/policy/v1alpha1/pao/${snapshotId}", "/api/policy/v1alpha1/pao/" + snapshotId) .willRespondWith() .status(200) .toPact(); @@ -155,10 +150,11 @@ RequestResponsePact deletePao(PactDslWithProvider builder) { @Pact(consumer = "datarepo") RequestResponsePact deletePaoThatDoesNotExist(PactDslWithProvider builder) { return builder - .given("a PAO with this id does not exist", Map.of("id", snapshotId.toString())) + .given("a PAO with this id does not exist") .uponReceiving("delete non-existent PAO") .method("DELETE") - .path("/api/policy/v1alpha1/pao/" + snapshotId) + .pathFromProviderState( + "/api/policy/v1alpha1/pao/${snapshotId}", "/api/policy/v1alpha1/pao/" + snapshotId) .willRespondWith() .status(404) .toPact();