Added project

This commit is contained in:
2023-06-11 00:23:46 +03:00
parent 7a8ca16c1f
commit 366ce44813
36 changed files with 2274 additions and 23 deletions

52
.gitignore vendored
View File

@@ -1,26 +1,36 @@
# ---> Java HELP.md
# Compiled class file target/
*.class !.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
# Log file !**/src/test/**/target/
.idea/
*.gz
*.log *.log
# BlueJ files ### STS ###
*.ctxt .apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
# Mobile Tools for Java (J2ME) ### IntelliJ IDEA ###
.mtj.tmp/ .idea
*.iws
*.iml
*.ipr
# Package Files # ### NetBeans ###
*.jar /nbproject/private/
*.war /nbbuild/
*.nar /dist/
*.ear /nbdist/
*.zip /.nb-gradle/
*.tar.gz build/
*.rar !**/src/main/**/build/
!**/src/test/**/build/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
### VS Code ###
.vscode/

234
README.md
View File

@@ -1,3 +1,233 @@
# profile-rest-service # Тестовое задание
Тестовое задание. В качестве тестового задания необходимо реализовать RESTfull приложение. ## Общее описание
В качестве тестового задания необходимо реализовать RESTfull приложение
1. При разработке использовать spring boot остальное на ваше усмотрение, желательно придерживаться экосистемы spring.
1. При разработке использовать в качестве базы Postgres
1. cross-origin должно быть отключено.
1. доступ к сервису возможен только при наличии токена secret во всех остальных случаях кроме “GET /exit” возвращать 401 название и реализация на ваше усмотрение, инструкция для передачи токена должна прилагаться в месте с тестовым заданием.
1. сервис должен запускаться на 8010 порту
1. name и email должны быть регистронезависимые
1. Добавить фильтр при регистрации на проверку уникальности поля email в случае если Email есть в базе возвращать 409 статус
1. к исходникам должен прилагаться артефакт приложения
1. Для данного приложения реализуйте и подключил OpenApi (swagger)
1. Версия java не выше 11
1. Сборщик Maven
## Описание endpoints
### POST /profiles/set
Создает запись профиля и присваивает ему id
**Request**:
принимает json следующей структурой:
```
{
"name": string
"email": string
"age": int
}
```
**Responses**:
в случае успеха возвращает id записи пользователя
**_status 200_**
```
{
"idUser": int
}
```
В случае не корректного email
**_status 400_**
```
{
"msg": string
}
```
В случае если email уже передавался (реализовать через фильтр)
**_status 403_**
```
{
“msg”: string
}
```
### GET /profiles/last
Возвращает последний созданный профиль
**Responses**:
**_status 200_**
```
{
"id": int
"name": string
"email": string
"age": int
"created": timestamp
}
```
### GET /profiles
Возвращает все созданные профили
**Responses**:
**_status 200_**
```
[{
"id": int
"name": string
"email": string
"age": int
"created": timestamp
}...]
```
###GET /profiles/{ID}
Возвращает профиль по его ID
Responses:
**_status 200_**
```
{
"id": int
"name": string
"email": string
"age": int
"created": timestamp
}
```
**_status 404_**
в случае если запись не найдена
```
{
"msg": string
}
```
### POST /profiles/get
Возвращает профиль по email
**Request**:
принимает json следующей структурой:
```
{
"email": string
}
```
**Responses**:
**_status 200_**
```
{
'id": int
"name": string
"email": string
"age": int
"created": timestamp
}
```
**_status 404_**
в случае если запись не найдена
```
{
"msg": string
}
```
### GET /error/last
Возвращает сообщение последней ошибки
**Responses**:
**_status 200_**
```
{
"msg": string
"created": timestamp
}
```
### Не обязательная часть задания:
**GET /exit**
Производит закрытия приложение с редиректом на страницу /exit-success (название вариативно) с надписью ‘приложение закрыто’ допускаются и другие варианты информирования о закрытие.
---
# Комментарий к выполненной работе
Программу можно запустить в нескольких режимах используя профили spring boot
1. **DEFAULT** - в данном режиме используется база данных postgresql с настройками по умолчанию, а именно `url:jdbc:postgresql://localhost/test, username: test, password: test`. Для запуска используем следующие параметры `java -jar restsrv.jar`
1. **DEMO** - в данном режиме используется база данных H2 DB. Для запуска используем следующие параметры `java -jar restsrv.jar --spring.profiles.active=demo`
1. **PRODUCTION** - в данном режиме используется база данных postgresql c альтернативными настройками прописанными в файле `application-prod.properties`, данный файл должен находиться в том же каталоге где и запускаемый jar-файл программы. Для запуска используем следующие параметры `java -jar restsrv.jar --spring.profiles.active=prod`
Пример содержимого файла `application-prod.properties`
```
RESTSRV_PGSQL_DB_HOST=jdbc:postgresql://localhost
RESTSRV_PGSQL_DB_PORT=5432
RESTSRV_PGSQL_DB_NAME=test
RESTSRV_PGSQL_DB_USER=test
RESTSRV_PGSQL_DB_PASSWORD=test
```
Так же для запуска программы в linux, можно воспользоваться скриптом `restsrv_linux.sh` , при этом запускаемый jar-файл должен называться `restsrv.jar` и находиться в том же каталоге, где и скрипт. Выполните `restsrv_linux.sh --help` для получения помощи. При запуске в режиме `PRODUCTION` будет выполнена проверка на наличие файла `application-prod.properties`, если он не найден, то запустится интерактивный режим, где будет предложено заполнить необходимые данные.
## Работа с программой
### Инициализация БД
Если работа ведётся с postgresql воспользуйтесь файлами `ddl-postgresql.sql` и `schema-postgresql.sql` см. каталог `init_postgresql_db`
### Работа с токеном безопасности.
По условию задания, доступ к эндпоинтам сервиса осуществляется при помощи токена `secret` . В каждом звпросе, в заголовке запроса, должна присутствовать следующая строка `Authorization: Bearer secret` , без данной записи в заголовке, при обращении к защищенным эндпоинтам будет возвращаться код 401.
### Прочее
В программе используется OpenApi c ui, для доступа к ui используем http://localhost:8010/swagger-ui/index.html , в представлении json используем http://localhost:8010/v3/api-docs
---
Тестовое задание выполнил
Александров А.А. (alexandrov@resprojects.ru)
ссылка на профиль hh.ru - https://hh.ru/resume/7cdada75ff015e78530039ed1f366c4b4a5273

