diff --git a/.env b/.env new file mode 100644 index 000000000..61b533da1 --- /dev/null +++ b/.env @@ -0,0 +1,19 @@ +MAIL_USERNAME=ramist4jr@gmail.com +MAIL_PASSWORD=keosdsdvkcbkqeoe +MAIL_TEST=ramist4jr@gmail.com + +CLIENT_SECRET_GITLAB=e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004 +CLIENT_SECRET_YANDEX=ed236c501e444a609b0f419e5e88f1e1 +CLIENT_SECRET_GOOGLE=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 +CLIENT_SECRET_GITHUB=d7bf69a8670d800bda28dff647278ab1cd5f4a16 + +CLIENT_ID_YANDEX=2f3395214ba84075956b76a34b231985 +CLIENT_ID_GITHUB=Ov23li1vRbEW9L5qj7r4 +CLIENT_ID_GITLAB=b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494 +CLIENT_ID_GOOGLE=413028536182-oe55n414gtnga6a95fosu4vfrombl3vm.apps.googleusercontent.com + + +DB_URL=jdbc:postgresql://localhost:5432/jira +DB_USER=jira +DB_PASSWORD=JiraRush +DB_NAME=jira \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..ffcab66aa --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..26eeb5d8f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM eclipse-temurin:17-jdk-alpine AS builder +WORKDIR /app +COPY .mvn ./.mvn +COPY mvnw mvnw +COPY pom.xml . +COPY src ./src +COPY resources/mails src/main/resources/mails/ +COPY resources/view src/main/resources/view/ +COPY resources/static src/main/resources/static/ +COPY resources/robots.txt src/main/resources/ +RUN ./mvnw clean package -DskipTests + +FROM eclipse-temurin:17-jre-alpine +WORKDIR /app +RUN apk add --no-cache wget +COPY --from=builder /app/target/jira-1.0.jar app.jar +COPY --from=builder /app/src/main/resources/mails ./resources/mails/ +COPY --from=builder /app/src/main/resources/view ./resources/view/ +COPY --from=builder /app/src/main/resources/static ./resources/static/ +COPY --from=builder /app/src/main/resources/robots.txt ./resources/ +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 719b268f5..b948c2f61 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,31 @@ +## Инструкция по запуску + 1) Склонировать проект + 2) В корне проекта выполнить в терминале docker-compose up (должен быть установлен докер) + 3) Всё. Можно открывать http://localhost + +## Проделанные работы: + - Удалить (Easy task) не работающие социальные сети или настроить их (Hard task) + Удалил авторизацию через vk, т.к. он требует редирект на https протокол. + Остальные исправил. + Авторизация через github не удастся, если в профиле публично не указал email. +- Вынес чувствительную информацию в отдельный проперти файл. +- Переделал тесты так, чтоб во время тестов использовался тестконтейнер. +- Написал тесты для всех публичных методов контроллера ProfileRestController. +- Сделал рефакторинг метода com.javarush.jira.bugtracking.attachment.FileUtil#upload чтоб он использовал современный подход для работы с файловой системмой. +- Добавитл новый функционал: добавления тегов к задаче (REST API + реализация на сервисе). Фронт делал. +- Добавил подсчет времени сколько задача находилась в работе и тестировании. Методы в классе com.javarush.jira.bugtracking.task.ActivityService +- Добавил Dockerfile и docker-compose.yaml +- Добавил локализацию для страницы логина (header & footer excluded) и сообщения с подтверждением на почту. + выбор языка зависит от Header Accept-language (Для проверки можно использовать curl 'http://localhost:8080/view/login' -H 'Accept-Language: en-US,en;q=0.9' или поменять язык в браузере) +- Также исправил отправку на почту подтверждение регистрации. + И поправил инжект HandlerExceptionResolver в классе com.javarush.jira.common.internal.config.RestAuthenticationEntryPoint, явно указав @Qualifier в конструкторе, + потому что в докере он не обращал внимание на @Qualifier, когда была аннотация @AllArgsConstructor. + +P.S. Насчет подтверждения почты: там сделано так, что в default profile используется тестовый email для подтверждения, +т.е. если вы введете свою почту, подтверждение всё равно будет приходить не на вашу почту, а на тестовую. +Чтобы подтверждение регистрации приходило на вашу почту, нужно указывать Active profile: prod (в docker-compose уже указан профиль prod) + + ## [REST API](http://localhost:8080/doc) ## Концепция: @@ -27,4 +55,4 @@ - https://habr.com/ru/articles/259055/ Список выполненных задач: -... \ No newline at end of file +... diff --git a/config/_application-prod.yaml b/config/_application-prod.yaml index 67fd8b7c2..f1b388e5d 100644 --- a/config/_application-prod.yaml +++ b/config/_application-prod.yaml @@ -3,7 +3,7 @@ app: host-url: http://localhost spring: datasource: - url: jdbc:postgresql://localhost:5432/jira - username: jira - password: JiraRush + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PASSWORD} diff --git a/config/nginx.conf b/config/nginx.conf index 82b9e234d..8253f9dcd 100644 --- a/config/nginx.conf +++ b/config/nginx.conf @@ -1,40 +1,68 @@ -# https://losst.ru/ustanovka-nginx-ubuntu-16-04 -# https://pai-bx.com/wiki/nginx/2332-useful-redirects-in-nginx/#1 -# sudo iptables -A INPUT ! -s 127.0.0.1 -p tcp -m tcp --dport 8080 -j DROP server { listen 80; + server_name localhost; - # https://www.digitalocean.com/community/tutorials/how-to-optimize-nginx-configuration gzip on; gzip_types text/css application/javascript application/json; gzip_min_length 2048; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - root /opt/jirarush/resources; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Forwarded-Port 80; if ($request_uri ~ ';') {return 404;} - # proxy_cookie_flags ~ secure samesite=none; + location /health { + access_log off; + return 200 "OK"; + add_header Content-Type text/plain; + } - # static location /static/ { + proxy_pass http://backend:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; expires 30d; access_log off; } + location /robots.txt { + proxy_pass http://backend:8080; access_log off; } location ~ (/$|/view/|/ui/|/oauth2/) { - expires 0m; - proxy_pass http://localhost:8080; - proxy_connect_timeout 30s; + proxy_pass http://backend:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 30s; } + location ~ (/api/|/doc|/swagger-ui/|/v3/api-docs/) { - proxy_pass http://localhost:8080; - proxy_connect_timeout 150s; + proxy_pass http://backend:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 150s; } + location / { - try_files /view/404.html = 404; + proxy_pass http://backend:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port 80; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; } } \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 000000000..57fc85f29 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,70 @@ + +services: + postgres: + image: postgres:17 + container_name: jira-postgres + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - jira-network + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}" ] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: . + container_name: jira-app + depends_on: + postgres: + condition: service_healthy + env_file: + - .env + environment: + DB_URL: jdbc:postgresql://postgres:5432/${DB_NAME} + APP_URL: http://localhost + SPRING_APPLICATION_BASEURL: http://localhost + SPRING_PROFILES_ACTIVE: prod + expose: + - "8080" + networks: + - jira-network + restart: unless-stopped + healthcheck: + test: [ "CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:8080 || exit 1" ] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + + nginx: + image: nginx:alpine + container_name: nginx-proxy + ports: + - "80:80" + volumes: + - ./config/nginx.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - backend + restart: unless-stopped + networks: + - jira-network + healthcheck: + test: [ "CMD-SHELL", "curl -f http://localhost/health 2>/dev/null || exit 1" ] + interval: 30s + timeout: 5s + retries: 3 + +volumes: + postgres_data: + +networks: + jira-network: + driver: bridge diff --git a/mvnw b/mvnw new file mode 100644 index 000000000..bd8896bf2 --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/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 +# +# http://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# 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 +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# 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 + +# 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 [ ! -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 + 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 + 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#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +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:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# 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 <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.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${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# 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" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +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 + +# 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" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# 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" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +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 + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +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 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 + 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 + 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 + fi +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" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/pom.xml b/pom.xml index f6c152c68..7b5f73f93 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,26 @@ spring-boot-starter-validation + + + org.testcontainers + testcontainers + 1.18.3 + test + + + org.testcontainers + junit-jupiter + 1.18.3 + test + + + org.testcontainers + postgresql + 1.18.3 + test + + com.fasterxml.jackson.datatype diff --git a/resources/mails/email-confirmation.html b/resources/mails/email-confirmation.html index 106e6129a..87f9fe3df 100644 --- a/resources/mails/email-confirmation.html +++ b/resources/mails/email-confirmation.html @@ -1,13 +1,12 @@ - + - JiraRush - подтверждение почты + JiraRush - подтверждение почты -

