diff --git a/.dockerignore b/.dockerignore index 7adad677..e420573a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,3 +9,5 @@ roman.iml frontend/node_modules frontend/build *.iml +!backend/target/surefire-reports +!backend/target/test.result \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..42a03abf --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @wireapp/integrations \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94f0971e..8f67a423 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,8 @@ name: CI on: - push: - branches-ignore: - - master - - staging - pull_request: + types: [ opened, synchronize ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -24,6 +20,16 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 + - name: Run tests + run: make docker-run-tests + + - name: Publish test report + uses: EnricoMi/publish-unit-test-result-action/composite@v2.7 + if: always() + with: + files: | + backend/target/reports/TEST-*.xml + - name: Build image id: docker_build uses: docker/build-push-action@v2 @@ -36,10 +42,11 @@ jobs: # Send webhook to Wire using Slack Bot - name: Webhook to Wire - uses: 8398a7/action-slack@v2 + uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} author_name: Docker CI pipeline + fields: repo,message,commit,author,action,eventName,ref,workflow env: SLACK_WEBHOOK_URL: ${{ secrets.WEBHOOK_CI }} # Send message only if previous step failed diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 70c66e3d..22246d73 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -185,10 +185,11 @@ jobs: # Send webhook to Wire using Slack Bot - name: Webhook to Wire - uses: 8398a7/action-slack@v2 + uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} author_name: ${{ env.SERVICE_NAME }} Quay Production Publish + fields: repo,message,commit,author,action,eventName,ref,workflow env: SLACK_WEBHOOK_URL: ${{ secrets.WEBHOOK_RELEASE }} # Send message only if previous step failed diff --git a/.github/workflows/quay.yml b/.github/workflows/quay.yml index 65891433..0fa5ee74 100644 --- a/.github/workflows/quay.yml +++ b/.github/workflows/quay.yml @@ -46,10 +46,11 @@ jobs: release_version=${{ github.event.inputs.tag }} # Send webhook to Wire using Slack Bot - name: Webhook to Wire - uses: 8398a7/action-slack@v2 + uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} author_name: Roman Quay Custom Tag Pipeline + fields: repo,message,commit,author,action,eventName,ref,workflow env: SLACK_WEBHOOK_URL: ${{ secrets.WEBHOOK_CI }} # Send message only if previous step failed diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 871bdf39..4234afd4 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -166,10 +166,11 @@ jobs: # Send webhook to Wire using Slack Bot - name: Webhook to Wire - uses: 8398a7/action-slack@v2 + uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} author_name: ${{ env.SERVICE_NAME }} Quay Staging Publish + fields: repo,message,commit,author,action,eventName,ref,workflow env: SLACK_WEBHOOK_URL: ${{ secrets.WEBHOOK_RELEASE }} # Send message only if previous step failed diff --git a/Dockerfile.UnitTests b/Dockerfile.UnitTests new file mode 100644 index 00000000..6fb0d3dc --- /dev/null +++ b/Dockerfile.UnitTests @@ -0,0 +1,12 @@ +FROM --platform=linux/x86_64 wirebot/cryptobox:1.4.0 AS test-stage +WORKDIR /app + +COPY . ./ +WORKDIR /app/backend + +# in case of error, write test output status code to /tmp/test.result and exit with 0 for later stages checks. +RUN echo "0" > /tmp/test.result +RUN ./mvnw test -Dmaven.test.skip=false 2>&1 || echo $? > /tmp/test.result || echo "Tests failed" + +FROM scratch AS export-stage +COPY --from=test-stage /app/backend/target/surefire-reports/TEST-*.xml /tmp/test.result / \ No newline at end of file diff --git a/Makefile b/Makefile index 37741581..e833eb43 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,6 @@ +docker-run-tests: + ./test.sh + db: docker-compose up -d db diff --git a/README.md b/README.md index 153035f3..b1be2beb 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ result. In order to receive events via _Websocket_ connect to: ``` -wss://proxy.services.wire.com/api/await/`` +wss://proxy.services.wire.com/await/`` ``` ### Events that are sent as HTTP `POST` to your endpoint (Webhook or Websocket) @@ -334,7 +334,7 @@ or Full description: https://proxy.services.wire.com/swagger#!/default/post -**Note:** `token` that comes with `conversation.init` events is _lifelong_. It should be stored for later usage. `token` +**Note:** `token` that comes with `conversation.bot_request` events is _lifelong_. It should be stored for later usage. `token` that comes with other event types has lifespan of 20 seconds. ### Bot Examples @@ -353,7 +353,7 @@ The best way how to run Roman is to use Docker, another option is to run the Rom - In order to actually being able to connect to the Wire backend, Roman's endpoints needs to run on HTTPS. - You need a PostgreSQL instance with an empty database and credentials. - In order to run it as a Docker container, you need to have Docker installed. -- In order to run it natively on JVM, you need to have JVM 11 installed + all necessary libraries +- In order to run it natively on JVM, you need to have JVM 17 installed + all necessary libraries for [Cryptobox4j](https://github.com/wireapp/cryptobox4j). ### Configuration @@ -455,7 +455,7 @@ env variables. See [Configuration section](#configuration) how to obtain them. As previously mentioned, Wire recommends running the Roman as a docker container. However, you can run it natively on the JVM as well. -Please note that Roman requires JVM >= 11. To run it natively, one needs to +Please note that Roman requires JVM >= 17. To run it natively, one needs to install [Cryptobox4j](https://github.com/wireapp/cryptobox4j) and other cryptographic libraries. You can use [Docker Build Image](https://github.com/wireapp/cryptobox4j/blob/master/dockerfiles/Dockerfile.cryptobox) @@ -464,17 +464,17 @@ as an inspiration what needs to be installed and what environment variables need Also, don't forget to read the [Configuration section](#configuration) and set all necessary environment variables for the Roman itselgf. -First, it is necessary to build the application: +First, it is necessary to build the application under `backend` directory: ```bash -# Maven and JVM 11 is required -mvn package -DskipTests +# Maven and JVM 17 is required +./mvnw package -DskipTests ``` Then to run it like that: ```bash -# JVM 11 required +# JVM 17 required java -jar target/roman.jar server roman.yaml ``` @@ -557,6 +557,20 @@ docker-compose -f docker-compose.prod.yml --env-file .env.prod up -d 10. All set! You can go to `https://roman.example.com/swagger` and start using Roman. +## Running Roman tests suite + +Tests run with the help of docker containers to setup all requirements. +To run the test suite just run: + +```bash +make docker-run-tests +``` + +or directly: +```bash +./test.sh +``` + ## Comprehensive tutorial how to onboard new bot Step-by-step guide, how to create a simple bot for Roman - [onboarding.md](docs/onboarding.md). diff --git a/backend/.mvn/wrapper/maven-wrapper.properties b/backend/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..642d572c --- /dev/null +++ b/backend/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/backend/mvnw b/backend/mvnw new file mode 100755 index 00000000..a16b5431 --- /dev/null +++ b/backend/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + 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 + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd new file mode 100755 index 00000000..c8d43372 --- /dev/null +++ b/backend/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/backend/pom.xml b/backend/pom.xml index 3bfcc69c..0aad5c25 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -6,7 +6,7 @@ com.wire.bots roman - 1.19.0 + 1.19.3 Roman Wire Bot API Proxy @@ -28,8 +28,8 @@ true - 3.6.0 - 4.0.0 + 3.6.4 + 4.0.7 0.11.5 5.8.2 11.0.18 @@ -67,10 +67,18 @@ io.dropwizard dropwizard-servlets + + io.dropwizard + dropwizard-jdbi3 + + + io.dropwizard + dropwizard-client + com.smoketurner dropwizard-swagger - 4.0.0-1 + 4.0.5-1 @@ -102,12 +110,12 @@ com.fasterxml.jackson.core jackson-databind - 2.15.1 + 2.17.1 jakarta.xml.bind jakarta.xml.bind-api - 4.0.1 + 4.0.2 io.jsonwebtoken @@ -178,6 +186,21 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.2 + + + org.apache.maven.surefire + surefire-junit-platform + 3.2.2 + + + + -Djava.library.path=/wire/cryptobox/dist/lib + + org.apache.maven.plugins maven-compiler-plugin diff --git a/backend/roman.yaml b/backend/roman.yaml index fe24d4b3..e585d373 100644 --- a/backend/roman.yaml +++ b/backend/roman.yaml @@ -22,7 +22,7 @@ logging: "com.wire.bots.logger": ${LOG_LEVEL:-INFO} swagger: - # make sure that this settings is the same as "server.rootPath" + # make sure that these settings is the same as "server.rootPath" uriPrefix: /api title: Roman Swagger description: Roman - Wire Bots Proxy @@ -42,10 +42,10 @@ jerseyClient: retries: 3 userAgent: roman tls: - protocol: TLSv1.2 + protocol: TLSv1.3 provider: SunJSSE supportedProtocols: - - TLSv1.1 + - TLSv1.3 - TLSv1.2 database: @@ -71,3 +71,5 @@ romanPubKeyBase64: ${ROMAN_PUB_KEY_BASE64:-} # optional enabling of CORS - in format a.domain.com,another.domain.com allowedCors: ${ALLOWED_CORS} + +healthchecks: false diff --git a/backend/src/main/java/com/wire/bots/roman/ImageProcessor.java b/backend/src/main/java/com/wire/bots/roman/ImageProcessor.java index 1888373a..6771dce4 100644 --- a/backend/src/main/java/com/wire/bots/roman/ImageProcessor.java +++ b/backend/src/main/java/com/wire/bots/roman/ImageProcessor.java @@ -1,29 +1,30 @@ package com.wire.bots.roman; import com.wire.bots.roman.resources.Picture; -import com.wire.xenon.assets.ImagePreview; +import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; public class ImageProcessor { - private static final int MEDIUM_DIMENSION = 2896; public static Picture getMediumImage(Picture picture) throws Exception { - return getScaledImage(picture, MEDIUM_DIMENSION); + return getScaledImage(picture); } - private static Boolean shouldScaleOriginalSize(double width, double height, double dimension) { - final double maxPixelCount = 1.3 * dimension * dimension; - return (width > 1.3 * dimension || height > 1.3 * dimension) + private static Boolean shouldScaleOriginalSize(double width, double height) { + final double maxPixelCount = 1.3 * (double) ImageProcessor.MEDIUM_DIMENSION * (double) ImageProcessor.MEDIUM_DIMENSION; + return (width > 1.3 * (double) ImageProcessor.MEDIUM_DIMENSION || height > 1.3 * (double) ImageProcessor.MEDIUM_DIMENSION) && width * height > maxPixelCount; } - private static Size getScaledSize(double origWidth, double origHeight, double dimension) { + private static Size getScaledSize(double origWidth, double origHeight) { Size ret = new Size(); - double op1 = Math.min(dimension / origWidth, dimension / origHeight); - double op2 = dimension / Math.sqrt(origWidth * origHeight); + double op1 = Math.min((double) ImageProcessor.MEDIUM_DIMENSION / origWidth, (double) ImageProcessor.MEDIUM_DIMENSION / origHeight); + double op2 = (double) ImageProcessor.MEDIUM_DIMENSION / Math.sqrt(origWidth * origHeight); double scale = Math.max(op1, op2); double width = Math.ceil(scale * origWidth); ret.width = width; @@ -52,37 +53,29 @@ private static BufferedImage resizeImage(BufferedImage originalImage, return resizedImage; } - // todo. fixme, xenon to expose getMimeType type and getImageData - private static Picture getScaledImage(ImagePreview image, int dimension) throws Exception { -// String resultImageType; -// switch ("image/jpeg") { -// case "image/jpeg": -// resultImageType = "jpg"; -// break; -// case "image/png": -// resultImageType = "png"; -// break; -// default: -// throw new IllegalArgumentException("Unsupported mime type"); -// } -// -// int origWidth = image.getWidth(); -// int origHeight = image.getHeight(); -// -// BufferedImage resultImage = ImageIO.read(new ByteArrayInputStream(image.getImageData())); -// -// if (shouldScaleOriginalSize(origWidth, origHeight, dimension)) { -// Size scaledSize = getScaledSize(origWidth, origHeight, dimension); -// resultImage = resizeImage(resultImage, (int) scaledSize.width, -// (int) scaledSize.height); -// } -// -// try (ByteArrayOutputStream resultStream = new ByteArrayOutputStream()) { -// ImageIO.write(resultImage, resultImageType, resultStream); -// resultStream.flush(); -// return new Picture(resultStream.toByteArray(), image.getMimeType()); -// } - return null; + private static Picture getScaledImage(Picture image) throws Exception { + String resultImageType = switch (image.getMimeType()) { + case "image/jpeg" -> "jpg"; + case "image/png" -> "png"; + default -> throw new IllegalArgumentException("Unsupported mime type"); + }; + + int origWidth = image.getWidth(); + int origHeight = image.getHeight(); + + BufferedImage resultImage = ImageIO.read(new ByteArrayInputStream(image.getImageData())); + + if (shouldScaleOriginalSize(origWidth, origHeight)) { + Size scaledSize = getScaledSize(origWidth, origHeight); + resultImage = resizeImage(resultImage, (int) scaledSize.width, + (int) scaledSize.height); + } + + try (ByteArrayOutputStream resultStream = new ByteArrayOutputStream()) { + ImageIO.write(resultImage, resultImageType, resultStream); + resultStream.flush(); + return new Picture(resultStream.toByteArray(), image.getMimeType()); + } } private static class Size { diff --git a/backend/src/main/java/com/wire/bots/roman/MessageHandler.java b/backend/src/main/java/com/wire/bots/roman/MessageHandler.java index 6027f9e7..34421c92 100644 --- a/backend/src/main/java/com/wire/bots/roman/MessageHandler.java +++ b/backend/src/main/java/com/wire/bots/roman/MessageHandler.java @@ -381,6 +381,7 @@ public void onBotRemoved(UUID botId, SystemMessage msg) { OutgoingMessage message = new OutgoingMessage(); message.botId = botId; message.type = "conversation.bot_removed"; + message.conversationId = msg.conversation.id; if (!send(message)) Logger.info("onBotRemoved: failed to deliver message"); diff --git a/backend/src/main/java/com/wire/bots/roman/ProviderClient.java b/backend/src/main/java/com/wire/bots/roman/ProviderClient.java index 5e5336fc..d36e91a1 100644 --- a/backend/src/main/java/com/wire/bots/roman/ProviderClient.java +++ b/backend/src/main/java/com/wire/bots/roman/ProviderClient.java @@ -15,6 +15,7 @@ import org.glassfish.jersey.logging.LoggingFeature; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.UUID; @@ -160,7 +161,7 @@ public Response updateServiceURL(NewCookie zprovider, UUID serviceId, String pas .put(Entity.entity(updateService, MediaType.APPLICATION_JSON)); } - public String uploadProfilePicture(Cookie cookie, byte[] image, String mimeType) throws Exception { + public AssetKey uploadProfilePicture(Cookie cookie, byte[] image, String mimeType) throws Exception { final boolean isPublic = true; final String retention = "eternal"; String strMetadata = String.format("{\"public\": %s, \"retention\": \"%s\"}", isPublic, retention); @@ -193,21 +194,21 @@ public String uploadProfilePicture(Cookie cookie, byte[] image, String mimeType) os.write(image); os.write("\r\n--frontier--\r\n".getBytes(StandardCharsets.UTF_8)); - Response response = providerTarget + try (Response response = providerTarget .path("provider") .path("assets") - .request(MediaType.APPLICATION_JSON_TYPE) + .request(MediaType.APPLICATION_JSON) .cookie(cookie) - .post(Entity.entity(os.toByteArray(), "multipart/mixed; boundary=frontier")); - - if (response.getStatus() >= 400) { - Logger.warning(response.readEntity(String.class)); - return null; - } + .post(Entity.entity(os.toByteArray(), "multipart/mixed; boundary=frontier"))) { - AssetKey assetKey = response.readEntity(AssetKey.class); + if (response.getStatus() >= 400) { + String msg = response.readEntity(String.class); + Logger.warning("Error uploading asset: %s, status: %d", msg, response.getStatus()); + throw new IOException(response.getStatusInfo().getReasonPhrase()); + } - return assetKey.id; + return response.readEntity(AssetKey.class); + } } @JsonInclude(JsonInclude.Include.NON_NULL) diff --git a/backend/src/main/java/com/wire/bots/roman/Sender.java b/backend/src/main/java/com/wire/bots/roman/Sender.java index 4622a895..6640e07c 100644 --- a/backend/src/main/java/com/wire/bots/roman/Sender.java +++ b/backend/src/main/java/com/wire/bots/roman/Sender.java @@ -8,7 +8,17 @@ import com.wire.bots.roman.model.Mention; import com.wire.lithium.ClientRepo; import com.wire.xenon.WireClient; -import com.wire.xenon.assets.*; +import com.wire.xenon.assets.AssetBase; +import com.wire.xenon.assets.AudioAsset; +import com.wire.xenon.assets.AudioPreview; +import com.wire.xenon.assets.ButtonActionConfirmation; +import com.wire.xenon.assets.Calling; +import com.wire.xenon.assets.FileAsset; +import com.wire.xenon.assets.FileAssetPreview; +import com.wire.xenon.assets.ImageAsset; +import com.wire.xenon.assets.ImagePreview; +import com.wire.xenon.assets.MessageText; +import com.wire.xenon.assets.Poll; import com.wire.xenon.backend.models.Conversation; import com.wire.xenon.exceptions.MissingStateException; import com.wire.xenon.models.AssetKey; @@ -230,6 +240,7 @@ private void setAssetMetadata(AssetBase asset, AssetMeta meta) { asset.setAssetToken(meta.assetToken); asset.setSha256(Base64.getDecoder().decode(meta.sha256)); asset.setOtrKey(Base64.getDecoder().decode(meta.otrKey)); + asset.setDomain(meta.domain); } private AssetKey uploadAssetData(WireClient wireClient, AssetBase asset) throws Exception { diff --git a/backend/src/main/java/com/wire/bots/roman/model/AssetMeta.java b/backend/src/main/java/com/wire/bots/roman/model/AssetMeta.java index 569002a4..fe353702 100644 --- a/backend/src/main/java/com/wire/bots/roman/model/AssetMeta.java +++ b/backend/src/main/java/com/wire/bots/roman/model/AssetMeta.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import jakarta.validation.constraints.NotNull; @JsonIgnoreProperties(ignoreUnknown = true) @@ -21,4 +20,8 @@ public class AssetMeta { @JsonProperty @NotNull public String otrKey; + + @JsonProperty + @NotNull + public String domain; } diff --git a/backend/src/main/java/com/wire/bots/roman/resources/BroadcastResource.java b/backend/src/main/java/com/wire/bots/roman/resources/BroadcastResource.java index a93b387d..fc1afddd 100644 --- a/backend/src/main/java/com/wire/bots/roman/resources/BroadcastResource.java +++ b/backend/src/main/java/com/wire/bots/roman/resources/BroadcastResource.java @@ -34,6 +34,7 @@ @Api @Path("/broadcast") @Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) public class BroadcastResource { private final Sender sender; private final BotsDAO botsDAO; diff --git a/backend/src/main/java/com/wire/bots/roman/resources/ConversationResource.java b/backend/src/main/java/com/wire/bots/roman/resources/ConversationResource.java index 6398be9d..58b6c06f 100644 --- a/backend/src/main/java/com/wire/bots/roman/resources/ConversationResource.java +++ b/backend/src/main/java/com/wire/bots/roman/resources/ConversationResource.java @@ -13,10 +13,7 @@ import io.swagger.annotations.*; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; +import jakarta.ws.rs.*; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; @@ -30,6 +27,7 @@ @Api @Path("/conversation") @Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) public class ConversationResource { private final Sender sender; diff --git a/backend/src/main/java/com/wire/bots/roman/resources/Picture.java b/backend/src/main/java/com/wire/bots/roman/resources/Picture.java index 475191be..33eb2b51 100644 --- a/backend/src/main/java/com/wire/bots/roman/resources/Picture.java +++ b/backend/src/main/java/com/wire/bots/roman/resources/Picture.java @@ -8,6 +8,7 @@ public class Picture extends ImagePreview { private boolean aPublic; private final byte[] imageData; + private final String mimeType; private int width; private int height; private String retention; @@ -16,6 +17,7 @@ public Picture(byte[] image, String mimeType) { super(UUID.randomUUID(), mimeType); this.imageData = image; + this.mimeType = mimeType; } public void setPublic(boolean aPublic) { @@ -45,4 +47,8 @@ public int getHeight() { public void setHeight(int height) { this.height = height; } + + public String getMimeType() { + return mimeType; + } } diff --git a/backend/src/main/java/com/wire/bots/roman/resources/ProviderResource.java b/backend/src/main/java/com/wire/bots/roman/resources/ProviderResource.java index 93a69d32..ecedead7 100644 --- a/backend/src/main/java/com/wire/bots/roman/resources/ProviderResource.java +++ b/backend/src/main/java/com/wire/bots/roman/resources/ProviderResource.java @@ -12,22 +12,23 @@ import com.wire.xenon.tools.Logger; import io.dropwizard.validation.ValidationMethod; import io.swagger.annotations.*; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; -import org.hibernate.validator.constraints.Length; -import org.jdbi.v3.core.Jdbi; - -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.hibernate.validator.constraints.Length; +import org.jdbi.v3.core.Jdbi; import static com.wire.bots.roman.Tools.generateToken; @Api @Path("/") @Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) public class ProviderResource { private final ProviderClient providerClient; diff --git a/backend/src/main/java/com/wire/bots/roman/resources/ServiceResource.java b/backend/src/main/java/com/wire/bots/roman/resources/ServiceResource.java index bb4e72d0..eb4c9804 100644 --- a/backend/src/main/java/com/wire/bots/roman/resources/ServiceResource.java +++ b/backend/src/main/java/com/wire/bots/roman/resources/ServiceResource.java @@ -11,6 +11,7 @@ import com.wire.bots.roman.model.Provider; import com.wire.bots.roman.model.Service; import com.wire.xenon.backend.models.ErrorMessage; +import com.wire.xenon.models.AssetKey; import com.wire.xenon.tools.Logger; import io.dropwizard.validation.ValidationMethod; import io.swagger.annotations.*; @@ -39,6 +40,7 @@ @Api @Path("/service") @Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) public class ServiceResource { private static final String PROFILE_KEY = "3-1-c9262f6f-892f-40d5-9349-fbeb62c8aba4"; @@ -98,9 +100,9 @@ public Response create(@ApiParam(hidden = true) @CookieParam(Z_ROMAN) String tok if (image != null) { String mimeType = "image/png"; Picture mediumImage = ImageProcessor.getMediumImage(new Picture(image, mimeType)); - String key = providerClient.uploadProfilePicture(cookie, mediumImage.getImageData(), mimeType); - service.assets.get(0).key = key; - service.assets.get(1).key = key; + AssetKey assetKey = providerClient.uploadProfilePicture(cookie, mediumImage.getImageData(), mimeType); + service.assets.get(0).key = assetKey.id; + service.assets.get(1).key = assetKey.id; } } } @@ -213,8 +215,8 @@ public Response update(@Context ContainerRequestContext context, byte[] image = Base64.getDecoder().decode(payload.avatar); String mimeType = "image/jpeg"; Picture mediumImage = ImageProcessor.getMediumImage(new Picture(image, mimeType)); - String key = providerClient.uploadProfilePicture(cookie, mediumImage.getImageData(), mimeType); - providerClient.updateServiceAvatar(cookie, provider.serviceId, key); + AssetKey assetKey = providerClient.uploadProfilePicture(cookie, mediumImage.getImageData(), mimeType); + providerClient.updateServiceAvatar(cookie, provider.serviceId, assetKey.id); } provider = providersDAO.get(providerId); diff --git a/backend/src/main/java/com/wire/bots/roman/resources/UsersResource.java b/backend/src/main/java/com/wire/bots/roman/resources/UsersResource.java index 3431e413..04783f05 100644 --- a/backend/src/main/java/com/wire/bots/roman/resources/UsersResource.java +++ b/backend/src/main/java/com/wire/bots/roman/resources/UsersResource.java @@ -9,20 +9,18 @@ import com.wire.xenon.backend.models.User; import com.wire.xenon.tools.Logger; import io.swagger.annotations.*; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; +import jakarta.ws.rs.*; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; + import java.util.UUID; @Api @Path("/users") @Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) public class UsersResource { private final ClientRepo repo; diff --git a/backend/src/test/java/com/wire/bots/roman/integrations/BroadcastResourceTest.java b/backend/src/test/java/com/wire/bots/roman/integrations/BroadcastResourceTest.java index 800aacd1..79291cb1 100644 --- a/backend/src/test/java/com/wire/bots/roman/integrations/BroadcastResourceTest.java +++ b/backend/src/test/java/com/wire/bots/roman/integrations/BroadcastResourceTest.java @@ -4,7 +4,11 @@ import com.wire.bots.roman.Const; import com.wire.bots.roman.DAO.ProvidersDAO; import com.wire.bots.roman.Tools; -import com.wire.bots.roman.model.*; +import com.wire.bots.roman.model.AssetMeta; +import com.wire.bots.roman.model.Attachment; +import com.wire.bots.roman.model.Config; +import com.wire.bots.roman.model.IncomingMessage; +import com.wire.bots.roman.model.Report; import com.wire.lithium.models.NewBotResponseModel; import com.wire.xenon.backend.models.Conversation; import com.wire.xenon.backend.models.NewBot; @@ -19,6 +23,7 @@ import org.jdbi.v3.core.Jdbi; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -29,21 +34,16 @@ import java.util.Random; import java.util.UUID; -import static io.dropwizard.testing.ConfigOverride.config; +import static com.wire.bots.roman.resources.dummies.Const.ROMAN_TEST_CONFIG; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(DropwizardExtensionsSupport.class) public class BroadcastResourceTest { private static final String BOT_CLIENT_DUMMY = "bot_client_dummy"; - private static final String CONFIG = "roman-test.yml"; @TempDir static Path tempDir; static final DropwizardAppExtension SUPPORT = new DropwizardAppExtension<>( - Application.class, CONFIG, - new ResourceConfigurationSourceProvider(), - config("database.url", () -> "jdbc:h2:" + tempDir.resolve("database.h2")), - config("key", "TcZA2Kq4GaOcIbQuOvasrw34321cZAfLW4Ga54fsds43hUuOdcdm42"), - config("apiHost", "http://localhost:8090")); + Application.class, ROMAN_TEST_CONFIG, new ResourceConfigurationSourceProvider()); private Client client; private Jdbi jdbi; @@ -61,6 +61,7 @@ public void afterClass() { } @Test + @Disabled("Fix when broadcast is testable") public void broadcastTest() throws InterruptedException { final Random random = new Random(); final UUID botId = UUID.randomUUID(); @@ -159,4 +160,4 @@ private NewBotResponseModel newBotFromBE(UUID botId, UUID userId, UUID convId, S return res.readEntity(NewBotResponseModel.class); } -} +} \ No newline at end of file diff --git a/backend/src/test/java/com/wire/bots/roman/integrations/DatabaseTest.java b/backend/src/test/java/com/wire/bots/roman/integrations/DatabaseTest.java index eacbccb5..2c59191c 100644 --- a/backend/src/test/java/com/wire/bots/roman/integrations/DatabaseTest.java +++ b/backend/src/test/java/com/wire/bots/roman/integrations/DatabaseTest.java @@ -1,179 +1,174 @@ -//package com.wire.bots.roman.integrations; -// -//import com.fasterxml.jackson.core.JsonProcessingException; -//import com.fasterxml.jackson.databind.ObjectMapper; -//import com.wire.bots.roman.Application; -//import com.wire.bots.roman.DAO.BroadcastDAO; -//import com.wire.bots.roman.DAO.OutgoingMessageDAO; -//import com.wire.bots.roman.DAO.ProvidersDAO; -//import com.wire.bots.roman.model.Attachment; -//import com.wire.bots.roman.model.Config; -//import com.wire.bots.roman.model.OutgoingMessage; -//import com.wire.bots.roman.model.Provider; -//import io.dropwizard.testing.ConfigOverride; -//import io.dropwizard.testing.DropwizardTestSupport; -//import org.jdbi.v3.core.Jdbi; -//import org.junit.After; -//import org.junit.Before; -//import org.junit.Test; -// -//import java.util.List; -//import java.util.UUID; -// -//import static org.assertj.core.api.Assertions.assertThat; -// -//public class DatabaseTest { -// private static final DropwizardTestSupport SUPPORT = new DropwizardTestSupport<>( -// Application.class, "roman.yaml", -// ConfigOverride.config("key", "TcZA2Kq4GaOcIbQuOvasrw34321cZAfLW4Ga54fsds43hUuOdcdm42"), -// ConfigOverride.config("romanPubKeyBase64", "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3xtHqyZPlb0lxlnP0rNA\n" + -// "JVmAjB1Tenl11brkkKihcJNRAYrnrT/6sPX4u2lVn/aPncUTjN8omL47MBct7qYV\n" + -// "1VY4a5beOyNiVL0ZjZMuh07aL9Z2A4cu67tKZrCoGttn3jpSVlqoOtwEgW+Tpgpm\n" + -// "KojcRC4DDXEZTEvRoi0RLzAyWCH/8hwWzXR7J082zmn0Ur211QVbOJN/62PAIWyj\n" + -// "l5bLglp00AY5OnBHgRNwwRkBJIJLwgNm8u9+0ZplqmMGd3C/QFNngCOeRvFe+5g4\n" + -// "qfO4/FOlbkM2kYFAi5KUowfG7cdMQELI+fe4v7yNsgrbMKhnIiLtDIU4wiQIRjbr\n" + -// "ZwIDAQAB")); -// private Jdbi jdbi; -// -// @Before -// public void beforeClass() throws Exception { -// SUPPORT.before(); -// Application app = SUPPORT.getApplication(); -// jdbi = app.getJdbi(); -// } -// -// @After -// public void afterClass() { -// SUPPORT.after(); -// } -// -// -// @Test -// public void testProviderDAO() { -// final ProvidersDAO providersDAO = jdbi.onDemand(ProvidersDAO.class); -// -// final UUID providerId = UUID.randomUUID(); -// final String name = "name"; -// final String email = "email@wire.com"; -// final String hash = "hash"; -// final String password = "password"; -// final int insert = providersDAO.insert(name, providerId, email, hash, password); -// assertThat(insert).isEqualTo(1); -// -// Provider provider = providersDAO.get(providerId); -// assertThat(provider).isNotNull(); -// assertThat(provider.name).isEqualTo(name); -// assertThat(provider.hash).isEqualTo(hash); -// assertThat(provider.password).isEqualTo(password); -// assertThat(provider.id).isEqualTo(providerId); -// assertThat(provider.email).isEqualTo(email); -// -// provider = providersDAO.get(email); -// assertThat(provider).isNotNull(); -// assertThat(provider.name).isEqualTo(name); -// assertThat(provider.hash).isEqualTo(hash); -// assertThat(provider.password).isEqualTo(password); -// assertThat(provider.id).isEqualTo(providerId); -// assertThat(provider.email).isEqualTo(email); -// -// final String url = "url"; -// final String auth = "auth"; -// final UUID serviceId = UUID.randomUUID(); -// final String service_name = "service name"; -// final String prefix = "/"; -// -// int update = providersDAO.update(providerId, url, auth, serviceId, service_name, prefix); -// assertThat(update).isEqualTo(1); -// -// provider = providersDAO.getByAuth(auth); -// assertThat(provider).isNotNull(); -// assertThat(provider.serviceAuth).isEqualTo(auth); -// assertThat(provider.serviceUrl).isEqualTo(url); -// assertThat(provider.serviceId).isEqualTo(serviceId); -// assertThat(provider.serviceName).isEqualTo(service_name); -// assertThat(provider.commandPrefix).isEqualTo(prefix); -// -// final String newURL = "newURL"; -// update = providersDAO.updateUrl(providerId, newURL); -// assertThat(update).isEqualTo(1); -// -// provider = providersDAO.get(providerId); -// assertThat(provider).isNotNull(); -// assertThat(provider.serviceUrl).isEqualTo(newURL); -// -// final String newName = "new service name"; -// update = providersDAO.updateServiceName(providerId, newName); -// assertThat(update).isEqualTo(1); -// -// provider = providersDAO.get(providerId); -// assertThat(provider).isNotNull(); -// assertThat(provider.serviceName).isEqualTo(newName); -// -// final String newPrefix = "@"; -// update = providersDAO.updateServicePrefix(providerId, newPrefix); -// assertThat(update).isEqualTo(1); -// -// provider = providersDAO.get(providerId); -// assertThat(provider).isNotNull(); -// assertThat(provider.commandPrefix).isEqualTo(newPrefix); -// -// final int deleteService = providersDAO.deleteService(providerId); -// provider = providersDAO.get(providerId); -// -// } -// -// @Test -// public void testBroadcastDAO() { -// final BroadcastDAO broadcastDAO = jdbi.onDemand(BroadcastDAO.class); -// -// final UUID providerId = UUID.randomUUID(); -// final UUID broadcastId = UUID.randomUUID(); -// final UUID botId = UUID.randomUUID(); -// final UUID messageId = UUID.randomUUID(); -// -// final int insert1 = broadcastDAO.insert(broadcastId, botId, providerId, messageId, 0); -// assertThat(insert1).isEqualTo(1); -// -// int insertStatus = broadcastDAO.insertStatus(messageId, 1); -// assertThat(insertStatus).isEqualTo(1); -// insertStatus = broadcastDAO.insertStatus(messageId, 2); -// assertThat(insertStatus).isEqualTo(1); -// insertStatus = broadcastDAO.insertStatus(messageId, 3); -// assertThat(insertStatus).isEqualTo(1); -// -// final UUID get = broadcastDAO.getBroadcastId(providerId); -// assertThat(get).isNotNull(); -// assertThat(get).isEqualTo(broadcastId); -// -// final List report = broadcastDAO.report(broadcastId); -// -// final UUID broadcastId2 = UUID.randomUUID(); -// final UUID botId2 = UUID.randomUUID(); -// final UUID messageId2 = UUID.randomUUID(); -// final int insert2 = broadcastDAO.insert(broadcastId2, botId2, providerId, messageId2, 0); -// assertThat(insert2).isEqualTo(1); -// -// final UUID get2 = broadcastDAO.getBroadcastId(providerId); -// assertThat(get2).isNotNull(); -// assertThat(get2).isEqualTo(broadcastId2); -// } -// -// @Test -// public void testOutgoingMessageDAO() throws JsonProcessingException { -// final ObjectMapper mapper = new ObjectMapper(); -// final OutgoingMessageDAO outgoingMessageDAO = jdbi.onDemand(OutgoingMessageDAO.class); -// OutgoingMessage message = new OutgoingMessage(); -// message.messageId = UUID.randomUUID(); -// message.token = "token"; -// message.attachment = new Attachment(); -// message.attachment.data = "data"; -// -// outgoingMessageDAO.insert(message.messageId, mapper.writeValueAsString(message)); -// -// final OutgoingMessage challenge = outgoingMessageDAO.get(message.messageId); -// assertThat(challenge).isNotNull(); -// assertThat(challenge.messageId).isEqualTo(message.messageId); -// -// outgoingMessageDAO.delete(message.messageId); -// } -//} +package com.wire.bots.roman.integrations; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wire.bots.roman.Application; +import com.wire.bots.roman.DAO.BroadcastDAO; +import com.wire.bots.roman.DAO.OutgoingMessageDAO; +import com.wire.bots.roman.DAO.ProvidersDAO; +import com.wire.bots.roman.model.Attachment; +import com.wire.bots.roman.model.Config; +import com.wire.bots.roman.model.OutgoingMessage; +import com.wire.bots.roman.model.Provider; +import io.dropwizard.configuration.ResourceConfigurationSourceProvider; +import io.dropwizard.testing.DropwizardTestSupport; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import org.jdbi.v3.core.Jdbi; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.List; +import java.util.UUID; + +import static com.wire.bots.roman.resources.dummies.Const.ROMAN_TEST_CONFIG; +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(DropwizardExtensionsSupport.class) +public class DatabaseTest { + private static final DropwizardTestSupport SUPPORT = new DropwizardTestSupport<>( + Application.class, ROMAN_TEST_CONFIG, new ResourceConfigurationSourceProvider() + ); + private Jdbi jdbi; + + @BeforeEach + public void beforeClass() throws Exception { + SUPPORT.before(); + Application app = SUPPORT.getApplication(); + jdbi = app.getJdbi(); + } + + @AfterEach + public void afterClass() { + SUPPORT.after(); + } + + @Test + public void testProviderDAO() { + final ProvidersDAO providersDAO = jdbi.onDemand(ProvidersDAO.class); + + final UUID providerId = UUID.randomUUID(); + final String name = "name"; + final String email = "email@wire.com"; + final String hash = "hash"; + final String password = "password"; + final int insert = providersDAO.insert(name, providerId, email, hash, password); + assertThat(insert).isEqualTo(1); + + Provider provider = providersDAO.get(providerId); + assertThat(provider).isNotNull(); + assertThat(provider.name).isEqualTo(name); + assertThat(provider.hash).isEqualTo(hash); + assertThat(provider.password).isEqualTo(password); + assertThat(provider.id).isEqualTo(providerId); + assertThat(provider.email).isEqualTo(email); + + provider = providersDAO.get(email); + assertThat(provider).isNotNull(); + assertThat(provider.name).isEqualTo(name); + assertThat(provider.hash).isEqualTo(hash); + assertThat(provider.password).isEqualTo(password); + assertThat(provider.id).isEqualTo(providerId); + assertThat(provider.email).isEqualTo(email); + + final String url = "url"; + final String auth = "auth"; + final UUID serviceId = UUID.randomUUID(); + final String service_name = "service name"; + final String prefix = "/"; + + int update = providersDAO.update(providerId, url, auth, serviceId, service_name, prefix); + assertThat(update).isEqualTo(1); + + provider = providersDAO.getByAuth(auth); + assertThat(provider).isNotNull(); + assertThat(provider.serviceAuth).isEqualTo(auth); + assertThat(provider.serviceUrl).isEqualTo(url); + assertThat(provider.serviceId).isEqualTo(serviceId); + assertThat(provider.serviceName).isEqualTo(service_name); + assertThat(provider.commandPrefix).isEqualTo(prefix); + + final String newURL = "newURL"; + update = providersDAO.updateUrl(providerId, newURL); + assertThat(update).isEqualTo(1); + + provider = providersDAO.get(providerId); + assertThat(provider).isNotNull(); + assertThat(provider.serviceUrl).isEqualTo(newURL); + + final String newName = "new service name"; + update = providersDAO.updateServiceName(providerId, newName); + assertThat(update).isEqualTo(1); + + provider = providersDAO.get(providerId); + assertThat(provider).isNotNull(); + assertThat(provider.serviceName).isEqualTo(newName); + + final String newPrefix = "@"; + update = providersDAO.updateServicePrefix(providerId, newPrefix); + assertThat(update).isEqualTo(1); + + provider = providersDAO.get(providerId); + assertThat(provider).isNotNull(); + assertThat(provider.commandPrefix).isEqualTo(newPrefix); + + final int deleteService = providersDAO.deleteService(providerId); + provider = providersDAO.get(providerId); + } + + @Test + public void testBroadcastDAO() { + final BroadcastDAO broadcastDAO = jdbi.onDemand(BroadcastDAO.class); + + final UUID providerId = UUID.randomUUID(); + final UUID broadcastId = UUID.randomUUID(); + final UUID botId = UUID.randomUUID(); + final UUID messageId = UUID.randomUUID(); + + final int insert1 = broadcastDAO.insert(broadcastId, botId, providerId, messageId, 0); + assertThat(insert1).isEqualTo(1); + + int insertStatus = broadcastDAO.insertStatus(messageId, 1); + assertThat(insertStatus).isEqualTo(1); + insertStatus = broadcastDAO.insertStatus(messageId, 2); + assertThat(insertStatus).isEqualTo(1); + insertStatus = broadcastDAO.insertStatus(messageId, 3); + assertThat(insertStatus).isEqualTo(1); + + final UUID get = broadcastDAO.getBroadcastId(providerId); + assertThat(get).isNotNull(); + assertThat(get).isEqualTo(broadcastId); + + final List report = broadcastDAO.report(broadcastId); + + final UUID broadcastId2 = UUID.randomUUID(); + final UUID botId2 = UUID.randomUUID(); + final UUID messageId2 = UUID.randomUUID(); + final int insert2 = broadcastDAO.insert(broadcastId2, botId2, providerId, messageId2, 0); + assertThat(insert2).isEqualTo(1); + + final UUID get2 = broadcastDAO.getBroadcastId(providerId); + assertThat(get2).isNotNull(); + assertThat(get2).isEqualTo(broadcastId2); + } + + @Test + public void testOutgoingMessageDAO() throws JsonProcessingException { + final ObjectMapper mapper = new ObjectMapper(); + final OutgoingMessageDAO outgoingMessageDAO = jdbi.onDemand(OutgoingMessageDAO.class); + OutgoingMessage message = new OutgoingMessage(); + message.messageId = UUID.randomUUID(); + message.token = "token"; + message.attachment = new Attachment(); + message.attachment.data = "data"; + + outgoingMessageDAO.insert(message.messageId, mapper.writeValueAsString(message)); + + final OutgoingMessage challenge = outgoingMessageDAO.get(message.messageId); + assertThat(challenge).isNotNull(); + assertThat(challenge.messageId).isEqualTo(message.messageId); + + outgoingMessageDAO.delete(message.messageId); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/wire/bots/roman/resources/dummies/Const.java b/backend/src/test/java/com/wire/bots/roman/resources/dummies/Const.java index c1401c49..35e81b29 100644 --- a/backend/src/test/java/com/wire/bots/roman/resources/dummies/Const.java +++ b/backend/src/test/java/com/wire/bots/roman/resources/dummies/Const.java @@ -8,4 +8,6 @@ public class Const { public static final UUID CONV_ID = UUID.fromString("21aa4705-ae33-4824-8302-c160a06dc657"); public static final UUID MSG_ID = UUID.fromString("51aa4705-ae33-4824-8302-c160a06dc657"); + public static final String ROMAN_TEST_CONFIG = "roman-test.yml"; + } diff --git a/backend/src/test/resources/roman-test.yml b/backend/src/test/resources/roman-test.yml index 4fbf1e5a..e36747e2 100644 --- a/backend/src/test/resources/roman-test.yml +++ b/backend/src/test/resources/roman-test.yml @@ -34,13 +34,6 @@ swagger: - https - http -assets: - mappings: - /assets: / - overrides: - # the default assumes you have build frontend by "npm run build" - /: ${FRONTEND_PATH:-../frontend/build} - jerseyClient: timeout: 40s connectionTimeout: 40s @@ -56,23 +49,23 @@ jerseyClient: - TLSv1.2 database: - driverClass: org.h2.Driver - user: sa - password: sa - url: jdbc:h2:./target/test + driverClass: org.postgresql.Driver + user: roman + password: roman + url: jdbc:postgresql://localhost:5432/roman token: dummy # used to sign JWT -key: ${APP_KEY:-} +key: ${APP_KEY:-TcZA2Kq4GaOcIbQuOvasrw34321cZAfLW4Ga54fsds43hUuOdcdm42} # the public URL of the Roman instance, should end with "/api" as all Roman endpoints # are served starting with /api -domain: ${PROXY_DOMAIN:-https://proxy.services.wire.com/api} +domain: http://localhost:8080/api # URL of the Wire Backend -apiHost: ${WIRE_API_HOST:-https://prod-nginz-https.wire.com} +apiHost: https://staging-nginz-https.zinfra.io/v4 # TLS public key of "domain" in base64 format - used to pin certificates in Wire backend # for the bot -romanPubKeyBase64: ${ROMAN_PUB_KEY_BASE64:-default} +romanPubKeyBase64: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3xtHqyZPlb0lxlnP0rNAJVmAjB1Tenl11brkkKihcJNRAYrnrT/6sPX4u2lVn/aPncUTjN8omL47MBct7qYV1VY4a5beOyNiVL0ZjZMuh07aL9Z2A4cu67tKZrCoGttn3jpSVlqoOtwEgW+TpgpmKojcRC4DDXEZTEvRoi0RLzAyWCH/8hwWzXR7J082zmn0Ur211QVbOJN/62PAIWyjl5bLglp00AY5OnBHgRNwwRkBJIJLwgNm8u9+0ZplqmMGd3C/QFNngCOeRvFe+5g4qfO4/FOlbkM2kYFAi5KUowfG7cdMQELI+fe4v7yNsgrbMKhnIiLtDIU4wiQIRjbrZwIDAQAB # optional enabling of CORS - in format a.domain.com,another.domain.com -allowedCors: ${ALLOWED_CORS} +allowedCors: ${ALLOWED_CORS} \ No newline at end of file diff --git a/docs/onboarding.md b/docs/onboarding.md index 15076bd7..a1bb253a 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -190,8 +190,8 @@ Now when we have a functional bot with a public URL, we need to register it in W } ``` -From this JSON you will need `service_code` *(in our case `41c5e09f-4867-43d9-b74f-12b27148469a:96917b38-b5d3-4217-bee8-d77569b3c4dc`)* -for the following steps. +From this JSON you will need the `service_code` *(in our case `41c5e09f-4867-43d9-b74f-12b27148469a:96917b38-b5d3-4217-bee8-d77569b3c4dc`)* +for the next steps. The `service_code` is nothing but the combination of your `ProviderID` and `ServiceID` separated by `:`. `ServiceID` is the id of the service you have just created. `ProviderID` and `ServiceID` are needed in the next step. ## 5. Whitelisting the Bot diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 038a5988..39c184d8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9060,9 +9060,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "funding": [ { "type": "individual", @@ -10462,9 +10462,9 @@ } }, "node_modules/ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" }, "node_modules/ip-regex": { "version": "2.1.0", diff --git a/frontend/src/app/pages/home/components/ServiceAccess.tsx b/frontend/src/app/pages/home/components/ServiceAccess.tsx index 646ac448..a98981f1 100644 --- a/frontend/src/app/pages/home/components/ServiceAccess.tsx +++ b/frontend/src/app/pages/home/components/ServiceAccess.tsx @@ -23,7 +23,7 @@ export default function ServiceAccessInfo(accessInfo: ServiceAccessInfoProps) { InputProps={{ readOnly: true }} - helperText={'Service code is used to enable the service in the team settings.'} + helperText={'Service code is made of ProviderID and ServiceID separated by a colon.'} /> diff --git a/test.sh b/test.sh new file mode 100755 index 00000000..135e7183 --- /dev/null +++ b/test.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env sh +set -x + +echo "1/4) Starting test environment..." +docker-compose up -d db + +echo "2/4) Running tests..." +DOCKER_BUILDKIT=1 docker build --network host --target export-stage --output backend/target/reports -f Dockerfile.UnitTests . + +echo "3/4) Cleaning up test environment..." +docker-compose stop + +echo "4/4) Evaluating tests result exit status..." +EXIT_CODE=`cat backend/target/reports/test.result` +exit $EXIT_CODE \ No newline at end of file