diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 00000000..c1dd12f1 Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..40ca0155 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..9b9c56c6 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,18 @@ +version: "3" +services: + postgres: + image: "postgres:13.4-alpine" + container_name: "main" + environment: + POSTGRES_USER: "postgres" + POSTGRES_PASSWORD: "postgres" + POSTGRES_DB: "todo" + ports: + - "35432:5432" + + redis: + image: "redis:7.2" + container_name: "redis" + restart: "unless-stopped" + ports: + - "6379:6379" \ No newline at end of file diff --git a/mvnw b/mvnw old mode 100644 new mode 100755 index 19529ddf..8a8fb228 --- a/mvnw +++ b/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -19,241 +19,298 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# 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 # ---------------------------------------------------------------------------- -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x +if [ -z "$MAVEN_SKIP_RC" ] ; then -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - 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" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 +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 - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` fi -} +fi -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" +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 - printf %x\\n $h -} -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + saveddir=`pwd` -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} + M2_HOME=`dirname "$PRG"`/.. -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" +# 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 -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac +# 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 -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" +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 -mkdir -p -- "${MAVEN_HOME%/*}" +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="`\\unset -f command; \\command -v java`" + fi +fi -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." fi -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac +# 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 "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then - distributionSha256Result=true + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 + # 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 -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +########################################################################################## +# 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 - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" + 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/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.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" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$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 -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" +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 -clean || : -exec_maven "$@" +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 249bdf38..e6e94e99 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,149 +1,188 @@ -<# : batch portion -@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 http://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 Apache Maven Wrapper startup batch script, version 3.3.2 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +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 "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\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/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq 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%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.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% + ) ) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" -if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" -} -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" +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 "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\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% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml index 03bc446d..5ffaef7b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.5 + 3.2.5 com.emobile @@ -15,13 +15,149 @@ SpringToDo - 21 + 17 + org.springframework.boot spring-boot-starter + 3.3.5 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.postgresql + postgresql + 42.5.1 + runtime + + + + org.springframework.boot + spring-boot-starter-jdbc + 3.3.5 + + + + org.springframework.boot + spring-boot-starter-web + 3.3.5 + + + + org.mapstruct + mapstruct + 1.5.5.Final + + + + org.mapstruct + mapstruct-processor + 1.5.5.Final + provided + + + + org.springframework.boot + spring-boot-starter-validation + 3.3.5 + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + + + + org.springframework.boot + spring-boot-starter-cache + 3.3.5 + + + + org.springframework.boot + spring-boot-starter-data-redis + 3.3.5 + + + + redis.clients + jedis + 5.2.0 + + + + com.fasterxml.jackson.core + jackson-databind + + + + org.springframework.boot + spring-boot-starter-actuator + 2.7.8 + + + + io.micrometer + micrometer-registry-prometheus + 1.15.1 + + + + org.mockito + mockito-junit-jupiter + 5.10.0 + test + + + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + + + + org.mockito + mockito-core + 5.10.0 + test + + + + org.testcontainers + testcontainers + 1.19.7 + test + + + + org.testcontainers + postgresql + 1.19.7 + test + + + + org.testcontainers + junit-jupiter + 1.19.7 + test + + + + org.skyscreamer + jsonassert + 1.5.1 + test @@ -29,6 +165,13 @@ spring-boot-starter-test test + + + org.flywaydb + flyway-core + 9.6.0 + + @@ -37,6 +180,11 @@ org.springframework.boot spring-boot-maven-plugin + + + + + diff --git a/src/main/java/com/emobile/springtodo/SpringToDoApplication.java b/src/main/java/com/emobile/springtodo/SpringToDoApplication.java index 41980514..e79fa82c 100644 --- a/src/main/java/com/emobile/springtodo/SpringToDoApplication.java +++ b/src/main/java/com/emobile/springtodo/SpringToDoApplication.java @@ -9,5 +9,4 @@ public class SpringToDoApplication { public static void main(String[] args) { SpringApplication.run(SpringToDoApplication.class, args); } - } diff --git a/src/main/java/com/emobile/springtodo/apiDocs/ToDoApiDocs.java b/src/main/java/com/emobile/springtodo/apiDocs/ToDoApiDocs.java new file mode 100644 index 00000000..9e947697 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/apiDocs/ToDoApiDocs.java @@ -0,0 +1,32 @@ +package com.emobile.springtodo.apiDocs; + +import com.emobile.springtodo.dto.ToDoDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.util.List; +import java.util.Map; + +@Tag(name = "Контроллер для работы с задачами", description = "Позволяет создавать, удалять и выводить список задач") +public interface ToDoApiDocs { + + @Operation(summary = "Создание задачи") + void createToDo(final ToDoDto toDoDto); + + @Operation(summary = "Вывод список задач") + List doDtoList(@Parameter(description = "Максимальное количество задач, возвращаемых в ответе") + int limit, + @Parameter(description = "Количество задач, которые нужно пропустить(смещение для пагинации)") + int offset); + + @Operation(summary = "Удаление задачи") + Map deleteToDo(long idToDo); + + @Operation(summary = "Вывод задачи по заданному id") + ToDoDto getToDoDto(long idToDo); + + @Operation(summary = "Внесение измененния в поле completed") + Map updateCompleted(@Parameter(description = "ID задачи, которой необходимо изменить поле completed") long idToDo, + @Parameter(description = "Новое значение") boolean completed); +} diff --git a/src/main/java/com/emobile/springtodo/config/CacheConfig.java b/src/main/java/com/emobile/springtodo/config/CacheConfig.java new file mode 100644 index 00000000..81e3d892 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/config/CacheConfig.java @@ -0,0 +1,23 @@ +package com.emobile.springtodo.config; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; + +import java.time.Duration; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public RedisCacheConfiguration cacheConfiguration(){ + return RedisCacheConfiguration.defaultCacheConfig() + .serializeValuesWith(RedisSerializationContext.SerializationPair + .fromSerializer(new GenericJackson2JsonRedisSerializer())) + .entryTtl(Duration.ofMinutes(10)); + } +} diff --git a/src/main/java/com/emobile/springtodo/config/SwaggerConfig.java b/src/main/java/com/emobile/springtodo/config/SwaggerConfig.java new file mode 100644 index 00000000..00940989 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/config/SwaggerConfig.java @@ -0,0 +1,11 @@ +package com.emobile.springtodo.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import org.springframework.context.annotation.Configuration; + +@OpenAPIDefinition(info = @Info(title = "ToDo", version = "1.0", + description = "Приложение по управлению задачами")) +@Configuration +public class SwaggerConfig { +} diff --git a/src/main/java/com/emobile/springtodo/controller/ToDoController.java b/src/main/java/com/emobile/springtodo/controller/ToDoController.java new file mode 100644 index 00000000..7b9edfe2 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/controller/ToDoController.java @@ -0,0 +1,63 @@ +package com.emobile.springtodo.controller; + +import com.emobile.springtodo.apiDocs.ToDoApiDocs; +import com.emobile.springtodo.dto.ToDoDto; +import com.emobile.springtodo.service.ToDoService; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/todos") +public class ToDoController implements ToDoApiDocs { + private final ToDoService toDoService; + + public ToDoController(ToDoService toDoService) { + this.toDoService = toDoService; + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @Override + public void createToDo(@RequestBody @Valid ToDoDto toDoDto) { + toDoService.save(toDoDto); + } + + @GetMapping + @Override + public List doDtoList(@RequestParam(defaultValue = "10") int limit, + @RequestParam(defaultValue = "0") int offset) { + return toDoService.findAll(limit,offset); + } + + @DeleteMapping + @Override + public Map deleteToDo(@RequestParam long idToDo) { + toDoService.deleteToDo(idToDo); + return Map.of("message", "задача с id " + idToDo + " успешно удалена"); + } + + @GetMapping("/getToDo") + @Override + public ToDoDto getToDoDto(@RequestParam long idToDo) { + return toDoService.getToDo(idToDo); + } + + @PatchMapping + @Override + public Map updateCompleted(@RequestParam long idToDo, @RequestParam boolean completed) { + toDoService.updateCompleted(idToDo,completed); + return Map.of("message", "задача с id " + idToDo + " успешно обновлена"); + } +} diff --git a/src/main/java/com/emobile/springtodo/dto/ToDoDto.java b/src/main/java/com/emobile/springtodo/dto/ToDoDto.java new file mode 100644 index 00000000..ecb5ad75 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/dto/ToDoDto.java @@ -0,0 +1,46 @@ +package com.emobile.springtodo.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public class ToDoDto { + @NotBlank(message = "Поле не может быть пустым") + String title; + @NotBlank(message = "Поле не может быть пустым") + String description; + @NotNull(message = "Поле обязательно для заполнения") + Boolean completed; + + public ToDoDto(String title, String description, boolean completed) { + this.title = title; + this.description = description; + this.completed = completed; + } + + public ToDoDto() { + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Boolean getCompleted() { + return completed; + } + + public void setCompleted(Boolean completed) { + this.completed = completed; + } +} diff --git a/src/main/java/com/emobile/springtodo/exception/ErrorMessage.java b/src/main/java/com/emobile/springtodo/exception/ErrorMessage.java new file mode 100644 index 00000000..c1c9997c --- /dev/null +++ b/src/main/java/com/emobile/springtodo/exception/ErrorMessage.java @@ -0,0 +1,17 @@ +package com.emobile.springtodo.exception; + +public class ErrorMessage { + private String message; + + public ErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/emobile/springtodo/exception/GlobalExceptionHandler.java b/src/main/java/com/emobile/springtodo/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..d5c21b8a --- /dev/null +++ b/src/main/java/com/emobile/springtodo/exception/GlobalExceptionHandler.java @@ -0,0 +1,37 @@ +package com.emobile.springtodo.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Map handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) { + Map errors = new HashMap<>(); + exception.getBindingResult().getFieldErrors().forEach(error -> + errors.put(error.getField(), error.getDefaultMessage()) + ); + return errors; + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorMessage handleJsonParseError(HttpMessageNotReadableException ex) { + return new ErrorMessage("Невозможно прочитать JSON: " + ex.getMostSpecificCause().getMessage()); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorMessage notFoundException(TaskNotFoundException ex) { + return new ErrorMessage("Объект не найден." + ex.getMessage()); + } +} diff --git a/src/main/java/com/emobile/springtodo/exception/TaskNotFoundException.java b/src/main/java/com/emobile/springtodo/exception/TaskNotFoundException.java new file mode 100644 index 00000000..cb1ff2c8 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/exception/TaskNotFoundException.java @@ -0,0 +1,8 @@ +package com.emobile.springtodo.exception; + +public class TaskNotFoundException extends RuntimeException{ + + public TaskNotFoundException(String message){ + super(message); + } +} diff --git a/src/main/java/com/emobile/springtodo/mapper/ToDoMapper.java b/src/main/java/com/emobile/springtodo/mapper/ToDoMapper.java new file mode 100644 index 00000000..731f13ba --- /dev/null +++ b/src/main/java/com/emobile/springtodo/mapper/ToDoMapper.java @@ -0,0 +1,14 @@ +package com.emobile.springtodo.mapper; + +import com.emobile.springtodo.dto.ToDoDto; +import com.emobile.springtodo.model.ToDo; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface ToDoMapper { + @Mapping(target = "id", ignore = true) + ToDo toEntity(ToDoDto toDoDto); + + ToDoDto toDto(ToDo toDo); +} diff --git a/src/main/java/com/emobile/springtodo/metrics/ToDoMetrics.java b/src/main/java/com/emobile/springtodo/metrics/ToDoMetrics.java new file mode 100644 index 00000000..1d17e507 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/metrics/ToDoMetrics.java @@ -0,0 +1,37 @@ +package com.emobile.springtodo.metrics; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import jakarta.annotation.PostConstruct; +import org.springframework.stereotype.Component; + +@Component +public class ToDoMetrics { + + private final MeterRegistry meterRegistry; + private Counter createdToDoCounter; + private Counter completedToDoCounter; + + public ToDoMetrics(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } + + @PostConstruct + public void init() { + createdToDoCounter = Counter.builder("todo.tasks.created") + .description("Количество созданных задач") + .register(meterRegistry); + + completedToDoCounter = Counter.builder("todo.tasks.completed") + .description("Количество завершённых задач") + .register(meterRegistry); + } + + public void incrementCreatedTasks() { + createdToDoCounter.increment(); + } + + public void incrementCompletedTasks() { + completedToDoCounter.increment(); + } +} diff --git a/src/main/java/com/emobile/springtodo/model/ToDo.java b/src/main/java/com/emobile/springtodo/model/ToDo.java new file mode 100644 index 00000000..a51d8406 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/model/ToDo.java @@ -0,0 +1,51 @@ +package com.emobile.springtodo.model; + +public class ToDo { + + private Long id; + private String title; + private String description; + private boolean completed; + + public ToDo() { + } + + public ToDo(Long id, String title, String description, boolean completed) { + this.id = id; + this.title = title; + this.description = description; + this.completed = completed; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean isCompleted() { + return completed; + } + + public void setCompleted(boolean completed) { + this.completed = completed; + } +} diff --git a/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java b/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java new file mode 100644 index 00000000..2565267b --- /dev/null +++ b/src/main/java/com/emobile/springtodo/repository/ToDoRepository.java @@ -0,0 +1,57 @@ +package com.emobile.springtodo.repository; + +import com.emobile.springtodo.model.ToDo; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public class ToDoRepository { + + private final JdbcTemplate jdbcTemplate; + + private final RowMapper rowMapper = (rs, rowNum) -> { + ToDo toDo = new ToDo(); + toDo.setId(rs.getLong("id")); + toDo.setTitle(rs.getString("title")); + toDo.setDescription(rs.getString("description")); + toDo.setCompleted(rs.getBoolean("completed")); + return toDo; + }; + + public ToDoRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void save(ToDo toDo){ + String sql = "INSERT INTO todo (title, description, completed) VALUES (?, ?, ?)"; + jdbcTemplate.update(sql, toDo.getTitle(), toDo.getDescription(), toDo.isCompleted()); + } + + public List findAll(int limit, int offset){ + String sql = "SELECT id, title, description, completed FROM todo LIMIT ? OFFSET ? "; + return jdbcTemplate.query(sql, rowMapper, limit, offset); + } + + public int delete(long idToDo){ + String sql = "DELETE from todo WHERE id = ?"; + return jdbcTemplate.update(sql, idToDo); + } + + public ToDo getToDoById(long idToDo){ + String sql = "SELECT * FROM todo WHERE id = ?"; + try { + return jdbcTemplate.queryForObject(sql, rowMapper, idToDo); + } catch (EmptyResultDataAccessException e){ + return null; + } + } + + public int updateCompleted(long idToDo, boolean completed){ + String sql = "UPDATE todo SET completed = ? WHERE id = ?"; + return jdbcTemplate.update(sql, completed, idToDo); + } +} diff --git a/src/main/java/com/emobile/springtodo/service/ToDoService.java b/src/main/java/com/emobile/springtodo/service/ToDoService.java new file mode 100644 index 00000000..73274305 --- /dev/null +++ b/src/main/java/com/emobile/springtodo/service/ToDoService.java @@ -0,0 +1,69 @@ +package com.emobile.springtodo.service; + +import com.emobile.springtodo.dto.ToDoDto; +import com.emobile.springtodo.exception.TaskNotFoundException; +import com.emobile.springtodo.mapper.ToDoMapper; +import com.emobile.springtodo.metrics.ToDoMetrics; +import com.emobile.springtodo.model.ToDo; +import com.emobile.springtodo.repository.ToDoRepository; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ToDoService { + private final ToDoRepository toDoRepository; + private final ToDoMapper toDoMapper; + private final ToDoMetrics toDoMetrics; + + public ToDoService(ToDoRepository toDoRepository, ToDoMapper toDoMapper, ToDoMetrics toDoMetrics) { + this.toDoRepository = toDoRepository; + this.toDoMapper = toDoMapper; + this.toDoMetrics = toDoMetrics; + } + + public void save(ToDoDto toDoDto){ + ToDo entity = toDoMapper.toEntity(toDoDto); + toDoRepository.save(entity); + toDoMetrics.incrementCreatedTasks(); + } + + public List findAll(int limit, int offset){ + List todos = toDoRepository.findAll(limit,offset); + return todos.stream().map(toDoMapper::toDto).toList(); + } + + @CacheEvict(value = "todo") + public void deleteToDo(long idToDo){ + int deleted = toDoRepository.delete(idToDo); + + if(deleted == 0){ + throw new TaskNotFoundException("Задача с id " + idToDo + " не существует"); + } + + toDoRepository.delete(idToDo); + } + + @Cacheable(value = "todo") + public ToDoDto getToDo(long idToDo){ + ToDo toDo = toDoRepository.getToDoById(idToDo); + + if(toDo == null){ + throw new TaskNotFoundException("Задача с id " + idToDo + " не существует"); + } + + return toDoMapper.toDto(toDo); + } + + public void updateCompleted(long idToDo, boolean completed){ + int update = toDoRepository.updateCompleted(idToDo, completed); + + if(update == 0 ){ + throw new TaskNotFoundException("Задача с id " + idToDo + " не существует"); + + } + toDoMetrics.incrementCompletedTasks(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 5c3e5461..f4c3336f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,23 @@ spring.application.name=SpringToDo + +spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.username=postgres +spring.datasource.password=postgres +spring.datasource.url=jdbc:postgresql://localhost:35432/todo + +spring.flyway.enabled=true +spring.flyway.baseline-on-migrate=true +spring.flyway.locations=classpath:db/migration + +spring.cache.type=redis +spring.data.redis.host=localhost +spring.data.redis.port=6379 + +management.endpoints.web.exposure.include=health,info,metrics +management.endpoint.health.show-details=always +management.info.env.enabled=true + +info.app.name =ToDo +info.app.description=REST API to manage tasks +info.developer.name=Khval Polina +info.developer.email= polina.java@yandex.ru \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__Create_todo_table.sql b/src/main/resources/db/migration/V1__Create_todo_table.sql new file mode 100644 index 00000000..1c779458 --- /dev/null +++ b/src/main/resources/db/migration/V1__Create_todo_table.sql @@ -0,0 +1,8 @@ +CREATE TABLE todo +( + id SERIAL NOT NULL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description VARCHAR(255) NOT NULL, + completed BOOLEAN NOT NULL DEFAULT FALSE +); + diff --git a/src/test/java/com/emobile/springtodo/controller/ToDoControllerTest.java b/src/test/java/com/emobile/springtodo/controller/ToDoControllerTest.java new file mode 100644 index 00000000..39cb2a01 --- /dev/null +++ b/src/test/java/com/emobile/springtodo/controller/ToDoControllerTest.java @@ -0,0 +1,120 @@ +package com.emobile.springtodo.controller; + +import com.emobile.springtodo.integration.AbstractIntegrationToDoTest; +import com.emobile.springtodo.dto.ToDoDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@Sql(scripts = {"/schema.sql", "/data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +public class ToDoControllerTest extends AbstractIntegrationToDoTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + private ToDoDto testDto; + + @BeforeEach + void setup() { + testDto = new ToDoDto(); + testDto.setTitle("Title JSON"); + testDto.setDescription("Desc JSON"); + testDto.setCompleted(false); + } + + @Test + @DisplayName("Создание новой задачи должно возвращать статус 201 Created") + void testCreateToDo() throws Exception { + mockMvc.perform(post("/api/todos") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testDto))) + .andExpect(status().isCreated()); + } + + @Test + @DisplayName("Получение списка всех задач с пагинацией возвращает корректный JSON") + void testGetAllToDos() throws Exception { + String jsonResponse = mockMvc.perform(get("/api/todos") + .param("limit", "10") + .param("offset", "0")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + String expectedPartialJson = """ + [ + { + "title":"Title1", + "description":"Description1", + "completed":false + }, + { + "title":"Title2", + "description":"Description2", + "completed":false + }, + { + "title":"Title3", + "description":"Description3", + "completed":false + } + ] + """; + + JSONAssert.assertEquals(expectedPartialJson, jsonResponse, false); + } + + @Test + @DisplayName("Получение задачи по ID возвращает корректный объект") + void testGetToDoById() throws Exception { + String jsonResponse = mockMvc.perform(get("/api/todos/getToDo") + .param("idToDo", "1")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + String expectedJson = """ + { + "title":"Title1", + "description":"Description1", + "completed":false + } + """; + + JSONAssert.assertEquals(expectedJson, jsonResponse, false); + } + + @Test + @DisplayName("Удаление задачи по ID должно вернуть сообщение об успешном удалении") + void testDeleteToDo() throws Exception { + mockMvc.perform(delete("/api/todos") + .param("idToDo", "2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("задача с id 2 успешно удалена")); + } + + @Test + @DisplayName("Обновление статуса выполнения задачи по ID должно вернуть сообщение об успешном обновлении") + void testUpdateCompleted() throws Exception { + mockMvc.perform(patch("/api/todos") + .param("idToDo", "3") + .param("completed", "true")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("задача с id 3 успешно обновлена")); + } +} diff --git a/src/test/java/com/emobile/springtodo/integration/AbstractIntegrationToDoTest.java b/src/test/java/com/emobile/springtodo/integration/AbstractIntegrationToDoTest.java new file mode 100644 index 00000000..194fa7fe --- /dev/null +++ b/src/test/java/com/emobile/springtodo/integration/AbstractIntegrationToDoTest.java @@ -0,0 +1,26 @@ +package com.emobile.springtodo.integration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +public class AbstractIntegrationToDoTest { + + static PostgreSQLContainer container = new PostgreSQLContainer<>("postgres:13.4") + .withUsername("postgres") + .withPassword("postgres") + .withDatabaseName("postgresTest") + .withExposedPorts(5432); + + static { + container.start(); + } + + @DynamicPropertySource + static void overrideProperties(DynamicPropertyRegistry dynamicPropertyRegistry){ + dynamicPropertyRegistry.add("spring.datasource.url",container::getJdbcUrl); + dynamicPropertyRegistry.add("spring.datasource.username",container::getUsername); + dynamicPropertyRegistry.add("spring.datasource.password",container::getPassword); + } +} diff --git a/src/test/java/com/emobile/springtodo/repository/ToDoRepositoryTest.java b/src/test/java/com/emobile/springtodo/repository/ToDoRepositoryTest.java new file mode 100644 index 00000000..2e8f26db --- /dev/null +++ b/src/test/java/com/emobile/springtodo/repository/ToDoRepositoryTest.java @@ -0,0 +1,98 @@ +package com.emobile.springtodo.repository; + +import com.emobile.springtodo.integration.AbstractIntegrationToDoTest; +import com.emobile.springtodo.model.ToDo; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.jdbc.Sql; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import(ToDoRepository.class) +@Sql(scripts = {"/schema.sql", "/data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +class ToDoRepositoryTest extends AbstractIntegrationToDoTest { + + @Autowired + private ToDoRepository toDoRepository; + + @Test + @DisplayName("Сохраняет новую задачу и проверяет корректность сохранения") + void saveToDo(){ + ToDo toDo = new ToDo(); + toDo.setId(1L); + toDo.setTitle("Title test"); + toDo.setDescription("Description test"); + toDo.setCompleted(false); + + toDoRepository.save(toDo); + + List all = toDoRepository.findAll(10, 0); + ToDo saved = all.get(all.size() - 1); + + assertEquals(toDo.getTitle(), saved.getTitle()); + assertEquals("Description test", saved.getDescription()); + assertFalse(saved.isCompleted()); + } + + @Test + @DisplayName("Получает все задачи с ограничением и смещением") + void findAllTest(){ + List todos = toDoRepository.findAll(10, 0); + + assertNotNull(todos); + assertEquals(3, todos.size()); + } + + @Test + @DisplayName("Удаляет задачу по ID и проверяет, что она удалена") + void testDelete() { + int rowsAffected = toDoRepository.delete(1L); + + assertEquals(1, rowsAffected); + + ToDo deleted = toDoRepository.getToDoById(1L); + + assertNull(deleted); + } + + @Test + @DisplayName("Получает задачу по существующему ID") + void testGetToDoByIdExists() { + ToDo todo = toDoRepository.getToDoById(2L); + + assertNotNull(todo); + assertEquals("Title2", todo.getTitle()); + } + + @Test + @DisplayName("Получение задачи по несуществующему ID возвращает null") + void testGetToDoByIdNotExists() { + ToDo todo = toDoRepository.getToDoById(20L); + + assertNull(todo); + } + + @Test + @DisplayName("Обновляет статус completed задачи и проверяет обновление") + void testUpdateCompleted() { + int rowsUpdated = toDoRepository.updateCompleted(2L, true); + + assertEquals(1, rowsUpdated); + + ToDo updated = toDoRepository.getToDoById(2L); + + assertTrue(updated.isCompleted()); + } +} diff --git a/src/test/java/com/emobile/springtodo/service/ToDoServiceTest.java b/src/test/java/com/emobile/springtodo/service/ToDoServiceTest.java new file mode 100644 index 00000000..c1b1a399 --- /dev/null +++ b/src/test/java/com/emobile/springtodo/service/ToDoServiceTest.java @@ -0,0 +1,128 @@ +package com.emobile.springtodo.service; + +import com.emobile.springtodo.dto.ToDoDto; +import com.emobile.springtodo.exception.TaskNotFoundException; +import com.emobile.springtodo.mapper.ToDoMapper; +import com.emobile.springtodo.metrics.ToDoMetrics; +import com.emobile.springtodo.model.ToDo; +import com.emobile.springtodo.repository.ToDoRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@ExtendWith(MockitoExtension.class) +class ToDoServiceTest { + + @Mock + private ToDoRepository toDoRepository; + + @Mock + private ToDoMapper toDoMapper; + + @Mock + private ToDoMetrics toDoMetrics; + + @InjectMocks + private ToDoService toDoService; + + private ToDoDto sampleDto; + private ToDo sampleEntity; + + @BeforeEach + void setUp() { + sampleDto = new ToDoDto("Title", "Test", false); + sampleEntity = new ToDo(1L, "Title", "Test", false); + } + + @Test + @DisplayName("Метод должен сохранять задачу и обновлять метрику созданных задач") + void saveShouldCallRepositoryAndMetrics() { + when(toDoMapper.toEntity(sampleDto)).thenReturn(sampleEntity); + + toDoService.save(sampleDto); + + verify(toDoMapper).toEntity(sampleDto); + verify(toDoRepository).save(sampleEntity); + verify(toDoMetrics).incrementCreatedTasks(); + } + + @Test + @DisplayName("Метод должен возвращать список задач") + void findAllShouldReturnMappedDto() { + when(toDoRepository.findAll(10, 0)).thenReturn(List.of(sampleEntity)); + when(toDoMapper.toDto(sampleEntity)).thenReturn(sampleDto); + + List resultList = toDoService.findAll(10, 0); + + assertEquals(1, resultList.size()); + assertEquals(sampleDto, resultList.get(0)); + } + + @Test + @DisplayName("Метод должен вернуть TaskNotFoundException, если задача с id не найдена") + void deleteShouldThrowExceptionWhenNotFound() { + when(toDoRepository.delete(1L)).thenReturn(0); + + TaskNotFoundException resultException = assertThrows(TaskNotFoundException.class, + () -> toDoService.deleteToDo(1L)); + + assertEquals("Задача с id 1 не существует", resultException.getMessage()); + } + + @Test + @DisplayName("Метод должен вернуть TaskNotFoundException, если задача с id не найдена") + void getToDoShouldThrowExceptionWhenNotFound() { + when(toDoRepository.getToDoById(1L)).thenReturn(null); + + TaskNotFoundException resultException = assertThrows(TaskNotFoundException.class, + () -> toDoService.getToDo(1L)); + + assertEquals("Задача с id 1 не существует", resultException.getMessage()); + } + + @Test + @DisplayName("Метод должен вернуть задачу по id") + void getToDoShouldReturnDtoWhenFound() { + when(toDoRepository.getToDoById(1L)).thenReturn(sampleEntity); + when(toDoMapper.toDto(sampleEntity)).thenReturn(sampleDto); + + ToDoDto result = toDoService.getToDo(1L); + + assertEquals(sampleDto, result); + } + + @Test + @DisplayName("Метод должен обновить статус задачи " + + "и инкрементировать метрику завершённых задач при успешном обновлении") + void updateCompletedShouldUpdateAndIncrementMetric() { + when(toDoRepository.updateCompleted(1L, true)).thenReturn(1); + + toDoService.updateCompleted(1L, true); + + verify(toDoRepository).updateCompleted(1L,true); + verify(toDoMetrics).incrementCompletedTasks(); + } + + @Test + @DisplayName("Метод должен вернуть TaskNotFoundException, если задача с id не найдена") + void updateCompletedShouldThrowExceptionWhenNotFound() { + when(toDoRepository.updateCompleted(1L, true)).thenReturn(0); + + TaskNotFoundException resultException = assertThrows(TaskNotFoundException.class, + () -> toDoService.updateCompleted(1L, true)); + + assertEquals("Задача с id 1 не существует", resultException.getMessage()); + } +} diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql new file mode 100644 index 00000000..8d1777a9 --- /dev/null +++ b/src/test/resources/data.sql @@ -0,0 +1,3 @@ +INSERT INTO todo(title, description, completed) VALUES('Title1', 'Description1', false); +INSERT INTO todo(title, description, completed) VALUES('Title2', 'Description2', false); +INSERT INTO todo(title, description, completed) VALUES('Title3', 'Description3', false); \ No newline at end of file diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 00000000..80929616 --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS todo; +CREATE TABLE todo +( + id SERIAL NOT NULL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description VARCHAR(255) NOT NULL, + completed BOOLEAN NOT NULL DEFAULT FALSE +);