-

Чтобы завершить настройку учетной записи и начать пользоваться JiraRush, подтвердите, что вы правильно указали вашу - электронную почту.

-Подтвердить почту +

+

+ \ No newline at end of file diff --git a/resources/view/login.html b/resources/view/login.html index 8765ca8ff..3847518bc 100644 --- a/resources/view/login.html +++ b/resources/view/login.html @@ -1,5 +1,5 @@ - + @@ -7,26 +7,27 @@
-

Sign in

+

-

- Bad credentials -

+

- +
- +
- +
@@ -34,24 +35,16 @@

Sign in


-
- OR -
+

-

- Sign in with social networks -

+

- - - @@ -63,7 +56,7 @@

Sign in

- New here? Create account +

diff --git a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java index 6cffbe175..31f13003d 100644 --- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java +++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java @@ -3,19 +3,18 @@ import com.javarush.jira.common.error.IllegalRequestDataException; import com.javarush.jira.common.error.NotFoundException; import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.web.multipart.MultipartFile; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStream; import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +@Slf4j @UtilityClass public class FileUtil { private static final String ATTACHMENT_PATH = "./attachments/%s/"; @@ -25,14 +24,14 @@ public static void upload(MultipartFile multipartFile, String directoryPath, Str throw new IllegalRequestDataException("Select a file to upload."); } - File dir = new File(directoryPath); - if (dir.exists() || dir.mkdirs()) { - File file = new File(directoryPath + fileName); - try (OutputStream outStream = new FileOutputStream(file)) { - outStream.write(multipartFile.getBytes()); - } catch (IOException ex) { - throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename()); - } + try { + Path dir = Paths.get(directoryPath); + Files.createDirectories(dir); + Path resolve = dir.resolve(fileName); + Files.write(resolve, multipartFile.getBytes()); + } catch (IOException e) { + log.error("Failed to upload file {}, {}", multipartFile.getOriginalFilename(), e.getMessage()); + throw new IllegalRequestDataException("Failed to upload file " + multipartFile.getOriginalFilename()); } } diff --git a/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java b/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java index 7938541bb..4730583c3 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; import java.util.List; import static com.javarush.jira.bugtracking.task.TaskUtil.getLatestValue; @@ -25,6 +26,53 @@ private static void checkBelong(HasAuthorId activity) { } } + //Сколько задача находилась в работе (ready_for_review минус in_progress ). + public Duration howLongWasInProgress(Long taskId) { + List activities = handler.getRepository().findAllByTaskIdOrderByUpdatedDesc(taskId); + Activity inProgress = activities.stream() + .filter(activity -> "in_progress".equals(activity.getStatusCode())) + .findFirst() + .orElseThrow(() -> + new DataConflictException("Task was never in progress")); + + Activity readyForReview = activities.stream() + .filter(activity -> "ready_for_review".equals(activity.getStatusCode())) + .findFirst() + .orElseThrow(() -> + new DataConflictException("Task was never ready for review")); + + if (inProgress.getUpdated() == null || readyForReview.getUpdated() == null) { + throw new DataConflictException("Activity timestamps are not initialized"); + } + if (readyForReview.getUpdated().isBefore(inProgress.getUpdated())) { + throw new DataConflictException("Invalid activity order for task " + taskId); + } + return Duration.between(inProgress.getUpdated(), readyForReview.getUpdated()); + } + + //Сколько задача находилась на тестировании (done минус ready_for_review). + public Duration howLongWasInTesting(Long taskId) { + List activities = handler.getRepository().findAllByTaskIdOrderByUpdatedDesc(taskId); + Activity readyForReview = activities.stream() + .filter(activity -> "ready_for_review".equals(activity.getStatusCode())) + .findFirst() + .orElseThrow(() -> + new DataConflictException("Task was never ready for review")); + Activity finished = activities.stream() + .filter(activity -> "done".equals(activity.getStatusCode())) + .findFirst() + .orElseThrow(() -> + new DataConflictException("Task was never done")); + + if (readyForReview.getUpdated() == null || finished.getUpdated() == null) { + throw new DataConflictException("Activity timestamps are not initialized"); + } + if (finished.getUpdated().isBefore(readyForReview.getUpdated())) { + throw new DataConflictException("Invalid activity order for task " + taskId); + } + return Duration.between(readyForReview.getUpdated(), finished.getUpdated()); + } + @Transactional public Activity create(ActivityTo activityTo) { checkBelong(activityTo); diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java index e6f385548..3f4af793b 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java @@ -7,6 +7,7 @@ import com.javarush.jira.bugtracking.sprint.SprintRepository; import com.javarush.jira.bugtracking.task.mapper.TaskExtMapper; import com.javarush.jira.bugtracking.task.mapper.TaskFullMapper; +import com.javarush.jira.bugtracking.task.to.TagTo; import com.javarush.jira.bugtracking.task.to.TaskToExt; import com.javarush.jira.bugtracking.task.to.TaskToFull; import com.javarush.jira.common.error.DataConflictException; @@ -20,7 +21,8 @@ import org.springframework.util.Assert; import java.time.LocalDateTime; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; import static com.javarush.jira.bugtracking.ObjectType.TASK; import static com.javarush.jira.bugtracking.task.TaskUtil.fillExtraFields; @@ -140,4 +142,55 @@ private void checkAssignmentActionPossible(long id, String userType, boolean ass throw new DataConflictException(String.format(assign ? CANNOT_ASSIGN : CANNOT_UN_ASSIGN, userType, task.getStatusCode())); } } + + private TagTo extractTags(Task task) { + if (task.getTags() == null || task.getTags().isEmpty()) { + return new TagTo(Collections.emptySet()); + } + return new TagTo(task.getTags()); + } + + @Transactional(readOnly = true) + public TagTo getTags(Long taskId) { + Task task = handler.getRepository().getExisted(taskId); + return extractTags(task); + } + + @Transactional + public void replaceTags(Long taskId, TagTo tagTo) { + Task task = handler.getRepository().getExisted(taskId); + task.setTags(normalizeTags(tagTo.getTags())); + } + + @Transactional + public void addTags(Long taskId, TagTo tagTo) { + Task task = handler.getRepository().getExisted(taskId); + Set tags = task.getTags(); + if (tags == null) { + tags = new HashSet<>(); + task.setTags(tags); + } + tags.addAll(normalizeTags(tagTo.getTags())); + } + + @Transactional + public void clearTags(Long taskId) { + Task task = handler.getRepository().getExisted(taskId); + Set tags = task.getTags(); + if (tags != null) { + tags.clear(); + } else + task.setTags(new HashSet<>()); + } + + private Set normalizeTags(Set tags) { + if (tags == null) return Collections.emptySet(); + + return tags.stream() + .filter(Objects::nonNull) + .map(String::trim) + .filter(tag -> !tag.isEmpty()) + .map(String::toLowerCase) + .collect(Collectors.toSet()); + } } diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskTagController.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskTagController.java new file mode 100644 index 000000000..3c3dafd38 --- /dev/null +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskTagController.java @@ -0,0 +1,40 @@ +package com.javarush.jira.bugtracking.task; + +import com.javarush.jira.bugtracking.task.to.TagTo; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/tasks/{taskId}/tags") +@RequiredArgsConstructor +public class TaskTagController { + + private final TaskService taskService; + + @GetMapping + public TagTo getTags(@PathVariable Long taskId) { + return taskService.getTags(taskId); + } + + @PutMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public void replaceTags(@PathVariable Long taskId, + @Valid @RequestBody TagTo tagTo) { + taskService.replaceTags(taskId, tagTo); + } + + @PostMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public void addTags(@PathVariable Long taskId, + @Valid @RequestBody TagTo tagTo) { + taskService.addTags(taskId, tagTo); + } + + @DeleteMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteTags(@PathVariable Long taskId) { + taskService.clearTags(taskId); + } +} diff --git a/src/main/java/com/javarush/jira/bugtracking/task/to/TagTo.java b/src/main/java/com/javarush/jira/bugtracking/task/to/TagTo.java new file mode 100644 index 000000000..b15b68deb --- /dev/null +++ b/src/main/java/com/javarush/jira/bugtracking/task/to/TagTo.java @@ -0,0 +1,20 @@ +package com.javarush.jira.bugtracking.task.to; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TagTo { + + @NotNull + @Size(min = 1, max = 10, message = "Must contain 1-10 tags") + private Set<@NotBlank @Size(min = 2, max = 50) String> tags; +} diff --git a/src/main/java/com/javarush/jira/common/internal/config/MvcConfig.java b/src/main/java/com/javarush/jira/common/internal/config/MvcConfig.java index 8a434a807..93d73fa19 100644 --- a/src/main/java/com/javarush/jira/common/internal/config/MvcConfig.java +++ b/src/main/java/com/javarush/jira/common/internal/config/MvcConfig.java @@ -7,21 +7,26 @@ import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.ui.ModelMap; import org.springframework.web.client.RestTemplate; import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.request.WebRequestInterceptor; import org.springframework.web.filter.ForwardedHeaderFilter; import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.handler.WebRequestHandlerInterceptorAdapter; +import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; import org.springframework.web.servlet.mvc.UrlFilenameViewController; import java.time.Duration; +import java.util.Arrays; +import java.util.Locale; import java.util.Properties; //@EnableWebMvc : http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration @@ -95,4 +100,23 @@ public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { .setReadTimeout(Duration.ofSeconds(10)) .build(); } + + @Bean + public ResourceBundleMessageSource resourceBundleMessageSource() { + var resourceBundle = new ResourceBundleMessageSource(); + resourceBundle.setDefaultLocale(new Locale("ru")); + return resourceBundle; + } + + @Bean + public LocaleResolver localeResolver() { + var localeResolver = new AcceptHeaderLocaleResolver(); + localeResolver.setDefaultLocale(new Locale("ru")); // русский по умолчанию + localeResolver.setSupportedLocales(Arrays.asList( + new Locale("ru"), + Locale.ENGLISH + )); + + return localeResolver; + } } diff --git a/src/main/java/com/javarush/jira/common/internal/config/RestAuthenticationEntryPoint.java b/src/main/java/com/javarush/jira/common/internal/config/RestAuthenticationEntryPoint.java index 85a134319..709bb1fcc 100644 --- a/src/main/java/com/javarush/jira/common/internal/config/RestAuthenticationEntryPoint.java +++ b/src/main/java/com/javarush/jira/common/internal/config/RestAuthenticationEntryPoint.java @@ -3,7 +3,6 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.AllArgsConstructor; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; @@ -13,12 +12,15 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @Component -@AllArgsConstructor public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { - @Qualifier("handlerExceptionResolver") private final HandlerExceptionResolver resolver; private final RequestMappingHandlerMapping mapping; + public RestAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver, RequestMappingHandlerMapping mapping) { + this.resolver = resolver; + this.mapping = mapping; + } + @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws ServletException { try { diff --git a/src/main/java/com/javarush/jira/mail/MailService.java b/src/main/java/com/javarush/jira/mail/MailService.java index 1b1623138..3447d52cf 100644 --- a/src/main/java/com/javarush/jira/mail/MailService.java +++ b/src/main/java/com/javarush/jira/mail/MailService.java @@ -15,6 +15,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.scheduling.annotation.Async; @@ -29,7 +30,6 @@ @Service @RequiredArgsConstructor public class MailService { - private static final Locale LOCALE_RU = Locale.forLanguageTag("ru"); private static final String OK = "OK"; private final MailCaseRepository mailCaseRepository; @@ -79,7 +79,7 @@ public String send(String toEmail, String toName, String template, Map params) { - Context context = new Context(LOCALE_RU, params); + Context context = new Context(LocaleContextHolder.getLocale(), params); return templateEngine.process(template, context); } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7fcba1570..d34d7b978 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,7 +1,7 @@ # https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html app: - host-url: http://localhost:8080 - test-mail: jira4jr@gmail.com + host-url: ${APP_URL:http://localhost:8080} + test-mail: ${MAIL_TEST} templates-update-cache: 5s mail-sending-props: core-pool-size: 8 @@ -25,10 +25,11 @@ spring: default_batch_fetch_size: 20 # https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc jdbc.batch_size: 20 + datasource: - url: jdbc:postgresql://localhost:5432/jira - username: jira - password: JiraRush + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PASSWORD} liquibase: changeLog: "classpath:db/changelog.sql" @@ -51,43 +52,34 @@ spring: client: registration: github: - client-id: 3d0d8738e65881fff266 - client-secret: 0f97031ce6178b7dfb67a6af587f37e222a16120 + client-id: ${CLIENT_ID_GITHUB} + client-secret: ${CLIENT_SECRET_GITHUB} + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + client-name: GitHub + authorization-grant-type: authorization_code scope: - - email + - user:email + - read:user google: - client-id: 329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com - client-secret: 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 + client-id: ${CLIENT_ID_GOOGLE} + client-secret: ${CLIENT_SECRET_GOOGLE} scope: - email - profile - vk: - client-id: 51562377 - client-secret: jNM1YHQy1362Mqs49wUN - client-name: Vkontakte - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - client-authentication-method: client_secret_post - authorization-grant-type: authorization_code - scope: email yandex: - client-id: 2f3395214ba84075956b76a34b231985 - client-secret: ed236c501e444a609b0f419e5e88f1e1 + client-id: ${CLIENT_ID_YANDEX} + client-secret: ${CLIENT_SECRET_YANDEX} client-name: Yandex redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" authorization-grant-type: authorization_code gitlab: - client-id: b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494 - client-secret: e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004 + client-id: ${CLIENT_ID_GITLAB} + client-secret: ${CLIENT_SECRET_GITLAB} client-name: GitLab redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" authorization-grant-type: authorization_code scope: read_user provider: - vk: - authorization-uri: https://oauth.vk.com/authorize - token-uri: https://oauth.vk.com/access_token - user-info-uri: https://api.vk.com/method/users.get?v=8.1 - user-name-attribute: response yandex: authorization-uri: https://oauth.yandex.ru/authorize token-uri: https://oauth.yandex.ru/token @@ -111,8 +103,8 @@ spring: enable: true auth: true host: smtp.gmail.com - username: jira4jr@gmail.com - password: zdfzsrqvgimldzyj + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} port: 587 thymeleaf.check-template-location: false diff --git a/src/main/resources/db/changelog-master.yaml b/src/main/resources/db/changelog-master.yaml new file mode 100644 index 000000000..4ac2431bc --- /dev/null +++ b/src/main/resources/db/changelog-master.yaml @@ -0,0 +1,5 @@ +databaseChangeLog: + - include: + file: classpath:db/changelog-data.sql + - include: + file: classpath:data4dev/data.sql \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties new file mode 100644 index 000000000..1bbf7e6db --- /dev/null +++ b/src/main/resources/messages.properties @@ -0,0 +1,15 @@ +mail.confirmation_welcome=Hello, {0}. +mail.confirmation_paragraph=To complete account setup and start using JiraRush, confirm that you have entered your email address correctly. +mail.confirmation_link=Confirm email +mail.title=JiraRush - Mail Confirmation + +login.title=Sign in +login.auth_error=Bad credentials +login.reset_password=Forgot your password? +login.sign_in_button=Sign in +login.mail_placeholder=E-mail +login.password_placeholder=Password +login.social_network=Sign in with social networks +login.register_text=New here? +login.register_create=Create account +login.or=OR \ No newline at end of file diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties new file mode 100644 index 000000000..1bbf7e6db --- /dev/null +++ b/src/main/resources/messages_en.properties @@ -0,0 +1,15 @@ +mail.confirmation_welcome=Hello, {0}. +mail.confirmation_paragraph=To complete account setup and start using JiraRush, confirm that you have entered your email address correctly. +mail.confirmation_link=Confirm email +mail.title=JiraRush - Mail Confirmation + +login.title=Sign in +login.auth_error=Bad credentials +login.reset_password=Forgot your password? +login.sign_in_button=Sign in +login.mail_placeholder=E-mail +login.password_placeholder=Password +login.social_network=Sign in with social networks +login.register_text=New here? +login.register_create=Create account +login.or=OR \ No newline at end of file diff --git a/src/main/resources/messages_ru.properties b/src/main/resources/messages_ru.properties new file mode 100644 index 000000000..b4df9fb45 --- /dev/null +++ b/src/main/resources/messages_ru.properties @@ -0,0 +1,15 @@ +mail.confirmation_welcome=\u041F\u0440\u0438\u0432\u0435\u0442, {0}. +mail.confirmation_paragraph=\u0427\u0442\u043E\u0431\u044B \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0443 \u0443\u0447\u0435\u0442\u043D\u043E\u0439 \u0437\u0430\u043F\u0438\u0441\u0438 \u0438 \u043D\u0430\u0447\u0430\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C\u0441\u044F JiraRush, \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435, \u0447\u0442\u043E \u0432\u044B \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u043E \u0443\u043A\u0430\u0437\u0430\u043B\u0438 \u0432\u0430\u0448\u0443 \u044D\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u0443\u044E \u043F\u043E\u0447\u0442\u0443. +mail.confirmation_link=\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u043F\u043E\u0447\u0442\u0443 +mail.title=JiraRush - \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u043E\u0447\u0442\u044B + +login.title=\u0412\u043E\u0439\u0442\u0438 +login.auth_error=\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 +login.reset_password=\u0417\u0430\u0431\u044B\u043B \u043F\u0430\u0440\u043E\u043B\u044C? +login.sign_in_button=\u0412\u043E\u0439\u0442\u0438 +login.mail_placeholder=\u041F\u043E\u0447\u0442\u0430 +login.password_placeholder=\u041F\u0430\u0440\u043E\u043B\u044C +login.social_network=\u0417\u0430\u0439\u0442\u0438 \u0447\u0435\u0440\u0435\u0437 \u0434\u0440\u0443\u0433\u0438\u0435 \u043F\u043B\u043E\u0449\u0430\u0434\u043A\u0438 +login.register_text=\u0422\u044B \u043D\u043E\u0432\u0438\u0447\u043E\u043A? +login.register_create=\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C\u0441\u044F +login.or=\u0418\u041B\u0418 \ No newline at end of file diff --git a/src/test/java/com/javarush/jira/BaseIntegrationTest.java b/src/test/java/com/javarush/jira/BaseIntegrationTest.java new file mode 100644 index 000000000..d6fd08bb4 --- /dev/null +++ b/src/test/java/com/javarush/jira/BaseIntegrationTest.java @@ -0,0 +1,26 @@ +package com.javarush.jira; + +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 abstract class BaseIntegrationTest extends AbstractControllerTest { + + + static PostgreSQLContainer postgreSQLContainer = + new PostgreSQLContainer<>("postgres:15"); + + static { + postgreSQLContainer.start(); + } + + @DynamicPropertySource + static void properties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl); + registry.add("spring.datasource.username", postgreSQLContainer::getUsername); + registry.add("spring.datasource.password", postgreSQLContainer::getPassword); + } + +} diff --git a/src/test/java/com/javarush/jira/JiraRushApplicationTests.java b/src/test/java/com/javarush/jira/JiraRushApplicationTests.java index 287178598..934dd969d 100644 --- a/src/test/java/com/javarush/jira/JiraRushApplicationTests.java +++ b/src/test/java/com/javarush/jira/JiraRushApplicationTests.java @@ -2,7 +2,7 @@ import org.junit.jupiter.api.Test; -class JiraRushApplicationTests extends BaseTests { +class JiraRushApplicationTests extends BaseIntegrationTest { @Test void contextLoads() { } diff --git a/src/test/java/com/javarush/jira/bugtracking/sprint/SprintControllerTest.java b/src/test/java/com/javarush/jira/bugtracking/sprint/SprintControllerTest.java index 503e58781..c16c01e9c 100644 --- a/src/test/java/com/javarush/jira/bugtracking/sprint/SprintControllerTest.java +++ b/src/test/java/com/javarush/jira/bugtracking/sprint/SprintControllerTest.java @@ -1,6 +1,6 @@ package com.javarush.jira.bugtracking.sprint; -import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.BaseIntegrationTest; import com.javarush.jira.bugtracking.sprint.to.SprintTo; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -9,8 +9,8 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import static com.javarush.jira.bugtracking.sprint.SprintTestData.NOT_FOUND; import static com.javarush.jira.bugtracking.sprint.SprintTestData.*; +import static com.javarush.jira.bugtracking.sprint.SprintTestData.NOT_FOUND; import static com.javarush.jira.common.BaseHandler.REST_URL; import static com.javarush.jira.common.util.JsonUtil.writeValue; import static com.javarush.jira.login.internal.web.UserTestData.*; @@ -19,7 +19,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -class SprintControllerTest extends AbstractControllerTest { +class SprintControllerTest extends BaseIntegrationTest { private static final String SPRINTS_REST_URL = REST_URL + "/sprints/"; private static final String SPRINTS_BY_PROJECT_REST_URL = SPRINTS_REST_URL + "by-project"; private static final String SPRINTS_BY_PROJECT_AND_STATUS_REST_URL = SPRINTS_REST_URL + "by-project-and-status"; diff --git a/src/test/java/com/javarush/jira/bugtracking/task/ActivityServiceTest.java b/src/test/java/com/javarush/jira/bugtracking/task/ActivityServiceTest.java new file mode 100644 index 000000000..ceb44d83c --- /dev/null +++ b/src/test/java/com/javarush/jira/bugtracking/task/ActivityServiceTest.java @@ -0,0 +1,35 @@ +package com.javarush.jira.bugtracking.task; + +import com.javarush.jira.BaseIntegrationTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +@Sql(scripts = "classpath:activity-data.sql") +class ActivityServiceTest extends BaseIntegrationTest { + private final Long TASK_ID = 1L; + @Autowired + private ActivityService activityService; + + @Test + void howLongWasInProgress() { + Duration duration = activityService.howLongWasInProgress(TASK_ID); + + assertNotNull(duration); + assertTrue(duration.toMillis() > 0); + assertEquals(3, duration.toHours()); + } + + @Test + void howLongWasInTesting() { + Duration duration = activityService.howLongWasInTesting(TASK_ID); + + assertNotNull(duration); + assertTrue(duration.toMillis() > 0); + assertEquals(2, duration.toHours()); + } +} \ No newline at end of file diff --git a/src/test/java/com/javarush/jira/bugtracking/task/TaskControllerTest.java b/src/test/java/com/javarush/jira/bugtracking/task/TaskControllerTest.java index b5c25e992..ec16fc052 100644 --- a/src/test/java/com/javarush/jira/bugtracking/task/TaskControllerTest.java +++ b/src/test/java/com/javarush/jira/bugtracking/task/TaskControllerTest.java @@ -1,6 +1,6 @@ package com.javarush.jira.bugtracking.task; -import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.BaseIntegrationTest; import com.javarush.jira.bugtracking.UserBelongRepository; import com.javarush.jira.bugtracking.task.to.ActivityTo; import com.javarush.jira.bugtracking.task.to.TaskToExt; @@ -16,8 +16,8 @@ import static com.javarush.jira.bugtracking.task.TaskController.REST_URL; import static com.javarush.jira.bugtracking.task.TaskService.CANNOT_ASSIGN; import static com.javarush.jira.bugtracking.task.TaskService.CANNOT_UN_ASSIGN; -import static com.javarush.jira.bugtracking.task.TaskTestData.NOT_FOUND; import static com.javarush.jira.bugtracking.task.TaskTestData.*; +import static com.javarush.jira.bugtracking.task.TaskTestData.NOT_FOUND; import static com.javarush.jira.common.util.JsonUtil.writeValue; import static com.javarush.jira.login.internal.web.UserTestData.*; import static org.hamcrest.Matchers.is; @@ -25,7 +25,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -class TaskControllerTest extends AbstractControllerTest { +class TaskControllerTest extends BaseIntegrationTest { private static final String TASKS_REST_URL_SLASH = REST_URL + "/"; private static final String TASKS_BY_PROJECT_REST_URL = REST_URL + "/by-project"; private static final String TASKS_BY_SPRINT_REST_URL = REST_URL + "/by-sprint"; diff --git a/src/test/java/com/javarush/jira/login/internal/web/AdminUserControllerTest.java b/src/test/java/com/javarush/jira/login/internal/web/AdminUserControllerTest.java index 43a770b22..7f50adb23 100644 --- a/src/test/java/com/javarush/jira/login/internal/web/AdminUserControllerTest.java +++ b/src/test/java/com/javarush/jira/login/internal/web/AdminUserControllerTest.java @@ -1,6 +1,6 @@ package com.javarush.jira.login.internal.web; -import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.BaseIntegrationTest; import com.javarush.jira.login.Role; import com.javarush.jira.login.User; import com.javarush.jira.login.internal.UserRepository; @@ -23,7 +23,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -class AdminUserControllerTest extends AbstractControllerTest { +class AdminUserControllerTest extends BaseIntegrationTest { private static final String REST_URL_SLASH = REST_URL + '/'; diff --git a/src/test/java/com/javarush/jira/login/internal/web/RegisterControllerTest.java b/src/test/java/com/javarush/jira/login/internal/web/RegisterControllerTest.java index 6d1e1d55f..c9eacb94e 100644 --- a/src/test/java/com/javarush/jira/login/internal/web/RegisterControllerTest.java +++ b/src/test/java/com/javarush/jira/login/internal/web/RegisterControllerTest.java @@ -1,6 +1,6 @@ package com.javarush.jira.login.internal.web; -import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.BaseIntegrationTest; import com.javarush.jira.login.UserTo; import com.javarush.jira.login.internal.verification.ConfirmData; import org.junit.jupiter.api.Test; @@ -17,7 +17,7 @@ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -class RegisterControllerTest extends AbstractControllerTest { +class RegisterControllerTest extends BaseIntegrationTest { @Test void showRegisterPage() throws Exception { diff --git a/src/test/java/com/javarush/jira/login/internal/web/UserControllerTest.java b/src/test/java/com/javarush/jira/login/internal/web/UserControllerTest.java index d6790ff3b..4a02589e9 100644 --- a/src/test/java/com/javarush/jira/login/internal/web/UserControllerTest.java +++ b/src/test/java/com/javarush/jira/login/internal/web/UserControllerTest.java @@ -1,6 +1,6 @@ package com.javarush.jira.login.internal.web; -import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.BaseIntegrationTest; import com.javarush.jira.login.User; import com.javarush.jira.login.UserTo; import com.javarush.jira.login.internal.UserMapper; @@ -23,7 +23,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -class UserControllerTest extends AbstractControllerTest { +class UserControllerTest extends BaseIntegrationTest { @Autowired UserMapper mapper; diff --git a/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java b/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java index a6fd5e3bf..32dff5238 100644 --- a/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java +++ b/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java @@ -1,8 +1,70 @@ package com.javarush.jira.profile.internal.web; -import com.javarush.jira.AbstractControllerTest; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.javarush.jira.BaseIntegrationTest; +import com.javarush.jira.common.BaseHandler; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import static com.javarush.jira.login.internal.web.UserTestData.USER_MAIL; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -class ProfileRestControllerTest extends AbstractControllerTest { +class ProfileRestControllerTest extends BaseIntegrationTest { + private static final String REST_URL_PROFILE = BaseHandler.REST_URL + "/profile"; + + @Autowired + private ObjectMapper objectMapper; + + @Test + void getUnauthorized() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL_PROFILE)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void get() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL_PROFILE)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void update() throws Exception { + perform(MockMvcRequestBuilders.put(REST_URL_PROFILE) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(ProfileTestData.getNewTo()))) + .andExpect(status().isNoContent()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateInvalidRequestBody() throws Exception { + perform(MockMvcRequestBuilders.put(REST_URL_PROFILE) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(ProfileTestData.getInvalidTo()))) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void updateUnauthorized() throws Exception { + perform(MockMvcRequestBuilders.put(REST_URL_PROFILE) + .contentType(MediaType.APPLICATION_JSON) + .content("{}")) + .andExpect(status().isUnauthorized()); + + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateUnsupportedMediaType() throws Exception { + perform(MockMvcRequestBuilders.put(REST_URL_PROFILE)) + .andExpect(status().isUnsupportedMediaType()); + } } \ No newline at end of file diff --git a/src/test/java/com/javarush/jira/project/internal/web/ProjectControllerTest.java b/src/test/java/com/javarush/jira/project/internal/web/ProjectControllerTest.java index 4e49e9beb..c82b97429 100644 --- a/src/test/java/com/javarush/jira/project/internal/web/ProjectControllerTest.java +++ b/src/test/java/com/javarush/jira/project/internal/web/ProjectControllerTest.java @@ -1,6 +1,6 @@ package com.javarush.jira.project.internal.web; -import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.BaseIntegrationTest; import com.javarush.jira.bugtracking.project.Project; import com.javarush.jira.bugtracking.project.ProjectRepository; import com.javarush.jira.common.BaseHandler; @@ -19,7 +19,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -public class ProjectControllerTest extends AbstractControllerTest { +public class ProjectControllerTest extends BaseIntegrationTest { private static final String REST_URL_PROJECT = BaseHandler.REST_URL + "/projects"; private static final String REST_URL_MNGR_PROJECT = BaseHandler.REST_URL + "/mngr/projects"; diff --git a/src/test/java/com/javarush/jira/ref/internal/web/ReferenceControllerTest.java b/src/test/java/com/javarush/jira/ref/internal/web/ReferenceControllerTest.java index 7164e6435..19abbf946 100644 --- a/src/test/java/com/javarush/jira/ref/internal/web/ReferenceControllerTest.java +++ b/src/test/java/com/javarush/jira/ref/internal/web/ReferenceControllerTest.java @@ -1,6 +1,6 @@ package com.javarush.jira.ref.internal.web; -import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.BaseIntegrationTest; import com.javarush.jira.ref.RefTo; import com.javarush.jira.ref.RefType; import com.javarush.jira.ref.ReferenceService; @@ -24,7 +24,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -public class ReferenceControllerTest extends AbstractControllerTest { +public class ReferenceControllerTest extends BaseIntegrationTest { private static final String REST_URL = ReferenceController.REST_URL + "/"; @Autowired diff --git a/src/test/resources/activity-data.sql b/src/test/resources/activity-data.sql new file mode 100644 index 000000000..5e150e27e --- /dev/null +++ b/src/test/resources/activity-data.sql @@ -0,0 +1,12 @@ + +insert into activity (id, task_id, author_id, status_code, updated) +values (1001, 1, 1, 'in_progress', now() - interval '5 hour') +ON CONFLICT (id) DO NOTHING; + +insert into activity (id, task_id, author_id, status_code, updated) +values (1002, 1, 1, 'ready_for_review', now() - interval '2 hour') +ON CONFLICT (id) DO NOTHING; + +insert into activity (id, task_id, author_id, status_code, updated) +values (1003, 1, 1, 'done', now()) +ON CONFLICT (id) DO NOTHING; diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 51137fd06..16f227b74 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -3,6 +3,7 @@ spring: init: mode: always datasource: - url: jdbc:postgresql://localhost:5433/jira-test + driver-class-name: org.postgresql.Driver username: jira - password: JiraRush \ No newline at end of file + password: JiraRush + url: jdbc:postgresql://localhost:5433/jira-test \ No newline at end of file