322
mvnw vendored Executable file
View File

@@ -0,0 +1,322 @@
#!/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 "$@"

182
mvnw.cmd vendored Normal file
View File

@@ -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%

93
pom.xml Normal file
View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>ru.resprojects</groupId>
<artifactId>restsrv</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>restsrv</name>
<description>Test task project for Mesh group</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Databases-->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
</dependency>
<!-- OpenApi -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.4.4</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-security</artifactId>
<version>1.4.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>restsrv</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

101
restsrv_linux.sh Executable file
View File

@@ -0,0 +1,101 @@
#!/bin/bash
EXECUTABLE_FILE=restsrv.jar
PROPERTIES_FILE=application-prod.properties
HELP="Usage: restsrv_linux [KEY]
Script without key is run program in DEFAULT mode.
DEFAULT mode uses postgresql with next parameters:
url: jdbc:postgresql://localhost/test
username: test
password: test
Also available next switches:
--prod - running program in PRODUCTION mode. For running in this mode needed additional
file application-prod.properties with PostgreSQL dataset information.
--demo - running program in DEMO mode. In this mode uses H2 database (in-memory) instead postgresql.
--help - display this is message
Examples:
restsrv_linux - run program in DEFAULT mode
restsrv_linux --demo - run program in DEMO mode.
restsrv_linux --prod - run program in PRODUCTION mode.
"
PROPERTIES_FILE_NOT_FOUND="
WARNING!
You try run program in PRODUCTION mode. For this mode need PostgreSQL but file
$PROPERTIES_FILE with dataset information is not found. Please fill next information and run program again!
"
if [ -f "$EXECUTABLE_FILE" ]; then
if [ -z "$1" ]; then
echo "Running program in DEFAULT mode"
java -jar "$EXECUTABLE_FILE"
else
case "$1" in
--help)
echo "$HELP"
;;
--demo)
echo "Running program in DEMO mode with H2 DB (in-memory)"
java -jar "$EXECUTABLE_FILE" --spring.profiles.active=demo
;;
--prod)
if [ -f "$PROPERTIES_FILE" ]; then
echo "Running program in PRODUCTION mode with PostgreSQL DB"
java -jar "$EXECUTABLE_FILE" --spring.profiles.active=prod
else
echo "$PROPERTIES_FILE_NOT_FOUND"
printf 'PostgreSQL database host name or IP address (default localhost): '
read -r RESTSRV_PGSQL_DB_HOST
if [ -z "$RESTSRV_PGSQL_DB_HOST" ]; then
RESTSRV_PGSQL_DB_HOST="jdbc:postgresql://localhost"
else
RESTSRV_PGSQL_DB_HOST="jdbc:postgresql://$RESTSRV_PGSQL_DB_HOST"
fi
printf 'PostgreSQL database port (default 5432): '
read -r RESTSRV_PGSQL_DB_PORT
if [ -z "$RESTSRV_PGSQL_DB_PORT" ]; then
RESTSRV_PGSQL_DB_PORT=5432
fi
printf 'PostgreSQL database name (default test): '
read -r RESTSRV_PGSQL_DB_NAME
if [ -z "$RESTSRV_PGSQL_DB_NAME" ]; then
RESTSRV_PGSQL_DB_NAME="test"
fi
printf 'PostgreSQL database user name: '
read -r RESTSRV_PGSQL_DB_USER
printf 'PostgreSQL database password: '
read -r -s RESTSRV_PGSQL_DB_PASSWORD
echo
touch "$PROPERTIES_FILE"
{
echo "RESTSRV_PGSQL_DB_HOST=$RESTSRV_PGSQL_DB_HOST"
echo "RESTSRV_PGSQL_DB_PORT=$RESTSRV_PGSQL_DB_PORT"
echo "RESTSRV_PGSQL_DB_NAME=$RESTSRV_PGSQL_DB_NAME"
echo "RESTSRV_PGSQL_DB_USER=$RESTSRV_PGSQL_DB_USER"
echo "RESTSRV_PGSQL_DB_PASSWORD=$RESTSRV_PGSQL_DB_PASSWORD"
} > "$PROPERTIES_FILE"
fi
;;
*)
echo "restsrv_linux: unknown option $1"
echo "Try 'restsrv_linux --help' for more information."
;;
esac
fi
else
echo "Executable file restsrv.jar is not found!"
fi

View File

@@ -0,0 +1,13 @@
package ru.resprojects.restsrv;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RestsrvApplication {
public static void main(String[] args) {
SpringApplication.run(RestsrvApplication.class, args);
}
}

View File

@@ -0,0 +1,17 @@
package ru.resprojects.restsrv.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ru.resprojects.restsrv.token.TokenResourceDetails;
@Configuration
public class AppConfig {
@Bean
@ConfigurationProperties("auth")
public TokenResourceDetails tokenResourceDetails() {
return new TokenResourceDetails();
}
}

View File

@@ -0,0 +1,23 @@
package ru.resprojects.restsrv.config;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
@Component
public class TokenAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = 7782026919358529193L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}

View File

@@ -0,0 +1,48 @@
package ru.resprojects.restsrv.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import ru.resprojects.restsrv.service.AuthDetailsService;
import ru.resprojects.restsrv.token.TokenResourceDetails;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class TokenRequestsFilter extends OncePerRequestFilter {
public static final String SECURITY_SCHEME = "Bearer";
private final AuthDetailsService authDetailsService;
private final TokenResourceDetails tokenResourceDetails;
public TokenRequestsFilter(AuthDetailsService authDetailsService, TokenResourceDetails tokenResourceDetails) {
this.authDetailsService = authDetailsService;
this.tokenResourceDetails = tokenResourceDetails;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String requestHeader = request.getHeader("Authorization");
String token = null;
if (requestHeader != null && requestHeader.startsWith(SECURITY_SCHEME + " ")) {
token = requestHeader.substring(SECURITY_SCHEME.length() + 1);
}
if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = authDetailsService.loadUserByUsername(tokenResourceDetails.getUserByToken(token));
UsernamePasswordAuthenticationToken usernameAuthToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernameAuthToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernameAuthToken);
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,72 @@
package ru.resprojects.restsrv.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import ru.resprojects.restsrv.service.AuthDetailsService;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_WHITELIST = {
// swagger ui
"/v3/api-docs/**",
"/swagger-ui.html",
"/swagger-ui/**"
};
private final TokenRequestsFilter tokenRequestsFilter;
private final AuthDetailsService authDetailsService;
private final TokenAuthenticationEntryPoint tokenAuthenticationEntryPoint;
public WebSecurityConfig(
TokenRequestsFilter tokenRequestsFilter,
AuthDetailsService authDetailsService, TokenAuthenticationEntryPoint tokenAuthenticationEntryPoint) {
this.tokenRequestsFilter = tokenRequestsFilter;
this.authDetailsService = authDetailsService;
this.tokenAuthenticationEntryPoint = tokenAuthenticationEntryPoint;
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(authDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers(AUTH_WHITELIST)
.permitAll()
.antMatchers("/**")
.authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(tokenAuthenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(tokenRequestsFilter, UsernamePasswordAuthenticationFilter.class);
}
}

View File

@@ -0,0 +1,51 @@
package ru.resprojects.restsrv.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.resprojects.restsrv.exception.LastException;
@RestController
@RequestMapping("/error")
@Schema(name = "/error")
@Tag(name ="Profiles", description = "the profiles API with documentation annotations")
@SecurityScheme(
name = "customTokenAuth",
type = SecuritySchemeType.HTTP,
in = SecuritySchemeIn.HEADER,
scheme = "bearer",
description = "Авторизация при помощи токена доступа. В HEADER запроса должна присутствовать строка вида Authorization: Bearer "
)
public class ErrorController {
@Operation(
summary = "Получить последнюю ошибку",
description = "Возвращает информацию о последней ошибке.",
security = @SecurityRequirement(name = "customTokenAuth"),
tags = { "profile" }
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "успешная операция",
content = @Content(schema = @Schema(implementation = LastException.class), mediaType = MediaType.APPLICATION_JSON_VALUE)
)
})
@GetMapping(value = "/last", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<LastException> getLastException() {
return ResponseEntity.ok(RestExceptionHandler.getLastException());
}
}

View File

@@ -0,0 +1,160 @@
package ru.resprojects.restsrv.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RestController;
import ru.resprojects.restsrv.dto.ProfileDto;
import ru.resprojects.restsrv.dto.ProfileIdDto;
import ru.resprojects.restsrv.dto.EmailDto;
import ru.resprojects.restsrv.exception.ErrorMessage;
import ru.resprojects.restsrv.model.Profile;
import ru.resprojects.restsrv.service.ProfileService;
import java.util.List;
@RestController
@RequestMapping("/profiles")
@Schema(name = "/profiles")
@Tag(name ="Profiles", description = "the profiles API with documentation annotations")
@SecurityScheme(
name = "customTokenAuth",
type = SecuritySchemeType.HTTP,
in = SecuritySchemeIn.HEADER,
scheme = "bearer",
description = "Авторизация при помощи токена доступа. В HEADER запроса должна присутствовать строка вида Authorization: Bearer "
)
public class ProfileController {
private final ProfileService profileService;
public ProfileController(ProfileService profileService) {
this.profileService = profileService;
}
@Operation(
summary = "Получить список профилей",
description = "Возвращает все созданные профили",
security = @SecurityRequirement(name = "customTokenAuth"),
tags = { "profile" }
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "успешная операция",
content = @Content(
array = @ArraySchema(schema = @Schema(implementation = Profile.class)),
mediaType = MediaType.APPLICATION_JSON_VALUE
)
)
})
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<Profile>> finaAll() {
return ResponseEntity.ok(profileService.findAll());
}
@Operation(
summary = "Поиск профиля по ID",
description = "Возвращает профиль с заданным ID",
security = @SecurityRequirement(name = "customTokenAuth"),
tags = { "profile" }
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Успешная операция",
content = @Content(schema = @Schema(implementation = Profile.class), mediaType = MediaType.APPLICATION_JSON_VALUE)),
@ApiResponse(responseCode = "404", description = "Профиль не найден",
content = @Content(schema = @Schema(implementation = ErrorMessage.class), mediaType = MediaType.APPLICATION_JSON_VALUE)
) })
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Profile> findById(
@Parameter(description="Id профиля.", required=true)
@PathVariable("id") Integer id) {
Profile profile = profileService.findById(id);
return ResponseEntity.ok(profile);
}
@Operation(
summary = "Поиск профиля по email",
description = "Возвращает профиль по указанному email",
security = @SecurityRequirement(name = "customTokenAuth"),
tags = { "profile" }
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Успешная операция",
content = @Content(schema = @Schema(implementation = Profile.class), mediaType = MediaType.APPLICATION_JSON_VALUE)),
@ApiResponse(responseCode = "404", description = "Профиль не найден",
content = @Content(schema = @Schema(implementation = ErrorMessage.class), mediaType = MediaType.APPLICATION_JSON_VALUE))
})
@PostMapping(value = "/get", consumes = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<Profile> findByEmail(
@Parameter(description="Email по которому необходимо произвести поиск.",
required=true, schema=@Schema(implementation = EmailDto.class))
@RequestBody EmailDto emailDto) {
Profile profile = profileService.findByEmail(emailDto.getEmail());
return ResponseEntity.ok(profile);
}
@Operation(
summary = "Получить последний созданный профиль",
description = "Возвращает последний созданный профиль",
security = @SecurityRequirement(name = "customTokenAuth"),
tags = { "profile" }
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "успешная операция",
content = @Content(schema = @Schema(implementation = Profile.class), mediaType = MediaType.APPLICATION_JSON_VALUE)
)
})
@GetMapping(value = "/last", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Profile> getLastProfile() {
Profile profile = profileService.getLastCreatedProfile();
return ResponseEntity.ok(profile);
}
@Operation(
summary = "Создание нового профиля",
description = "Создает профиль и возвращается его id",
security = @SecurityRequirement(name = "customTokenAuth"),
tags = { "profile" }
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Успешная операция",
content = @Content(schema = @Schema(implementation = ProfileIdDto.class), mediaType = MediaType.APPLICATION_JSON_VALUE)),
@ApiResponse(responseCode = "400", description = "Передан некорректный email добавляемого профиля",
content = @Content(schema = @Schema(implementation = ErrorMessage.class), mediaType = MediaType.APPLICATION_JSON_VALUE)),
@ApiResponse(responseCode = "403", description = "Email добавляемого профиля уже существует",
content = @Content(schema = @Schema(implementation = ErrorMessage.class), mediaType = MediaType.APPLICATION_JSON_VALUE))
})
@PostMapping(value = "/set", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ProfileIdDto> save(
@Parameter(description="Данные нового профиля",
required=true, schema=@Schema(implementation = ProfileDto.class))
@RequestBody ProfileDto profileDto) {
Profile profile = new Profile();
profile.setName(profileDto.getName());
profile.setAge(profileDto.getAge());
profile.setEmail(profileDto.getEmail());
Profile newProfile = profileService.save(profile);
ProfileIdDto profileIdDto = new ProfileIdDto(newProfile.getId());
return ResponseEntity.ok(profileIdDto);
}
}

View File

@@ -0,0 +1,48 @@
package ru.resprojects.restsrv.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import ru.resprojects.restsrv.exception.BadResourceException;
import ru.resprojects.restsrv.exception.ErrorMessage;
import ru.resprojects.restsrv.exception.LastException;
import ru.resprojects.restsrv.exception.ResourceAlreadyExistsException;
import ru.resprojects.restsrv.exception.ResourceNotFoundException;
import javax.servlet.http.HttpServletRequest;
import java.sql.Timestamp;
@RestControllerAdvice
public class RestExceptionHandler {
private static LastException lastException;
@ExceptionHandler(value = {ResourceNotFoundException.class})
public ResponseEntity<ErrorMessage> resourceNotFound(HttpServletRequest request, ResourceNotFoundException exception) {
setLastException(exception.getErrorMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getErrorMessage());
}
@ExceptionHandler(value = {BadResourceException.class})
public ResponseEntity<ErrorMessage> badResource(HttpServletRequest request, BadResourceException exception) {
setLastException(exception.getErrorMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getErrorMessage());
}
@ExceptionHandler(value = {ResourceAlreadyExistsException.class})
public ResponseEntity<ErrorMessage> alreadyExistResource(HttpServletRequest request, ResourceAlreadyExistsException exception) {
setLastException(exception.getErrorMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(exception.getErrorMessage());
}
private static void setLastException(ErrorMessage errorMessage) {
lastException = new LastException(errorMessage.getMessage(), new Timestamp(System.currentTimeMillis()));
}
public static LastException getLastException() {
return lastException;
}
}

View File

@@ -0,0 +1,20 @@
package ru.resprojects.restsrv.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
public class EmailDto implements Serializable {
private static final long serialVersionUID = -5708728999488347598L;
@Schema(description = "Email для поиска профиля")
private String email;
public EmailDto() {
}
}

View File

@@ -0,0 +1,33 @@
package ru.resprojects.restsrv.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
public class ProfileDto implements Serializable {
private static final long serialVersionUID = 8315646868618789737L;
@Schema(description = "Имя пользователя",
example = "Alex", required = false)
@JsonProperty("name")
private String name;
@Schema(description = "E-mail пользователя",
example = "user@example.com", required = true)
@JsonProperty("email")
private String email;
@Schema(description = "Возраст пользователя",
example = "30", required = false)
@JsonProperty("age")
private Integer age;
public ProfileDto() {
}
}

View File

@@ -0,0 +1,24 @@
package ru.resprojects.restsrv.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
@AllArgsConstructor
public class ProfileIdDto implements Serializable {
private static final long serialVersionUID = -7924116702230840180L;
@Schema(description = "ID созданного профиля")
@JsonProperty("idUser")
private Integer userId;
public ProfileIdDto() {
}
}

View File

@@ -0,0 +1,18 @@
package ru.resprojects.restsrv.exception;
public class BadResourceException extends RuntimeException {
private static final long serialVersionUID = 5658497082104293714L;
public BadResourceException() {
}
public BadResourceException(String msg) {
super(msg);
}
public ErrorMessage getErrorMessage() {
return new ErrorMessage(this.getMessage());
}
}

View File

@@ -0,0 +1,24 @@
package ru.resprojects.restsrv.exception;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
@AllArgsConstructor
public class ErrorMessage implements Serializable {
private static final long serialVersionUID = -814379271893846783L;
@Schema(description = "Сообщение о ошибке")
@JsonProperty("msg")
private String message;
public ErrorMessage() {
}
}

View File

@@ -0,0 +1,28 @@
package ru.resprojects.restsrv.exception;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.sql.Timestamp;
@Getter
@Setter
@AllArgsConstructor
public class LastException implements Serializable {
private static final long serialVersionUID = -1153271692647620929L;
@Schema(description = "Сообщение о ошибки")
@JsonProperty("msg")
private String message;
@Schema(description = "Дата и время возникновения ошибки")
@JsonProperty("created")
private Timestamp created;
public LastException() {
}
}

View File

@@ -0,0 +1,18 @@
package ru.resprojects.restsrv.exception;
public class ResourceAlreadyExistsException extends RuntimeException {
private static final long serialVersionUID = -2318810158739700082L;
public ResourceAlreadyExistsException() {
}
public ResourceAlreadyExistsException(String msg) {
super(msg);
}
public ErrorMessage getErrorMessage() {
return new ErrorMessage(this.getMessage());
}
}

View File

@@ -0,0 +1,18 @@
package ru.resprojects.restsrv.exception;
public class ResourceNotFoundException extends RuntimeException {
private static final long serialVersionUID = -4368442279968350909L;
public ResourceNotFoundException() {
}
public ResourceNotFoundException(String msg) {
super(msg);
}
public ErrorMessage getErrorMessage() {
return new ErrorMessage(this.getMessage());
}
}

View File

@@ -0,0 +1,79 @@
package ru.resprojects.restsrv.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PrePersist;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import java.io.Serializable;
import java.sql.Timestamp;
@Entity
@Table(name = "profile")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Getter
@Setter
@ToString
public class Profile implements Serializable {
private static final long serialVersionUID = 4048798961366546485L;
private static final int START_SEQ = 5000;
@Schema(description = "Unique identifier of the Profile.",
example = "1", required = true)
@Id
@SequenceGenerator(name = "global_seq", sequenceName = "global_seq",
allocationSize = 1, initialValue = START_SEQ)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq")
private Integer id;
@Schema(description = "Имя пользователя",
example = "Alex", required = false)
private String name;
@Schema(description = "E-mail пользователя",
example = "user@example.com", required = true)
private String email;
@Schema(description = "Возраст пользователя",
example = "30", required = false)
private Integer age;
@Schema(description = "Дата и время создания профиля",
example = "2020-08-24T10:16:17.929+00:00", required = false)
private Timestamp created;
public Profile() {
}
@PrePersist
public void prePersist() {
if (created == null) {
created = new Timestamp(System.currentTimeMillis());
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Profile profile = (Profile) o;
return id.equals(profile.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
}

View File

@@ -0,0 +1,15 @@
package ru.resprojects.restsrv.repository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import ru.resprojects.restsrv.model.Profile;
import java.util.List;
@Repository
public interface ProfileRepository extends CrudRepository<Profile, Integer> {
List<Profile> findByEmailContainingIgnoreCase(String email);
Boolean existsByEmailContainingIgnoreCase(String email);
}

View File

@@ -0,0 +1,25 @@
package ru.resprojects.restsrv.service;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@Service
public class AuthDetailsService implements UserDetailsService {
private static final String INMEMORY_USER = "user";
private static final String INMEMORY_PASSWORD = "$2y$12$jj9Q40qh3NOokcPjIg2cFuFK/7jBZlZ/RcrEbXkOALRv88hcuLF5a";
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (INMEMORY_USER.equals(username)) {
return new User(INMEMORY_USER, INMEMORY_PASSWORD, new ArrayList<>());
} else {
throw new UsernameNotFoundException("Authorization error! User " + username + " not found!");
}
}
}

View File

@@ -0,0 +1,64 @@
package ru.resprojects.restsrv.service;
import org.springframework.stereotype.Service;
import ru.resprojects.restsrv.exception.BadResourceException;
import ru.resprojects.restsrv.exception.ResourceAlreadyExistsException;
import ru.resprojects.restsrv.exception.ResourceNotFoundException;
import ru.resprojects.restsrv.model.Profile;
import ru.resprojects.restsrv.repository.ProfileRepository;
import java.util.ArrayList;
import java.util.List;
import static ru.resprojects.restsrv.util.ValidationUtil.isEmailValid;
@Service
public class ProfileService {
private final ProfileRepository repository;
private Profile lastCreatedProfile;
public ProfileService(ProfileRepository repository) {
this.repository = repository;
}
public Profile findById(Integer id) throws ResourceNotFoundException {
return repository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Cannot find profile with id: " + id));
}
public List<Profile> findAll() {
List<Profile> profiles = new ArrayList<>();
repository.findAll().forEach(profiles::add);
return profiles;
}
public Profile findByEmail(String email) throws ResourceNotFoundException {
List<Profile> profiles = repository.findByEmailContainingIgnoreCase(email);
if (!profiles.isEmpty()) {
return profiles.get(0);
} else {
throw new ResourceNotFoundException("Cannot find profile with email: " + email);
}
}
public Profile save(Profile profile) throws BadResourceException, ResourceAlreadyExistsException {
if (profile == null) {
throw new BadResourceException("Failed to save profile. Profile is null.");
}
String email = profile.getEmail();
if (!isEmailValid(email)) {
throw new BadResourceException("Failed to save profile. Email " + email + " is not valid.");
}
if (repository.existsByEmailContainingIgnoreCase(email)) {
throw new ResourceAlreadyExistsException("Profile with email: " + profile.getEmail() + " already exists.");
}
lastCreatedProfile = repository.save(profile);
return lastCreatedProfile;
}
public Profile getLastCreatedProfile() {
return lastCreatedProfile;
}
}

View File

@@ -0,0 +1,20 @@
package ru.resprojects.restsrv.token;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class TokenResourceDetails {
private String token;
private String user;
public String getUserByToken(String token) {
if (!this.token.equals(token)) {
return null;
}
return user;
}
}

View File

@@ -0,0 +1,19 @@
package ru.resprojects.restsrv.util;
import java.util.regex.Pattern;
public final class ValidationUtil {
private ValidationUtil() {
}
// https://stackoverflow.com/a/48725527
public static boolean isEmailValid(String email) {
final Pattern EMAIL_REGEX = Pattern.compile(
"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?",
Pattern.CASE_INSENSITIVE
);
return EMAIL_REGEX.matcher(email).matches();
}
}

View File

@@ -0,0 +1,65 @@
server:
port: 8010
spring:
profiles:
active: pgsql
---
spring:
profiles: pgsql, prod
jpa:
database: postgresql
generate-ddl: true
properties:
hibernate:
jdbc:
lob:
non_contextual_creation: true
database-platform: org.hibernate.dialect.PostgreSQL9Dialect
open-in-view: false
datasource:
platform: postgresql
initialization-mode: never
---
spring:
profiles: pgsql
datasource:
url: jdbc:postgresql://localhost/test
username: test
password: test
---
spring:
profiles: prod
datasource:
url: ${RESTSRV_PGSQL_DB_HOST}:${RESTSRV_PGSQL_DB_PORT}/${RESTSRV_PGSQL_DB_NAME}
username: ${RESTSRV_PGSQL_DB_USER}
password: ${RESTSRV_PGSQL_DB_PASSWORD}
---
spring:
profiles: test, demo
jpa:
database: h2
open-in-view: false
hibernate:
ddl-auto: none
database-platform: org.hibernate.dialect.H2Dialect
datasource:
url: jdbc:h2:mem:restsrv;DB_CLOSE_ON_EXIT=FALSE
initialization-mode: always
platform: h2
---
logging:
level:
ru.resprojects: debug
org.springframework.transaction: debug
org.springframework: error
pattern:
file: "%d %p %c{1.} [%t] %m%n"
console: "%clr(%d{HH:mm:ss.SSS}){yellow} %clr(%-5p) %clr(---){faint} %clr([%t]){cyan} %clr(%logger{36}){blue} %clr(:){red} %clr(%msg){faint}%n"
file:
name: restsrv.log
max-size: 5MB
auth:
token: secret
user: user

View File

@@ -0,0 +1,6 @@
DELETE FROM profile;
ALTER SEQUENCE global_seq RESTART WITH 5000;
INSERT INTO profile (name, email, age, created) VALUES
('h2user1', 'h2user1@example.com', 10, now()),
('h2user2', 'h2user2@example.com', 20, now());

View File

@@ -0,0 +1,6 @@
DELETE FROM profile;
ALTER SEQUENCE global_seq RESTART WITH 5000;
INSERT INTO profile (name, email, age) VALUES
('user1', 'user1@example.com', 20),
('user2', 'user2@example.com', 30);

View File

@@ -0,0 +1,13 @@
DROP TABLE IF EXISTS profile;
DROP SEQUENCE IF EXISTS global_seq;
CREATE SEQUENCE global_seq MINVALUE 5000;
CREATE TABLE profile (
id INT DEFAULT global_seq.nextval PRIMARY KEY,
name VARCHAR NOT NULL,
email VARCHAR NOT NULL,
age INT DEFAULT 0 NOT NULL,
created TIMESTAMP
);
CREATE UNIQUE INDEX profile_unique_email_idx ON profile(email);

View File

@@ -0,0 +1,13 @@
DROP TABLE IF EXISTS profile;
DROP SEQUENCE IF EXISTS global_seq CASCADE;
CREATE SEQUENCE global_seq START 5000;
CREATE TABLE profile (
id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
name VARCHAR NOT NULL,
email VARCHAR NOT NULL,
age INTEGER DEFAULT 0 NOT NULL,
created TIMESTAMP DEFAULT now()::timestamp
);
CREATE UNIQUE INDEX profile_unique_email_idx ON profile(email);

View File

@@ -0,0 +1,44 @@
package ru.resprojects.restsrv;
import org.junit.Test;
import ru.resprojects.restsrv.util.ValidationUtil;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class ValidationUtilTest {
@Test
public void whenPassCorrectEmailToValidatorThenReturnTrue() {
List<String> emails = List.of(
"john@somewhere.com",
"john.foo@somewhere.com",
"john.foo+label@somewhere.com",
"john@192.168.1.10",
"john+label@192.168.1.10",
"john.foo@someserver",
"JOHN.FOO@somewhere.com"
);
for (String email : emails) {
assertThat(ValidationUtil.isEmailValid(email)).isTrue();
}
}
@Test
public void whenPassIncorrectEmailToValidatorThenReturnFalse() {
List<String> emails = List.of(
"@someserver",
"@someserver.com",
"john@.",
".@somewhere.com",
".@.somewhere.com"
);
for (String email : emails) {
assertThat(ValidationUtil.isEmailValid(email)).isFalse();
}
}
}

View File

@@ -0,0 +1,196 @@
package ru.resprojects.restsrv.controller;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.context.junit4.SpringRunner;
import ru.resprojects.restsrv.config.TokenRequestsFilter;
import ru.resprojects.restsrv.dto.EmailDto;
import ru.resprojects.restsrv.dto.ProfileDto;
import ru.resprojects.restsrv.dto.ProfileIdDto;
import ru.resprojects.restsrv.exception.ErrorMessage;
import ru.resprojects.restsrv.exception.LastException;
import ru.resprojects.restsrv.model.Profile;
import ru.resprojects.restsrv.repository.ProfileRepository;
import ru.resprojects.restsrv.token.TokenResourceDetails;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles(profiles = {"test"})
@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
scripts = {"classpath:schema-h2.sql"},
config = @SqlConfig(encoding = "UTF-8"))
public class ProfileControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private ProfileRepository repository;
@Autowired
private TokenResourceDetails tokenResourceDetails;
private Profile exampleProfile;
private final HttpHeaders headers = new HttpHeaders();
@Before
public void init() {
headers.add("Authorization", TokenRequestsFilter.SECURITY_SCHEME + " " + tokenResourceDetails.getToken());
Profile profile = new Profile();
profile.setName("Alex");
profile.setEmail("alex@example.com");
profile.setAge(10);
exampleProfile = repository.save(profile);
}
@Test
public void whenFindByExistentIdThenStatus200AndReturnProfile() {
HttpEntity<String> entity = new HttpEntity<>(null, headers);
ResponseEntity<Profile> profile = restTemplate.exchange("/profiles/5000", HttpMethod.GET, entity, Profile.class);
assertThat(profile.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(profile.getBody()).isNotNull();
assertThat(profile.getBody().getName()).isEqualTo(exampleProfile.getName());
}
@Test
public void whenRequestAllProfilesThenStatus200AndReturnListOfProfiles() {
HttpEntity<String> entity = new HttpEntity<>(null, headers);
ResponseEntity<Profile[]> profiles = restTemplate.exchange("/profiles", HttpMethod.GET, entity, Profile[].class);
assertThat(profiles.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(profiles.getBody()).isNotNull();
assertThat(profiles.getBody()).isNotEmpty();
assertThat(profiles.getBody()[0].getName()).isEqualTo(exampleProfile.getName());
}
@Test
public void whenFindProfileByEmailThenStatus200AndReturnProfile() {
EmailDto emailDto = new EmailDto();
emailDto.setEmail("alex@example.com");
HttpEntity<EmailDto> entity = new HttpEntity<>(emailDto, headers);
ResponseEntity<Profile> profile = restTemplate.exchange("/profiles/get", HttpMethod.POST, entity, Profile.class);
assertThat(profile.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(profile.getBody()).isNotNull();
assertThat(profile.getBody().getName()).isEqualTo(exampleProfile.getName());
assertThat(profile.getBody().getCreated()).isNotNull();
}
@Test
public void whenCreateProfileThenStatus200AndReturnIdSavedProfile() {
ProfileDto profileDto = new ProfileDto();
profileDto.setName("John");
profileDto.setAge(10);
profileDto.setEmail("john@gmail.com");
HttpEntity<ProfileDto> entity = new HttpEntity<>(profileDto, headers);
ResponseEntity<ProfileIdDto> newProfileId = restTemplate.exchange("/profiles/set", HttpMethod.POST, entity, ProfileIdDto.class);
assertThat(newProfileId.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(newProfileId.getBody()).isNotNull();
assertThat(newProfileId.getBody().getUserId()).isEqualTo(5001);
}
@Test
public void whenRequestLastCreatedProfileThenStatus200AndReturnLastSavedProfile() {
ProfileDto profileDto = new ProfileDto();
profileDto.setName("John");
profileDto.setAge(10);
profileDto.setEmail("john@gmail.com");
HttpEntity<ProfileDto> entity = new HttpEntity<>(profileDto, headers);
ResponseEntity<ProfileIdDto> newProfileId = restTemplate.exchange("/profiles/set", HttpMethod.POST, entity, ProfileIdDto.class);
assertThat(newProfileId.getBody()).isNotNull();
HttpEntity<Void> newEntity = new HttpEntity<>(null, headers);
ResponseEntity<Profile> profile = restTemplate.exchange("/profiles/last", HttpMethod.GET, newEntity, Profile.class);
assertThat(profile.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(profile.getBody()).isNotNull();
assertThat(profile.getBody().getId()).isEqualTo(newProfileId.getBody().getUserId());
}
@Test
public void whenRequestProfileWithNonexistentIdThenStatus404AndReturnErrorMessage() {
HttpEntity<String> entity = new HttpEntity<>(null, headers);
ResponseEntity<ErrorMessage> errorMessage = restTemplate.exchange("/profiles/5010", HttpMethod.GET, entity, ErrorMessage.class);
assertThat(errorMessage.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(errorMessage.getBody()).isNotNull();
}
@Test
public void whenRequestProfileWithNonexistentEmailThenStatus404AndReturnErrorMessage() {
EmailDto emailDto = new EmailDto();
emailDto.setEmail("test@example.com");
HttpEntity<EmailDto> entity = new HttpEntity<>(emailDto, headers);
ResponseEntity<ErrorMessage> errorMessage = restTemplate.exchange("/profiles/get", HttpMethod.POST, entity, ErrorMessage.class);
assertThat(errorMessage.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(errorMessage.getBody()).isNotNull();
}
@Test
public void whenCreatProfileWithIncorrectEmailThenReturnStatus400AndErrorMessage() {
ProfileDto profileDto = new ProfileDto();
profileDto.setName("John");
profileDto.setAge(10);
profileDto.setEmail(".@gmail.com");
HttpEntity<ProfileDto> entity = new HttpEntity<>(profileDto, headers);
ResponseEntity<ErrorMessage> errorMessage = restTemplate.exchange("/profiles/set", HttpMethod.POST, entity, ErrorMessage.class);
assertThat(errorMessage.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(errorMessage.getBody()).isNotNull();
}
@Test
public void whenCreatProfileWithExistentEmailThenReturnStatus403AndErrorMessage() {
ProfileDto profileDto = new ProfileDto();
profileDto.setName("John");
profileDto.setAge(10);
profileDto.setEmail("alex@example.com");
HttpEntity<ProfileDto> entity = new HttpEntity<>(profileDto, headers);
ResponseEntity<ErrorMessage> errorMessage = restTemplate.exchange("/profiles/set", HttpMethod.POST, entity, ErrorMessage.class);
assertThat(errorMessage.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
assertThat(errorMessage.getBody()).isNotNull();
}
@Test
public void whenRequestLastErrorThenStatus200AndReturnMessageWithLastError() {
HttpEntity<String> entity = new HttpEntity<>(null, headers);
ResponseEntity<ErrorMessage> errorMessage = restTemplate.exchange("/profiles/5010", HttpMethod.GET, entity, ErrorMessage.class);
assertThat(errorMessage.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(errorMessage.getBody()).isNotNull();
ResponseEntity<LastException> lastErrorMessage = restTemplate.exchange("/error/last", HttpMethod.GET, entity, LastException.class);
assertThat(lastErrorMessage.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(lastErrorMessage.getBody()).isNotNull();
assertThat(lastErrorMessage.getBody().getMessage()).isEqualTo(errorMessage.getBody().getMessage());
assertThat(lastErrorMessage.getBody().getCreated()).isNotNull();
}
}

View File

@@ -0,0 +1,133 @@
package ru.resprojects.restsrv.service;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.context.junit4.SpringRunner;
import ru.resprojects.restsrv.RestsrvApplication;
import ru.resprojects.restsrv.exception.BadResourceException;
import ru.resprojects.restsrv.exception.ResourceAlreadyExistsException;
import ru.resprojects.restsrv.exception.ResourceNotFoundException;
import ru.resprojects.restsrv.model.Profile;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RestsrvApplication.class)
@ActiveProfiles(profiles = {"test"})
@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
scripts = {"classpath:schema-h2.sql"},
config = @SqlConfig(encoding = "UTF-8"))
public class ProfileServiceTest {
@Autowired
private ProfileService profileService;
private Profile exampleProfile;
@Before
public void init() {
Profile profile = new Profile();
profile.setName("h2user1");
profile.setEmail("h2user1@example.com");
profile.setAge(20);
exampleProfile = profileService.save(profile);
}
@Test
public void whenSaveNewProfileThenReturnedProfileWithId() {
Profile exampleProfile = new Profile();
exampleProfile.setName("Alex");
exampleProfile.setEmail("alex@example.com");
exampleProfile.setAge(20);
Profile newProfile = profileService.save(exampleProfile);
assertThat(newProfile).isNotNull();
assertThat(newProfile.getId()).isNotNull();
}
@Test
public void whenFindByIdThenReturnedProfile() {
Profile profile = profileService.findById(5000);
assertThat(profile).isNotNull();
}
@Test
public void whenFindByEmailThenReturnedProfile() {
Profile profile = profileService.findByEmail("h2user1@example.com");
assertThat(profile).isNotNull();
}
@Test
public void whenFindByEmailCaseInsensitiveThenReturnedProfile() {
Profile profile = profileService.findByEmail("H2uSer1@eXAmple.com");
assertThat(profile).isNotNull();
}
@Test
public void whenGetLastCreatedProfileThenReturnedLastSavedProfile() {
Profile profile = profileService.getLastCreatedProfile();
assertThat(profile).isNotNull();
assertThat(profile).isEqualTo(exampleProfile);
}
@Test
public void whenFindAllThenReturnedAllProfiles() {
List<Profile> profiles = profileService.findAll();
assertThat(profiles).isNotEmpty();
}
@Test(expected = ResourceNotFoundException.class)
public void whenFindByNonexistentIdWhenException() {
profileService.findById(5010);
}
@Test(expected = ResourceNotFoundException.class)
public void whenFindByNonexistentEmailThenException() {
profileService.findByEmail("test@test.com");
}
@Test(expected = BadResourceException.class)
public void whenSaveProfileWithIncorrectEmailThenException() {
Profile exampleProfile = new Profile();
exampleProfile.setName("Alex");
exampleProfile.setEmail(".@example.com");
exampleProfile.setAge(20);
profileService.save(exampleProfile);
}
@Test(expected = ResourceAlreadyExistsException.class)
public void whenSaveProfileWithExistEmailThenException() {
Profile exampleProfile = new Profile();
exampleProfile.setName("Alex");
exampleProfile.setEmail("h2user1@example.com");
exampleProfile.setAge(20);
profileService.save(exampleProfile);
}
@Test(expected = ResourceAlreadyExistsException.class)
public void whenSaveProfileWithExistEmailCaseInsensitiveThenException() {
Profile exampleProfile = new Profile();
exampleProfile.setName("Alex");
exampleProfile.setEmail("H2user1@example.com");
exampleProfile.setAge(20);
profileService.save(exampleProfile);
}
}