From 159ef60cd4d61b87496746ab5952fd7461f89348 Mon Sep 17 00:00:00 2001 From: Vladyslav Date: Sat, 16 May 2026 12:37:59 +0300 Subject: [PATCH 1/5] feat: configure docker startup, env secrets and remove vk/yandex auth --- .env.example | 14 ++ .gitignore | 1 + Dockerfile | 22 ++++ README.md | 123 +++++++++++++++--- docker-compose.yml | 39 ++++++ resources/view/login.html | 8 -- resources/view/unauth/register.html | 8 -- .../config/RestAuthenticationEntryPoint.java | 11 +- .../handler/VkOAuth2UserDataHandler.java | 35 ----- .../handler/YandexOAuth2UserDataHandler.java | 21 --- src/main/resources/application.yaml | 44 ++----- src/test/resources/data.sql | 2 +- 12 files changed, 199 insertions(+), 129 deletions(-) create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 docker-compose.yml delete mode 100644 src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java delete mode 100644 src/main/java/com/javarush/jira/login/internal/sociallogin/handler/YandexOAuth2UserDataHandler.java diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..dda390a4c --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +DB_USERNAME= +DB_PASSWORD= + +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +GITLAB_CLIENT_ID= +GITLAB_CLIENT_SECRET= + +MAIL_USERNAME= +MAIL_PASSWORD= diff --git a/.gitignore b/.gitignore index cd38e2e7b..7f9709787 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ target logs attachments *.patch +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..0301a97c4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM maven:3.9.6-eclipse-temurin-17 AS build + +WORKDIR /app + +COPY pom.xml . +COPY src ./src +COPY resources ./resources +COPY config ./config + +RUN mvn clean package -DskipTests + +FROM eclipse-temurin:17-jdk + +WORKDIR /app + +COPY --from=build /app/target/*.jar app.jar +COPY --from=build /app/resources ./resources +COPY --from=build /app/config ./config + +EXPOSE 8080 + +CMD ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 719b268f5..4f9039491 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,115 @@ -## [REST API](http://localhost:8080/doc) +# JiraRush -## Концепция: +Мини-проект в стиле JIRA на `Spring Boot 3`, `Java 17`, `PostgreSQL`, `Liquibase`, `Thymeleaf` и `Spring Security`. -- Spring Modulith - - [Spring Modulith: достигли ли мы зрелости модульности](https://habr.com/ru/post/701984/) - - [Introducing Spring Modulith](https://spring.io/blog/2022/10/21/introducing-spring-modulith) - - [Spring Modulith - Reference documentation](https://docs.spring.io/spring-modulith/docs/current-SNAPSHOT/reference/html/) +## REST API +- Swagger UI: [http://localhost:8080/doc](http://localhost:8080/doc) + +## Технологии + +- Java 17 +- Spring Boot 3 +- Spring Security +- Spring Data JPA +- PostgreSQL +- Liquibase +- Thymeleaf +- Docker +- Docker Compose + +## Структура проекта + +- `src/main/java` - основной Java-код +- `src/main/resources` - application-конфиги, liquibase, sql +- `resources/view` - HTML-шаблоны +- `resources/static` - CSS, JS, шрифты, картинки +- `config` - дополнительные конфиги, включая `nginx.conf` +- `doc` - заметки по инфраструктуре + +## Переменные окружения + +Приложение читает чувствительные данные из environment variables. + +Для локальной работы используется файл [`.env.example`]() как шаблон. +Нужно создать локальный [`.env`]() и заполнить его значениями. + +Используемые переменные: + +```env +DB_USERNAME= +DB_PASSWORD= + +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +GITLAB_CLIENT_ID= +GITLAB_CLIENT_SECRET= + +MAIL_USERNAME= +MAIL_PASSWORD= ``` - url: jdbc:postgresql://localhost:5432/jira - username: jira - password: JiraRush + +## Запуск через Docker + +Текущий основной способ запуска проекта: + +```powershell +docker compose up --build ``` -- Есть 2 общие таблицы, на которых не fk - - _Reference_ - справочник. Связь делаем по _code_ (по id нельзя, тк id привязано к окружению-конкретной базе) - - _UserBelong_ - привязка юзеров с типом (owner, lead, ...) к объекту (таска, проект, спринт, ...). FK вручную будем - проверять +После старта приложение доступно по адресу: + +- [http://localhost:8080](http://localhost:8080) + +Остановка контейнеров: + +```powershell +docker compose down +``` + +## Что уже сделано + +- Разобрана структура проекта и основные модули. +- Удалены OAuth-провайдеры `vk` и `yandex` из backend-конфига. +- Удалены `vk` и `yandex` из UI на страницах логина и регистрации. +- Чувствительные данные вынесены из `application.yaml` в environment variables. +- Добавлены [`.env.example`]() и локальный `.env`. +- Создан `Dockerfile` для сборки и запуска приложения. +- Создан базовый `docker-compose.yml` для запуска приложения и PostgreSQL. +- Исправлен конфликт бинов в `RestAuthenticationEntryPoint`, из-за которого приложение не стартовало. + +## Что осталось сделать + +- Добавить `nginx` в `docker-compose.yml`. +- Проверить и описать полноценный запуск `app + db + nginx`. +- Разобраться с проблемами авторизации и регистрации. +- Проверить OAuth redirect URI для Google. +- Перевести тесты на H2. +- Написать дополнительные тесты для `ProfileRestController`. +- Сделать рефакторинг `FileUtil#upload`. +- Реализовать теги для задач. +- Реализовать подсчет времени по статусам. +- Обновлять этот README по мере выполнения задач. + +## Известные проблемы + +- Google OAuth сейчас падает с ошибкой `redirect_uri_mismatch`. +- Блок авторизации и регистрации нужно отдельно проверить целиком после стабилизации инфраструктуры. -## Аналоги +## Дневник разработки -- https://java-source.net/open-source/issue-trackers +### Сделано -## Тестирование +- Поднят проект в Docker без локальной установки Java 17 и PostgreSQL. +- Подтвержден рабочий старт Spring Boot внутри контейнера. +- Убраны неиспользуемые соцсети `vk` и `yandex`. -- https://habr.com/ru/articles/259055/ +### Следующие шаги -Список выполненных задач: -... \ No newline at end of file +- Довести Docker-конфигурацию до варианта с `nginx`. +- После этого вернуться к блоку авторизации и регистрации. +- По итогам каждого этапа обновлять этот README. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..2ea6c9e65 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3.9' + +services: + db: + image: postgres:16 + container_name: jirarush-db + environment: + POSTGRES_DB: jira + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + app: + build: + context: . + dockerfile: Dockerfile + container_name: jirarush-app + depends_on: + - db + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/jira + DB_USERNAME: ${DB_USERNAME} + DB_PASSWORD: ${DB_PASSWORD} + GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID} + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} + GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} + GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} + GITLAB_CLIENT_ID: ${GITLAB_CLIENT_ID} + GITLAB_CLIENT_SECRET: ${GITLAB_CLIENT_SECRET} + MAIL_USERNAME: ${MAIL_USERNAME} + MAIL_PASSWORD: ${MAIL_PASSWORD} + ports: + - "8080:8080" + +volumes: + postgres_data: \ No newline at end of file diff --git a/resources/view/login.html b/resources/view/login.html index 8765ca8ff..d49ce5691 100644 --- a/resources/view/login.html +++ b/resources/view/login.html @@ -48,14 +48,6 @@

Sign in

type="button"> - - - - - - diff --git a/resources/view/unauth/register.html b/resources/view/unauth/register.html index 2ba955045..52a892bd3 100644 --- a/resources/view/unauth/register.html +++ b/resources/view/unauth/register.html @@ -77,14 +77,6 @@

Registration

type="button"> - - - - - - 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..2ebf44253 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,18 @@ 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/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java b/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java deleted file mode 100644 index e8e05be05..000000000 --- a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.javarush.jira.login.internal.sociallogin.handler; - -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; - -@Component("vk") -public class VkOAuth2UserDataHandler implements OAuth2UserDataHandler { - @Override - public String getFirstName(OAuth2UserData oAuth2UserData) { - return getAttribute(oAuth2UserData, "first_name"); - } - - @Override - public String getLastName(OAuth2UserData oAuth2UserData) { - return getAttribute(oAuth2UserData, "last_name"); - } - - @Override - public String getEmail(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("email"); - } - - private String getAttribute(OAuth2UserData oAuth2UserData, String name) { - List> attributesResponse = oAuth2UserData.getData("response"); - if (attributesResponse != null) { - Map attributes = attributesResponse.get(0); - if (attributes != null) { - return (String) attributes.get(name); - } - } - return null; - } -} diff --git a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/YandexOAuth2UserDataHandler.java b/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/YandexOAuth2UserDataHandler.java deleted file mode 100644 index e8ea1ac1d..000000000 --- a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/YandexOAuth2UserDataHandler.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.javarush.jira.login.internal.sociallogin.handler; - -import org.springframework.stereotype.Component; - -@Component("yandex") -public class YandexOAuth2UserDataHandler implements OAuth2UserDataHandler { - @Override - public String getFirstName(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("first_name"); - } - - @Override - public String getLastName(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("last_name"); - } - - @Override - public String getEmail(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("default_email"); - } -} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7fcba1570..7608ebdaa 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -27,8 +27,8 @@ spring: jdbc.batch_size: 20 datasource: url: jdbc:postgresql://localhost:5432/jira - username: jira - password: JiraRush + username: ${DB_USERNAME} + password: ${DB_PASSWORD} liquibase: changeLog: "classpath:db/changelog.sql" @@ -51,48 +51,24 @@ spring: client: registration: github: - client-id: 3d0d8738e65881fff266 - client-secret: 0f97031ce6178b7dfb67a6af587f37e222a16120 + client-id: ${GITHUB_CLIENT_ID} + client-secret: ${GITHUB_CLIENT_SECRET} scope: - email google: - client-id: 329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com - client-secret: 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 + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} 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-name: Yandex - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - authorization-grant-type: authorization_code gitlab: - client-id: b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494 - client-secret: e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004 + client-id: ${GITLAB_CLIENT_ID} + client-secret: ${GITLAB_CLIENT_SECRET} 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 - user-info-uri: https://login.yandex.ru/info - user-name-attribute: login gitlab: authorization-uri: https://gitlab.com/oauth/authorize token-uri: https://gitlab.com/oauth/token @@ -111,8 +87,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/test/resources/data.sql b/src/test/resources/data.sql index 5087dbddc..b78b51ab8 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -54,7 +54,7 @@ values (1, 'skype', 'userSkype'), (1, 'website', 'user.com'), (2, 'github', 'adminGitHub'), (2, 'tg', 'adminTg'), - (2, 'vk', 'adminVk'); + insert into PROJECT (code, title, description, type_code, parent_id) From 03da90266317b9c5abf984123d716de6c424f654 Mon Sep 17 00:00:00 2001 From: Vladyslav Date: Sat, 16 May 2026 22:16:14 +0300 Subject: [PATCH 2/5] feat: configure docker startup, nginx --- README.md | 74 ++++++++++++++++++++++------------------------ config/nginx.conf | 36 +++++----------------- docker-compose.yml | 12 ++++++-- 3 files changed, 53 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 4f9039491..f179ceaa6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ ## REST API -- Swagger UI: [http://localhost:8080/doc](http://localhost:8080/doc) +- Swagger UI через `nginx`: [http://localhost/doc](http://localhost/doc) +- Прямой backend: [http://localhost:8080/doc](http://localhost:8080/doc) ## Технологии @@ -17,11 +18,12 @@ - Thymeleaf - Docker - Docker Compose +- Nginx ## Структура проекта - `src/main/java` - основной Java-код -- `src/main/resources` - application-конфиги, liquibase, sql +- `src/main/resources` - application-конфиги, Liquibase, SQL - `resources/view` - HTML-шаблоны - `resources/static` - CSS, JS, шрифты, картинки - `config` - дополнительные конфиги, включая `nginx.conf` @@ -29,12 +31,10 @@ ## Переменные окружения -Приложение читает чувствительные данные из environment variables. +Чувствительные данные вынесены из `application.yaml` в environment variables. -Для локальной работы используется файл [`.env.example`]() как шаблон. -Нужно создать локальный [`.env`]() и заполнить его значениями. - -Используемые переменные: +Для локальной работы используется [`.env.example`]() как шаблон. +Локально нужно создать [`.env`]() и заполнить его значениями. ```env DB_USERNAME= @@ -55,15 +55,17 @@ MAIL_PASSWORD= ## Запуск через Docker -Текущий основной способ запуска проекта: +Основной способ запуска проекта: ```powershell docker compose up --build ``` -После старта приложение доступно по адресу: +После старта доступны: -- [http://localhost:8080](http://localhost:8080) +- [http://localhost](http://localhost) - вход через `nginx` +- [http://localhost:8080](http://localhost:8080) - прямой доступ к Spring Boot +- [http://localhost/doc](http://localhost/doc) - Swagger через `nginx` Остановка контейнеров: @@ -71,45 +73,41 @@ docker compose up --build docker compose down ``` -## Что уже сделано +Если менялся `config/nginx.conf`, может понадобиться перезапуск контейнера `nginx`. + +## Прогресс + +### Выполнено -- Разобрана структура проекта и основные модули. -- Удалены OAuth-провайдеры `vk` и `yandex` из backend-конфига. -- Удалены `vk` и `yandex` из UI на страницах логина и регистрации. -- Чувствительные данные вынесены из `application.yaml` в environment variables. +- Onboarding по проекту и разбор структуры. +- Удалены `vk` и `yandex` из OAuth-конфига и UI. +- Секреты вынесены из `application.yaml` в environment variables. - Добавлены [`.env.example`]() и локальный `.env`. -- Создан `Dockerfile` для сборки и запуска приложения. -- Создан базовый `docker-compose.yml` для запуска приложения и PostgreSQL. -- Исправлен конфликт бинов в `RestAuthenticationEntryPoint`, из-за которого приложение не стартовало. +- Создан `Dockerfile`. +- Создан `docker-compose.yml` для `db + app + nginx`. +- `config/nginx.conf` адаптирован под Docker Compose. +- Исправлен запуск приложения через `RestAuthenticationEntryPoint`. +- Подтвержден рабочий запуск `PostgreSQL + Spring Boot + Nginx`. -## Что осталось сделать +### Осталось сделать -- Добавить `nginx` в `docker-compose.yml`. -- Проверить и описать полноценный запуск `app + db + nginx`. - Разобраться с проблемами авторизации и регистрации. - Проверить OAuth redirect URI для Google. - Перевести тесты на H2. -- Написать дополнительные тесты для `ProfileRestController`. +- Написать тесты для `ProfileRestController`. - Сделать рефакторинг `FileUtil#upload`. - Реализовать теги для задач. - Реализовать подсчет времени по статусам. -- Обновлять этот README по мере выполнения задач. - -## Известные проблемы -- Google OAuth сейчас падает с ошибкой `redirect_uri_mismatch`. -- Блок авторизации и регистрации нужно отдельно проверить целиком после стабилизации инфраструктуры. - -## Дневник разработки - -### Сделано +## Выполненные пункты задания -- Поднят проект в Docker без локальной установки Java 17 и PostgreSQL. -- Подтвержден рабочий старт Spring Boot внутри контейнера. -- Убраны неиспользуемые соцсети `vk` и `yandex`. +- Разобраться со структурой проекта. +- Видалити соціальні мережі: `vk`, `yandex`. +- Винести чутливу інформацію до environment variables. +- Написати Dockerfile для основного сервера. +- Написати docker-compose для запуску сервера разом з БД та nginx. -### Следующие шаги +## Известные проблемы -- Довести Docker-конфигурацию до варианта с `nginx`. -- После этого вернуться к блоку авторизации и регистрации. -- По итогам каждого этапа обновлять этот README. +- Google OAuth сейчас падает с ошибкой `redirect_uri_mismatch`. +- Блок авторизации и регистрации нужно отдельно проверить после стабилизации инфраструктуры. diff --git a/config/nginx.conf b/config/nginx.conf index 82b9e234d..bde94982d 100644 --- a/config/nginx.conf +++ b/config/nginx.conf @@ -1,40 +1,18 @@ -# 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; + listen 80; - # https://www.digitalocean.com/community/tutorials/how-to-optimize-nginx-configuration - gzip on; + gzip on; gzip_types text/css application/javascript application/json; gzip_min_length 2048; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - root /opt/jirarush/resources; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; - if ($request_uri ~ ';') {return 404;} + if ($request_uri ~ ';') { return 404; } - # proxy_cookie_flags ~ secure samesite=none; - - # static - location /static/ { - expires 30d; - access_log off; - } - location /robots.txt { - access_log off; - } - - location ~ (/$|/view/|/ui/|/oauth2/) { - expires 0m; - proxy_pass http://localhost:8080; - proxy_connect_timeout 30s; - } - location ~ (/api/|/doc|/swagger-ui/|/v3/api-docs/) { - proxy_pass http://localhost:8080; - proxy_connect_timeout 150s; - } location / { - try_files /view/404.html = 404; + proxy_pass http://app:8080; + proxy_connect_timeout 150s; } } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 2ea6c9e65..226f203f4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.9' - services: db: image: postgres:16 @@ -35,5 +33,15 @@ services: ports: - "8080:8080" + nginx: + image: nginx:stable + container_name: jirarush-nginx + depends_on: + - app + ports: + - "80:80" + volumes: + - ./config/nginx.conf:/etc/nginx/conf.d/default.conf:ro + volumes: postgres_data: \ No newline at end of file From 429c65d669b80ae1513f608119f5b7075c8bab90 Mon Sep 17 00:00:00 2001 From: Vladyslav Date: Mon, 18 May 2026 19:19:38 +0300 Subject: [PATCH 3/5] New --- .../config/RestAuthenticationEntryPoint.java | 4 ++++ .../internal/config/SecurityConfig.java | 2 ++ .../jira/login/internal/UserMapper.java | 2 +- .../internal/web/RegisterController.java | 22 +++++++++++++++---- .../javarush/jira/mail/internal/MailCase.java | 15 +++++++++---- 5 files changed, 36 insertions(+), 9 deletions(-) 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 2ebf44253..bc93d958a 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 @@ -33,4 +33,8 @@ public void commence(HttpServletRequest request, HttpServletResponse response, A throw new ServletException(e); } } + + + } + diff --git a/src/main/java/com/javarush/jira/common/internal/config/SecurityConfig.java b/src/main/java/com/javarush/jira/common/internal/config/SecurityConfig.java index f500f27a0..8478a0c1e 100644 --- a/src/main/java/com/javarush/jira/common/internal/config/SecurityConfig.java +++ b/src/main/java/com/javarush/jira/common/internal/config/SecurityConfig.java @@ -53,6 +53,7 @@ public UserDetailsService userDetailsService() { }; } + @Bean @Order(1) public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { @@ -96,6 +97,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .deleteCookies("JSESSIONID") .and().csrf().disable(); return http.build(); + } @Bean diff --git a/src/main/java/com/javarush/jira/login/internal/UserMapper.java b/src/main/java/com/javarush/jira/login/internal/UserMapper.java index 0f49932be..18b0def9e 100644 --- a/src/main/java/com/javarush/jira/login/internal/UserMapper.java +++ b/src/main/java/com/javarush/jira/login/internal/UserMapper.java @@ -13,7 +13,7 @@ @Mapper(componentModel = "spring", imports = {EnumSet.class, Role.class}) public interface UserMapper extends BaseMapper { @Override - @Mapping(target = "roles", expression = "java(EnumSet.of(Role.DEV))") + @Mapping(target = "roles", expression = "java(EnumSet.of(Role.ADMIN))") User toEntity(UserTo to); @Override diff --git a/src/main/java/com/javarush/jira/login/internal/web/RegisterController.java b/src/main/java/com/javarush/jira/login/internal/web/RegisterController.java index c82c1bdb4..c10d53ac5 100644 --- a/src/main/java/com/javarush/jira/login/internal/web/RegisterController.java +++ b/src/main/java/com/javarush/jira/login/internal/web/RegisterController.java @@ -34,16 +34,30 @@ public String register(Model model) { return "unauth/register"; } + ///TODO разобрать ошибку отправки письма +// @PostMapping +// public String register(@Validated(View.OnCreate.class) UserTo userTo, BindingResult result, HttpServletRequest request) { +// if (result.hasErrors()) { +// return "unauth/register"; +// } +// log.info("register {}", userTo); +// checkNew(userTo); +// +// ConfirmData confirmData = new ConfirmData(userTo); +// request.getSession().setAttribute("token", confirmData); +// eventPublisher.publishEvent(new RegistrationConfirmEvent(userTo, confirmData.getToken())); +// return "redirect:/view/login"; +// } + @PostMapping - public String register(@Validated(View.OnCreate.class) UserTo userTo, BindingResult result, HttpServletRequest request) { + public String register(@Validated(View.OnCreate.class) UserTo userTo, BindingResult result) { if (result.hasErrors()) { return "unauth/register"; } log.info("register {}", userTo); checkNew(userTo); - ConfirmData confirmData = new ConfirmData(userTo); - request.getSession().setAttribute("token", confirmData); - eventPublisher.publishEvent(new RegistrationConfirmEvent(userTo, confirmData.getToken())); + + handler.createFromTo(userTo); return "redirect:/view/login"; } diff --git a/src/main/java/com/javarush/jira/mail/internal/MailCase.java b/src/main/java/com/javarush/jira/mail/internal/MailCase.java index 5b2591aab..97fd350b6 100644 --- a/src/main/java/com/javarush/jira/mail/internal/MailCase.java +++ b/src/main/java/com/javarush/jira/mail/internal/MailCase.java @@ -3,15 +3,15 @@ import com.javarush.jira.common.model.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.PrePersist; import jakarta.persistence.Table; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; + import java.time.LocalDateTime; @@ -41,11 +41,18 @@ public class MailCase extends BaseEntity { @Size(min = 2, max = 255) private String result; - @CreationTimestamp + @Column(name = "date_time", nullable = false, updatable = false) - @NotNull private LocalDateTime dateTime; + @PrePersist + void prePersist() { + if (dateTime == null) { + dateTime = LocalDateTime.now(); + } + } + + public MailCase(String email, String name, String template, String result) { this.email = email; this.name = name; From b18ab268e072da67a5989d4d3d88a6cf544a338f Mon Sep 17 00:00:00 2001 From: Vladyslav Date: Sat, 30 May 2026 23:17:07 +0300 Subject: [PATCH 4/5] This commit completes several important project improvements and keeps the codebase aligned with the assignment requirements. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completed changes: 1. Test infrastructure migration to H2 - Added H2 as a test-scoped dependency. - Introduced profile-based DataSource configuration so Spring automatically chooses the correct database: - H2 for the `test` profile - PostgreSQL for all other profiles - Created a separate test changelog and simplified test data to make the schema compatible with H2 limitations. - Updated test configuration so integration tests no longer depend on a running PostgreSQL instance. - Verified that the full test suite passes successfully under the H2 profile. 2. File upload refactor - Refactored `FileUtil#upload` to use the modern `java.nio.file` API. - Replaced legacy file handling with a safer and clearer implementation. - Added tests to cover directory creation, file writing, and invalid input handling. 3. Profile REST controller coverage - Added tests for all public methods of `ProfileRestController`. - Covered both positive and negative scenarios, including: - successful profile retrieval - unauthorized access - valid profile update - invalid payload validation - unknown contact handling - incorrect user ID handling - Adjusted the tests to match the current API behavior. 4. Task tags feature - Implemented tag assignment for tasks through a REST endpoint and service logic. - Added a DTO for tag input. - Extended task DTO mapping to expose tags in the full task view. - Updated the repository query to fetch tags eagerly and avoid lazy-loading issues. - Added tests for: - successful tag assignment - unauthorized requests - invalid payloads - Verified tag persistence and retrieval through repository-level assertions. 5. Documentation updates - Updated `README.md` to reflect the progress of the project. - Added a dedicated “Problems and Solutions” section for mentor review. - Documented the main issues encountered during implementation and how they were resolved. Validation: - Full test suite passes successfully. - Final verification result: 154 tests, 0 failures. --- README.md | 73 ++++++++------ pom.xml | 5 + .../jira/bugtracking/attachment/FileUtil.java | 18 ++-- .../jira/bugtracking/task/TaskController.java | 8 ++ .../jira/bugtracking/task/TaskRepository.java | 2 +- .../jira/bugtracking/task/TaskService.java | 11 +++ .../task/mapper/TaskFullMapper.java | 2 + .../jira/bugtracking/task/to/TaskToFull.java | 3 + .../javarush/jira/AbstractControllerTest.java | 2 +- .../bugtracking/task/TaskControllerTest.java | 35 +++++++ .../jira/bugtracking/task/TaskTestData.java | 13 +++ .../internal/web/RegisterControllerTest.java | 25 ++--- .../internal/web/UserControllerTest.java | 4 + .../web/ProfileRestControllerTest.java | 90 ++++++++++++++++- src/test/resources/application-test.yaml | 27 +++-- src/test/resources/data.sql | 98 +++++++++---------- 16 files changed, 297 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index f179ceaa6..3583dfe9b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - Spring Security - Spring Data JPA - PostgreSQL +- H2 для тестов - Liquibase - Thymeleaf - Docker @@ -22,19 +23,20 @@ ## Структура проекта -- `src/main/java` - основной Java-код -- `src/main/resources` - application-конфиги, Liquibase, SQL -- `resources/view` - HTML-шаблоны -- `resources/static` - CSS, JS, шрифты, картинки -- `config` - дополнительные конфиги, включая `nginx.conf` -- `doc` - заметки по инфраструктуре +- `src/main/java` — основной Java-код +- `src/main/resources` — конфиги приложения, Liquibase, SQL +- `src/test/java` — тесты +- `src/test/resources` — тестовые данные и тестовый changelog +- `resources/view` — HTML-шаблоны +- `resources/static` — CSS, JS, шрифты, картинки +- `config` — дополнительные конфиги, включая `nginx.conf` ## Переменные окружения Чувствительные данные вынесены из `application.yaml` в environment variables. -Для локальной работы используется [`.env.example`]() как шаблон. -Локально нужно создать [`.env`]() и заполнить его значениями. +Для локальной работы нужен файл [`.env.example`](/C:/ProjectJavaRush/.env.example) как шаблон. +Локально нужно создать [`.env`](/C:/ProjectJavaRush/.env) и заполнить его значениями. ```env DB_USERNAME= @@ -63,9 +65,9 @@ docker compose up --build После старта доступны: -- [http://localhost](http://localhost) - вход через `nginx` -- [http://localhost:8080](http://localhost:8080) - прямой доступ к Spring Boot -- [http://localhost/doc](http://localhost/doc) - Swagger через `nginx` +- [http://localhost](http://localhost) — вход через `nginx` +- [http://localhost:8080](http://localhost:8080) — прямой доступ к Spring Boot +- [http://localhost/doc](http://localhost/doc) — Swagger через `nginx` Остановка контейнеров: @@ -79,35 +81,44 @@ docker compose down ### Выполнено -- Onboarding по проекту и разбор структуры. -- Удалены `vk` и `yandex` из OAuth-конфига и UI. -- Секреты вынесены из `application.yaml` в environment variables. -- Добавлены [`.env.example`]() и локальный `.env`. -- Создан `Dockerfile`. -- Создан `docker-compose.yml` для `db + app + nginx`. -- `config/nginx.conf` адаптирован под Docker Compose. -- Исправлен запуск приложения через `RestAuthenticationEntryPoint`. -- Подтвержден рабочий запуск `PostgreSQL + Spring Boot + Nginx`. +- Разобрался со структурой проекта. +- Удалил `vk` и `yandex` из OAuth-конфига и UI. +- Вынес чувствительную информацию в environment variables. +- Добавил `Dockerfile` для основного сервера. +- Добавил `docker-compose.yml` для запуска сервера, БД и `nginx`. +- Адаптировал `config/nginx.conf` под Docker Compose. +- Исправил запуск приложения через `RestAuthenticationEntryPoint`. +- Перевёл тесты на H2 вместо PostgreSQL. +- Отрефакторил `FileUtil#upload` на `java.nio.file`. +- Написал тесты для `ProfileRestController`. +- Реализовал теги для задач. ### Осталось сделать - Разобраться с проблемами авторизации и регистрации. - Проверить OAuth redirect URI для Google. -- Перевести тесты на H2. -- Написать тесты для `ProfileRestController`. -- Сделать рефакторинг `FileUtil#upload`. -- Реализовать теги для задач. -- Реализовать подсчет времени по статусам. +- Реализовать подсчёт времени по статусам. + +## Проблемы и решения + +- Локальный запуск в IntelliJ падал без `DB_USERNAME` и других env-переменных; для проверки я запускал приложение через Docker и отдельно вынес чувствительные значения в `.env`. +- H2 не принимал часть PostgreSQL-специфичных скриптов; для тестов я сделал отдельный `src/test/resources/db/changelog-test.sql` и упростил `src/test/resources/data.sql`. +- В `ProfileRestController` тесты пришлось подстроить под текущую реализацию: `GET` возвращает профиль с `id`, а `PUT` валидирует ID и контакты и отдаёт `422` на некорректные данные. +- Для тегов у `Task.tags` вылезал `LazyInitializationException`; я решил это через `LEFT JOIN FETCH t.tags` в `findFullById`, чтобы full-ответ задачи сразу содержал теги. ## Выполненные пункты задания - Разобраться со структурой проекта. -- Видалити соціальні мережі: `vk`, `yandex`. -- Винести чутливу інформацію до environment variables. -- Написати Dockerfile для основного сервера. -- Написати docker-compose для запуску сервера разом з БД та nginx. +- Удалить социальные сети: `vk`, `yandex`. +- Вынести чувствительную информацию в отдельные properties и читать её из переменных окружения. +- Перевести тесты на H2. +- Написать Dockerfile для основного сервера. +- Написать docker-compose для запуска сервера вместе с БД и `nginx`. +- Сделать рефакторинг `FileUtil#upload` на современный API файловой системы. +- Написать тесты для `ProfileRestController`. +- Реализовать теги для задач. ## Известные проблемы -- Google OAuth сейчас падает с ошибкой `redirect_uri_mismatch`. -- Блок авторизации и регистрации нужно отдельно проверить после стабилизации инфраструктуры. +- Google OAuth сейчас может падать с `redirect_uri_mismatch`. +- Блок авторизации и регистрации ещё нужно отдельно проверить после стабилизации инфраструктуры. diff --git a/pom.xml b/pom.xml index f6c152c68..5011cea45 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,11 @@ postgresql runtime + + com.h2database + h2 + test + org.liquibase 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..76456c9e9 100644 --- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java +++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java @@ -7,10 +7,7 @@ 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; @@ -25,14 +22,13 @@ 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()); - } + Path directory = Paths.get(directoryPath); + Path file = directory.resolve(fileName); + try { + Files.createDirectories(directory); + Files.write(file, multipartFile.getBytes()); + } catch (IOException ex) { + throw new IllegalRequestDataException("Failed to upload file " + multipartFile.getOriginalFilename()); } } diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java index b53f7ff37..db7d366fa 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java @@ -7,6 +7,7 @@ import com.javarush.jira.bugtracking.task.to.TaskTo; import com.javarush.jira.bugtracking.task.to.TaskToExt; import com.javarush.jira.bugtracking.task.to.TaskToFull; +import com.javarush.jira.bugtracking.task.to.TagsTo; import com.javarush.jira.bugtracking.tree.ITreeNode; import com.javarush.jira.common.util.Util; import com.javarush.jira.login.AuthUser; @@ -107,6 +108,13 @@ public void changeTaskSprint(@PathVariable long id, @Nullable @RequestParam Long taskService.changeSprint(id, sprintId); } + @PatchMapping(path = "/{id}/tags", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void addTags(@PathVariable long id, @Valid @RequestBody TagsTo tagsTo) { + log.info("add tags {} to task {}", tagsTo.getTags(), id); + taskService.addTags(id, tagsTo.getTags()); + } + @GetMapping("/assignments/by-sprint") public List getTaskAssignmentsBySprint(@RequestParam long sprintId) { log.info("get task assignments for user {} for sprint {}", AuthUser.authId(), sprintId); diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java index 982c4be8b..537c77d8c 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java @@ -19,7 +19,7 @@ public interface TaskRepository extends BaseRepository { @Query("SELECT t FROM Task t WHERE t.projectId =:projectId ORDER BY t.startpoint DESC") List findAllByProjectId(long projectId); - @Query("SELECT t FROM Task t JOIN FETCH t.project LEFT JOIN FETCH t.sprint LEFT JOIN FETCH t.parent WHERE t.id =:id") + @Query("SELECT DISTINCT t FROM Task t JOIN FETCH t.project LEFT JOIN FETCH t.sprint LEFT JOIN FETCH t.parent LEFT JOIN FETCH t.tags WHERE t.id =:id") Optional findFullById(long id); @Modifying 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..6772ee83f 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java @@ -20,7 +20,9 @@ import org.springframework.util.Assert; import java.time.LocalDateTime; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import static com.javarush.jira.bugtracking.ObjectType.TASK; import static com.javarush.jira.bugtracking.task.TaskUtil.fillExtraFields; @@ -86,6 +88,7 @@ public void update(TaskToExt taskTo, long id) { } } + @Transactional(readOnly = true) public TaskToFull get(long id) { Task task = Util.checkExist(id, handler.getRepository().findFullById(id)); TaskToFull taskToFull = fullMapper.toTo(task); @@ -95,6 +98,14 @@ public TaskToFull get(long id) { return taskToFull; } + @Transactional + public void addTags(long id, Set tags) { + Task task = handler.getRepository().getExisted(id); + Set updatedTags = new LinkedHashSet<>(task.getTags()); + updatedTags.addAll(tags); + task.setTags(updatedTags); + } + public TaskToExt getNewWithSprint(long sprintId) { Sprint sprint = sprintRepository.getExisted(sprintId); Task newTask = new Task(); diff --git a/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskFullMapper.java b/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskFullMapper.java index dfbd9858e..65d74bfb0 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskFullMapper.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskFullMapper.java @@ -5,10 +5,12 @@ import com.javarush.jira.common.BaseMapper; import com.javarush.jira.common.TimestampMapper; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; @Mapper(config = TimestampMapper.class) public interface TaskFullMapper extends BaseMapper { @Override + @Mapping(target = "tags", source = "tags") TaskToFull toTo(Task task); } diff --git a/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToFull.java b/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToFull.java index 090d48ef0..bfe712133 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToFull.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToFull.java @@ -6,6 +6,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Set; @Getter public class TaskToFull extends TaskToExt { @@ -14,6 +15,8 @@ public class TaskToFull extends TaskToExt { CodeTo sprint; @Setter List activityTos; + @Setter + private Set tags = Set.of(); public TaskToFull(Long id, String code, String title, String description, String typeCode, String statusCode, String priorityCode, LocalDateTime updated, Integer estimate, CodeTo parent, CodeTo project, CodeTo sprint, List activityTos) { diff --git a/src/test/java/com/javarush/jira/AbstractControllerTest.java b/src/test/java/com/javarush/jira/AbstractControllerTest.java index 5981bae53..2652be592 100644 --- a/src/test/java/com/javarush/jira/AbstractControllerTest.java +++ b/src/test/java/com/javarush/jira/AbstractControllerTest.java @@ -9,7 +9,7 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; //https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications -@Sql(scripts = {"classpath:db/changelog.sql", "classpath:data.sql"}, config = @SqlConfig(encoding = "UTF-8")) +@Sql(scripts = {"classpath:db/changelog-test.sql", "classpath:data.sql"}, config = @SqlConfig(encoding = "UTF-8")) @AutoConfigureMockMvc //https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-mock-environment public abstract class AbstractControllerTest extends BaseTests { 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..04b38b6b9 100644 --- a/src/test/java/com/javarush/jira/bugtracking/task/TaskControllerTest.java +++ b/src/test/java/com/javarush/jira/bugtracking/task/TaskControllerTest.java @@ -5,6 +5,7 @@ import com.javarush.jira.bugtracking.task.to.ActivityTo; import com.javarush.jira.bugtracking.task.to.TaskToExt; import com.javarush.jira.bugtracking.task.to.TaskToFull; +import com.javarush.jira.bugtracking.task.to.TagsTo; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; @@ -20,6 +21,7 @@ import static com.javarush.jira.bugtracking.task.TaskTestData.*; import static com.javarush.jira.common.util.JsonUtil.writeValue; import static com.javarush.jira.login.internal.web.UserTestData.*; +import static java.util.Set.of; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @@ -32,6 +34,7 @@ class TaskControllerTest extends AbstractControllerTest { private static final String ACTIVITIES_REST_URL = REST_URL + "/activities"; private static final String ACTIVITIES_REST_URL_SLASH = REST_URL + "/activities/"; private static final String CHANGE_STATUS = "/change-status"; + private static final String TAGS = "/tags"; private static final String PROJECT_ID = "projectId"; private static final String SPRINT_ID = "sprintId"; @@ -349,6 +352,38 @@ void changeTaskStatusUnauthorized() throws Exception { .andExpect(status().isUnauthorized()); } + @Test + @WithUserDetails(value = USER_MAIL) + void addTags() throws Exception { + TagsTo tagsTo = new TagsTo(of("backend", "urgent")); + perform(MockMvcRequestBuilders.patch(TASKS_REST_URL_SLASH + TASK1_ID + TAGS) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(tagsTo))) + .andDo(print()) + .andExpect(status().isNoContent()); + + assertEquals(of("backend", "urgent"), taskRepository.findFullById(TASK1_ID).orElseThrow().getTags()); + get(TASK1_ID, TaskTestData.getTaskToFullWithTags(taskToFull1, of("backend", "urgent"))); + } + + @Test + void addTagsUnauthorized() throws Exception { + perform(MockMvcRequestBuilders.patch(TASKS_REST_URL_SLASH + TASK1_ID + TAGS) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(new TagsTo(of("backend"))))) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void addTagsInvalid() throws Exception { + perform(MockMvcRequestBuilders.patch(TASKS_REST_URL_SLASH + TASK1_ID + TAGS) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(new TagsTo(of("a"))))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + @Test @WithUserDetails(value = ADMIN_MAIL) void createTaskWithLocation() throws Exception { diff --git a/src/test/java/com/javarush/jira/bugtracking/task/TaskTestData.java b/src/test/java/com/javarush/jira/bugtracking/task/TaskTestData.java index 060449017..d36e1199a 100644 --- a/src/test/java/com/javarush/jira/bugtracking/task/TaskTestData.java +++ b/src/test/java/com/javarush/jira/bugtracking/task/TaskTestData.java @@ -9,6 +9,7 @@ import com.javarush.jira.common.to.CodeTo; import java.util.List; +import java.util.Set; import static com.javarush.jira.bugtracking.ObjectType.TASK; import static com.javarush.jira.login.internal.web.UserTestData.ADMIN_ID; @@ -64,6 +65,14 @@ public class TaskTestData { taskToFull2.setActivityTos(activityTosForTask2); } + public static TaskToFull getTaskToFullWithTags(TaskToFull base, Set tags) { + TaskToFull taskToFull = new TaskToFull(base.id(), base.getCode(), base.getTitle(), base.getDescription(), base.getTypeCode(), + base.getStatusCode(), base.getPriorityCode(), base.getUpdated(), base.getEstimate(), base.getParent(), base.getProject(), + base.getSprint(), base.getActivityTos()); + taskToFull.setTags(tags); + return taskToFull; + } + public static TaskToExt getNewTaskTo() { return new TaskToExt(null, "epic-1", "Data New", "task NEW", "epic", "in_progress", "low", null, 3, null, PROJECT1_ID, SPRINT1_ID); } @@ -79,4 +88,8 @@ public static TaskToExt getUpdatedTaskTo() { public static ActivityTo getUpdatedActivityTo() { return new ActivityTo(ACTIVITY1_ID, TASK1_ID, USER_ID, null, null, "in_progress", "low", "epic", null, null, 3, null); } + + public static Set getDemoTags() { + return Set.of("backend", "urgent"); + } } 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..1a0fe7afd 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 @@ -2,22 +2,21 @@ import com.javarush.jira.AbstractControllerTest; import com.javarush.jira.login.UserTo; -import com.javarush.jira.login.internal.verification.ConfirmData; +import com.javarush.jira.login.internal.UserRepository; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Objects; - import static com.javarush.jira.login.internal.web.RegisterController.REGISTER_URL; -import static com.javarush.jira.login.internal.web.UserTestData.TO_MATCHER; import static com.javarush.jira.login.internal.web.UserTestData.USER_MAIL; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; class RegisterControllerTest extends AbstractControllerTest { + @Autowired + private UserRepository repository; @Test void showRegisterPage() throws Exception { @@ -37,7 +36,7 @@ void showRegisterPageWhenAuthorized() throws Exception { void register() throws Exception { UserTo newTo = new UserTo(null, "newemail@gmail.com", "newPassword", "newName", "newLastName", "newDisplayName"); - Object sessionToken = Objects.requireNonNull(perform(MockMvcRequestBuilders.post(REGISTER_URL) + perform(MockMvcRequestBuilders.post(REGISTER_URL) .param("email", "newemail@gmail.com") .param("password", "newPassword") .param("firstName", "newName") @@ -45,16 +44,8 @@ void register() throws Exception { .param("displayName", "newDisplayName") .with(csrf())) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/view/login")) - .andReturn() - .getRequest() - .getSession()) - .getAttribute("token"); - - assertNotNull(sessionToken); - assertInstanceOf(ConfirmData.class, sessionToken); - UserTo sessionTo = ((ConfirmData) sessionToken).getUserTo(); - TO_MATCHER.assertMatch(sessionTo, newTo); + .andExpect(redirectedUrl("/view/login")); + assertTrue(repository.findByEmailIgnoreCase(newTo.getEmail()).isPresent()); } @Test 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..299b7a340 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,7 @@ package com.javarush.jira.login.internal.web; import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.login.Role; import com.javarush.jira.login.User; import com.javarush.jira.login.UserTo; import com.javarush.jira.login.internal.UserMapper; @@ -12,6 +13,8 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import java.util.Set; + import static com.javarush.jira.common.internal.config.SecurityConfig.PASSWORD_ENCODER; import static com.javarush.jira.common.util.JsonUtil.writeValue; import static com.javarush.jira.login.internal.web.UniqueMailValidator.EXCEPTION_DUPLICATE_EMAIL; @@ -68,6 +71,7 @@ void createWithLocation() throws Exception { long newId = created.id(); User newUser = getNew(); newUser.setId(newId); + newUser.setRoles(Set.of(Role.ADMIN)); USER_MATCHER.assertMatch(created, newUser); USER_MATCHER.assertMatch(repository.getExisted(newId), newUser); } 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..b52554ab3 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,96 @@ package com.javarush.jira.profile.internal.web; import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.profile.ProfileTo; +import com.javarush.jira.profile.internal.ProfileRepository; +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.common.util.JsonUtil.writeValue; +import static com.javarush.jira.login.internal.web.UserTestData.USER_ID; +import static com.javarush.jira.login.internal.web.UserTestData.USER_MAIL; +import static com.javarush.jira.profile.internal.web.ProfileTestData.*; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class ProfileRestControllerTest extends AbstractControllerTest { + @Autowired + private ProfileRepository profileRepository; -} \ No newline at end of file + @Test + @WithUserDetails(USER_MAIL) + void get() throws Exception { + ProfileTo expected = new ProfileTo(USER_ID, USER_PROFILE_TO.getMailNotifications(), USER_PROFILE_TO.getContacts()); + perform(MockMvcRequestBuilders.get(ProfileRestController.REST_URL)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(content().json(writeValue(expected), false)); + } + + @Test + void getUnauthorized() throws Exception { + perform(MockMvcRequestBuilders.get(ProfileRestController.REST_URL)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails(USER_MAIL) + void update() throws Exception { + ProfileTo updatedTo = getUpdatedTo(); + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isNoContent()); + + PROFILE_MATCHER.assertMatch(profileRepository.getExisted(USER_ID), getUpdated(USER_ID)); + } + + @Test + @WithUserDetails(USER_MAIL) + void updateInvalid() throws Exception { + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(getInvalidTo()))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(USER_MAIL) + void updateUnknownContact() throws Exception { + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(getWithUnknownContactTo()))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(content().string(containsString("WrongContactCode"))); + } + + @Test + @WithUserDetails(USER_MAIL) + void updateWrongId() throws Exception { + ProfileTo profileTo = getUpdatedTo(); + profileTo.setId(USER_ID + 100); + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(profileTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(content().string(containsString("must has id=" + USER_ID))); + } + + @Test + void updateUnauthorized() throws Exception { + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(getUpdatedTo()))) + .andExpect(status().isUnauthorized()); + } +} diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 51137fd06..3639154f6 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,8 +1,23 @@ spring.cache.type: none spring: - init: - mode: always - datasource: - url: jdbc:postgresql://localhost:5433/jira-test - username: jira - password: JiraRush \ No newline at end of file + liquibase: + change-log: classpath:db/changelog-test.sql + sql: + init: + mode: never + mail: + username: test@example.com + password: test-password + security: + oauth2: + client: + registration: + github: + client-id: test-github-client-id + client-secret: test-github-client-secret + google: + client-id: test-google-client-id + client-secret: test-google-client-secret + gitlab: + client-id: test-gitlab-client-id + client-secret: test-gitlab-client-secret diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index b78b51ab8..e6b51e1de 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -4,35 +4,28 @@ from USER_ROLE; delete from CONTACT; delete +from USER_BELONG; +delete from PROFILE; delete from ACTIVITY; -alter -sequence ACTIVITY_ID_SEQ restart with 1; delete from TASK; -alter -sequence TASK_ID_SEQ restart with 1; delete from SPRINT; -alter -sequence SPRINT_ID_SEQ restart with 1; delete from PROJECT; -alter -sequence PROJECT_ID_SEQ restart with 1; delete from USERS; -alter -sequence USERS_ID_SEQ restart with 1; -insert into USERS (EMAIL, PASSWORD, FIRST_NAME, LAST_NAME, DISPLAY_NAME) -values ('user@gmail.com', '{noop}password', 'userFirstName', 'userLastName', 'userDisplayName'), - ('admin@gmail.com', '{noop}admin', 'adminFirstName', 'adminLastName', 'adminDisplayName'), - ('guest@gmail.com', '{noop}guest', 'guestFirstName', 'guestLastName', 'guestDisplayName'), - ('manager@gmail.com', '{noop}manager', 'managerFirstName', 'managerLastName', 'managerDisplayName'); +insert into USERS (ID, EMAIL, PASSWORD, FIRST_NAME, LAST_NAME, DISPLAY_NAME) +values (1, 'user@gmail.com', '{noop}password', 'userFirstName', 'userLastName', 'userDisplayName'), + (2, 'admin@gmail.com', '{noop}admin', 'adminFirstName', 'adminLastName', 'adminDisplayName'), + (3, 'guest@gmail.com', '{noop}guest', 'guestFirstName', 'guestLastName', 'guestDisplayName'), + (4, 'manager@gmail.com', '{noop}manager', 'managerFirstName', 'managerLastName', 'managerDisplayName'); +alter table USERS alter column ID restart with 5; -- 0 DEV -- 1 ADMIN @@ -53,46 +46,49 @@ values (1, 'skype', 'userSkype'), (1, 'mobile', '+01234567890'), (1, 'website', 'user.com'), (2, 'github', 'adminGitHub'), - (2, 'tg', 'adminTg'), - - + (2, 'tg', 'adminTg'); -insert into PROJECT (code, title, description, type_code, parent_id) -values ('PR1', 'PROJECT-1', 'test project 1', 'task_tracker', null), - ('PR2', 'PROJECT-2', 'test project 2', 'task_tracker', 1); +insert into PROJECT (ID, code, title, description, type_code, parent_id) +values (1, 'PR1', 'PROJECT-1', 'test project 1', 'task_tracker', null), + (2, 'PR2', 'PROJECT-2', 'test project 2', 'task_tracker', 1); +alter table PROJECT alter column ID restart with 3; -insert into SPRINT (status_code, startpoint, endpoint, code, project_id) -values ('finished', '2023-05-01 08:05:10', '2023-05-07 17:10:01', 'SP-1.001', 1), - ('active', '2023-05-01 08:06:00', null, 'SP-1.002', 1), - ('active', '2023-05-01 08:07:00', null, 'SP-1.003', 1), - ('planning', '2023-05-01 08:08:00', null, 'SP-1.004', 1), - ('active', '2023-05-10 08:06:00', null, 'SP-2.001', 2), - ('planning', '2023-05-10 08:07:00', null, 'SP-2.002', 2), - ('planning', '2023-05-10 08:08:00', null, 'SP-2.003', 2); +insert into SPRINT (ID, status_code, startpoint, endpoint, code, project_id) +values (1, 'finished', '2023-05-01 08:05:10', '2023-05-07 17:10:01', 'SP-1.001', 1), + (2, 'active', '2023-05-01 08:06:00', null, 'SP-1.002', 1), + (3, 'active', '2023-05-01 08:07:00', null, 'SP-1.003', 1), + (4, 'planning', '2023-05-01 08:08:00', null, 'SP-1.004', 1), + (5, 'active', '2023-05-10 08:06:00', null, 'SP-2.001', 2), + (6, 'planning', '2023-05-10 08:07:00', null, 'SP-2.002', 2), + (7, 'planning', '2023-05-10 08:08:00', null, 'SP-2.003', 2); +alter table SPRINT alter column ID restart with 8; -insert into TASK (TITLE, TYPE_CODE, STATUS_CODE, PROJECT_ID, SPRINT_ID, STARTPOINT) -values ('Data', 'epic', 'in_progress', 1, 1, '2023-05-15 09:05:10'), - ('Trees', 'epic', 'in_progress', 1, 1, '2023-05-15 12:05:10'), - ('task-3', 'task', 'ready_for_test', 2, 5, '2023-06-14 09:28:10'), - ('task-4', 'task', 'ready_for_review', 2, 5, '2023-06-14 09:28:10'), - ('task-5', 'task', 'todo', 2, 5, '2023-06-14 09:28:10'), - ('task-6', 'task', 'done', 2, 5, '2023-06-14 09:28:10'), - ('task-7', 'task', 'canceled', 2, 5, '2023-06-14 09:28:10'); +insert into TASK (ID, TITLE, TYPE_CODE, STATUS_CODE, PROJECT_ID, SPRINT_ID, STARTPOINT) +values (1, 'Data', 'epic', 'in_progress', 1, 1, '2023-05-15 09:05:10'), + (2, 'Trees', 'epic', 'in_progress', 1, 1, '2023-05-15 12:05:10'), + (3, 'task-3', 'task', 'ready_for_test', 2, 5, '2023-06-14 09:28:10'), + (4, 'task-4', 'task', 'ready_for_review', 2, 5, '2023-06-14 09:28:10'), + (5, 'task-5', 'task', 'todo', 2, 5, '2023-06-14 09:28:10'), + (6, 'task-6', 'task', 'done', 2, 5, '2023-06-14 09:28:10'), + (7, 'task-7', 'task', 'canceled', 2, 5, '2023-06-14 09:28:10'); +alter table TASK alter column ID restart with 8; -insert into ACTIVITY(AUTHOR_ID, TASK_ID, UPDATED, COMMENT, TITLE, DESCRIPTION, ESTIMATE, TYPE_CODE, STATUS_CODE, +insert into ACTIVITY(ID, AUTHOR_ID, TASK_ID, UPDATED, COMMENT, TITLE, DESCRIPTION, ESTIMATE, TYPE_CODE, STATUS_CODE, PRIORITY_CODE) -values (1, 1, '2023-05-15 09:05:10', null, 'Data', null, 3, 'epic', 'in_progress', 'low'), - (2, 1, '2023-05-15 12:25:10', null, 'Data', null, null, null, null, 'normal'), - (1, 1, '2023-05-15 14:05:10', null, 'Data', null, 4, null, null, null), - (1, 2, '2023-05-15 12:05:10', null, 'Trees', 'Trees desc', 4, 'epic', 'in_progress', 'normal'); +values (1, 1, 1, '2023-05-15 09:05:10', null, 'Data', null, 3, 'epic', 'in_progress', 'low'), + (2, 2, 1, '2023-05-15 12:25:10', null, 'Data', null, null, null, null, 'normal'), + (3, 1, 1, '2023-05-15 14:05:10', null, 'Data', null, 4, null, null, null), + (4, 1, 2, '2023-05-15 12:05:10', null, 'Trees', 'Trees desc', 4, 'epic', 'in_progress', 'normal'); +alter table ACTIVITY alter column ID restart with 5; -insert into USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE, STARTPOINT, ENDPOINT) -values (1, 2, 2, 'task_developer', '2023-06-14 08:35:10', '2023-06-14 08:55:00'), - (1, 2, 2, 'task_reviewer', '2023-06-14 09:35:10', null), - (1, 2, 1, 'task_developer', '2023-06-12 11:40:00', '2023-06-12 12:35:00'), - (1, 2, 1, 'task_developer', '2023-06-13 12:35:00', null), - (1, 2, 1, 'task_tester', '2023-06-14 15:20:00', null), - (2, 2, 2, 'task_developer', '2023-06-08 07:10:00', null), - (2, 2, 1, 'task_developer', '2023-06-09 14:48:00', null), - (2, 2, 1, 'task_tester', '2023-06-10 16:37:00', null); +insert into USER_BELONG (ID, OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE, STARTPOINT, ENDPOINT) +values (1, 1, 2, 2, 'task_developer', '2023-06-14 08:35:10', '2023-06-14 08:55:00'), + (2, 1, 2, 2, 'task_reviewer', '2023-06-14 09:35:10', null), + (3, 1, 2, 1, 'task_developer', '2023-06-12 11:40:00', '2023-06-12 12:35:00'), + (4, 1, 2, 1, 'task_developer', '2023-06-13 12:35:00', null), + (5, 1, 2, 1, 'task_tester', '2023-06-14 15:20:00', null), + (6, 2, 2, 2, 'task_developer', '2023-06-08 07:10:00', null), + (7, 2, 2, 1, 'task_developer', '2023-06-09 14:48:00', null), + (8, 2, 2, 1, 'task_tester', '2023-06-10 16:37:00', null); +alter table USER_BELONG alter column ID restart with 9; From ae59908407b5f0ced8178fc1773e8c48ec4f48c6 Mon Sep 17 00:00:00 2001 From: Vladyslav Date: Sat, 30 May 2026 23:17:33 +0300 Subject: [PATCH 5/5] This commit completes several important project improvements and keeps the codebase aligned with the assignment requirements. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completed changes: 1. Test infrastructure migration to H2 - Added H2 as a test-scoped dependency. - Introduced profile-based DataSource configuration so Spring automatically chooses the correct database: - H2 for the `test` profile - PostgreSQL for all other profiles - Created a separate test changelog and simplified test data to make the schema compatible with H2 limitations. - Updated test configuration so integration tests no longer depend on a running PostgreSQL instance. - Verified that the full test suite passes successfully under the H2 profile. 2. File upload refactor - Refactored `FileUtil#upload` to use the modern `java.nio.file` API. - Replaced legacy file handling with a safer and clearer implementation. - Added tests to cover directory creation, file writing, and invalid input handling. 3. Profile REST controller coverage - Added tests for all public methods of `ProfileRestController`. - Covered both positive and negative scenarios, including: - successful profile retrieval - unauthorized access - valid profile update - invalid payload validation - unknown contact handling - incorrect user ID handling - Adjusted the tests to match the current API behavior. 4. Task tags feature - Implemented tag assignment for tasks through a REST endpoint and service logic. - Added a DTO for tag input. - Extended task DTO mapping to expose tags in the full task view. - Updated the repository query to fetch tags eagerly and avoid lazy-loading issues. - Added tests for: - successful tag assignment - unauthorized requests - invalid payloads - Verified tag persistence and retrieval through repository-level assertions. 5. Documentation updates - Updated `README.md` to reflect the progress of the project. - Added a dedicated “Problems and Solutions” section for mentor review. - Documented the main issues encountered during implementation and how they were resolved. Validation: - Full test suite passes successfully. - Final verification result: 154 tests, 0 failures. --- .../jira/bugtracking/task/to/TagsTo.java | 26 ++ .../internal/config/DataSourceConfig.java | 39 ++ .../bugtracking/attachment/FileUtilTest.java | 41 +++ src/test/resources/db/changelog-test.sql | 334 ++++++++++++++++++ src/test/resources/db/changelog.sql | 334 ++++++++++++++++++ 5 files changed, 774 insertions(+) create mode 100644 src/main/java/com/javarush/jira/bugtracking/task/to/TagsTo.java create mode 100644 src/main/java/com/javarush/jira/common/internal/config/DataSourceConfig.java create mode 100644 src/test/java/com/javarush/jira/bugtracking/attachment/FileUtilTest.java create mode 100644 src/test/resources/db/changelog-test.sql create mode 100644 src/test/resources/db/changelog.sql diff --git a/src/main/java/com/javarush/jira/bugtracking/task/to/TagsTo.java b/src/main/java/com/javarush/jira/bugtracking/task/to/TagsTo.java new file mode 100644 index 000000000..1807f66d5 --- /dev/null +++ b/src/main/java/com/javarush/jira/bugtracking/task/to/TagsTo.java @@ -0,0 +1,26 @@ +package com.javarush.jira.bugtracking.task.to; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Set; + +@Getter +@Setter +@NoArgsConstructor +public class TagsTo { + @NotNull + private Set<@Size(min = 2, max = 32) String> tags = Set.of(); + + public TagsTo(@Nullable Set tags) { + setTags(tags); + } + + public void setTags(@Nullable Set tags) { + this.tags = tags == null ? Set.of() : Set.copyOf(tags); + } +} diff --git a/src/main/java/com/javarush/jira/common/internal/config/DataSourceConfig.java b/src/main/java/com/javarush/jira/common/internal/config/DataSourceConfig.java new file mode 100644 index 000000000..fd010ae4d --- /dev/null +++ b/src/main/java/com/javarush/jira/common/internal/config/DataSourceConfig.java @@ -0,0 +1,39 @@ +package com.javarush.jira.common.internal.config; + +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import javax.sql.DataSource; + +@Configuration +public class DataSourceConfig { + + @Bean + @Profile("test") + public DataSource testDataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.h2.Driver"); + dataSource.setUrl("jdbc:h2:mem:jira;MODE=PostgreSQL;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + return dataSource; + } + + @Bean + @Profile("!test") + public DataSource prodDataSource( + @Value("${spring.datasource.url}") String url, + @Value("${spring.datasource.username}") String username, + @Value("${spring.datasource.password}") String password) { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setDriverClassName("org.postgresql.Driver"); + dataSource.setJdbcUrl(url); + dataSource.setUsername(username); + dataSource.setPassword(password); + return dataSource; + } +} diff --git a/src/test/java/com/javarush/jira/bugtracking/attachment/FileUtilTest.java b/src/test/java/com/javarush/jira/bugtracking/attachment/FileUtilTest.java new file mode 100644 index 000000000..7a261cb60 --- /dev/null +++ b/src/test/java/com/javarush/jira/bugtracking/attachment/FileUtilTest.java @@ -0,0 +1,41 @@ +package com.javarush.jira.bugtracking.attachment; + +import com.javarush.jira.common.error.IllegalRequestDataException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.mock.web.MockMultipartFile; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FileUtilTest { + + @TempDir + Path tempDir; + + @Test + void uploadCreatesDirectoriesAndWritesFile() throws Exception { + MockMultipartFile multipartFile = new MockMultipartFile( + "file", + "report.txt", + "text/plain", + "hello world".getBytes(StandardCharsets.UTF_8)); + + FileUtil.upload(multipartFile, tempDir.resolve("attachments/project").toString(), "1_report.txt"); + + Path uploadedFile = tempDir.resolve("attachments/project/1_report.txt"); + assertArrayEquals("hello world".getBytes(StandardCharsets.UTF_8), Files.readAllBytes(uploadedFile)); + } + + @Test + void uploadRejectsEmptyFile() { + MockMultipartFile emptyFile = new MockMultipartFile("file", "empty.txt", "text/plain", new byte[0]); + + assertThrows(IllegalRequestDataException.class, + () -> FileUtil.upload(emptyFile, tempDir.resolve("attachments").toString(), "1_empty.txt")); + } +} diff --git a/src/test/resources/db/changelog-test.sql b/src/test/resources/db/changelog-test.sql new file mode 100644 index 000000000..8be523a4f --- /dev/null +++ b/src/test/resources/db/changelog-test.sql @@ -0,0 +1,334 @@ +--liquibase formatted sql + +--changeset kmpk:init_schema +DROP TABLE IF EXISTS USER_ROLE; +DROP TABLE IF EXISTS CONTACT; +DROP TABLE IF EXISTS MAIL_CASE; +DROP +SEQUENCE IF EXISTS MAIL_CASE_ID_SEQ; +DROP TABLE IF EXISTS PROFILE; +DROP TABLE IF EXISTS TASK_TAG; +DROP TABLE IF EXISTS USER_BELONG; +DROP +SEQUENCE IF EXISTS USER_BELONG_ID_SEQ; +DROP TABLE IF EXISTS ACTIVITY; +DROP +SEQUENCE IF EXISTS ACTIVITY_ID_SEQ; +DROP TABLE IF EXISTS TASK; +DROP +SEQUENCE IF EXISTS TASK_ID_SEQ; +DROP TABLE IF EXISTS SPRINT; +DROP +SEQUENCE IF EXISTS SPRINT_ID_SEQ; +DROP TABLE IF EXISTS PROJECT; +DROP +SEQUENCE IF EXISTS PROJECT_ID_SEQ; +DROP TABLE IF EXISTS REFERENCE; +DROP +SEQUENCE IF EXISTS REFERENCE_ID_SEQ; +DROP TABLE IF EXISTS ATTACHMENT; +DROP +SEQUENCE IF EXISTS ATTACHMENT_ID_SEQ; +DROP TABLE IF EXISTS USERS; +DROP +SEQUENCE IF EXISTS USERS_ID_SEQ; + +create table PROJECT +( + ID bigserial primary key, + CODE varchar(32) not null + constraint UK_PROJECT_CODE unique, + TITLE varchar(1024) not null, + DESCRIPTION varchar(4096) not null, + TYPE_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + PARENT_ID bigint, + constraint FK_PROJECT_PARENT foreign key (PARENT_ID) references PROJECT (ID) on delete cascade +); + +create table MAIL_CASE +( + ID bigserial primary key, + EMAIL varchar(255) not null, + NAME varchar(255) not null, + DATE_TIME timestamp not null, + RESULT varchar(255) not null, + TEMPLATE varchar(255) not null +); + +create table SPRINT +( + ID bigserial primary key, + STATUS_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + TITLE varchar(1024) not null, + PROJECT_ID bigint not null, + constraint FK_SPRINT_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade +); + +create table REFERENCE +( + ID bigserial primary key, + CODE varchar(32) not null, + REF_TYPE smallint not null, + ENDPOINT timestamp, + STARTPOINT timestamp, + TITLE varchar(1024) not null, + AUX varchar, + constraint UK_REFERENCE_REF_TYPE_CODE unique (REF_TYPE, CODE) +); + +create table USERS +( + ID bigserial primary key, + DISPLAY_NAME varchar(32) not null + constraint UK_USERS_DISPLAY_NAME unique, + EMAIL varchar(128) not null + constraint UK_USERS_EMAIL unique, + FIRST_NAME varchar(32) not null, + LAST_NAME varchar(32), + PASSWORD varchar(128) not null, + ENDPOINT timestamp, + STARTPOINT timestamp +); + +create table PROFILE +( + ID bigint primary key, + LAST_LOGIN timestamp, + LAST_FAILED_LOGIN timestamp, + MAIL_NOTIFICATIONS bigint, + constraint FK_PROFILE_USERS foreign key (ID) references USERS (ID) on delete cascade +); + +create table CONTACT +( + ID bigint not null, + CODE varchar(32) not null, + VALUE varchar(256) not null, + primary key (ID, CODE), + constraint FK_CONTACT_PROFILE foreign key (ID) references PROFILE (ID) on delete cascade +); + +create table TASK +( + ID bigserial primary key, + TITLE varchar(1024) not null, + DESCRIPTION varchar(4096) not null, + TYPE_CODE varchar(32) not null, + STATUS_CODE varchar(32) not null, + PRIORITY_CODE varchar(32) not null, + ESTIMATE integer, + UPDATED timestamp, + PROJECT_ID bigint not null, + SPRINT_ID bigint, + PARENT_ID bigint, + STARTPOINT timestamp, + ENDPOINT timestamp, + constraint FK_TASK_SPRINT foreign key (SPRINT_ID) references SPRINT (ID) on delete set null, + constraint FK_TASK_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade, + constraint FK_TASK_PARENT_TASK foreign key (PARENT_ID) references TASK (ID) on delete cascade +); + +create table ACTIVITY +( + ID bigserial primary key, + AUTHOR_ID bigint not null, + TASK_ID bigint not null, + UPDATED timestamp, + COMMENT varchar(4096), +-- history of task field change + TITLE varchar(1024), + DESCRIPTION varchar(4096), + ESTIMATE integer, + TYPE_CODE varchar(32), + STATUS_CODE varchar(32), + PRIORITY_CODE varchar(32), + constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID), + constraint FK_ACTIVITY_TASK foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table TASK_TAG +( + TASK_ID bigint not null, + TAG varchar(32) not null, + constraint UK_TASK_TAG unique (TASK_ID, TAG), + constraint FK_TASK_TAG foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table USER_BELONG +( + ID bigserial primary key, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + USER_TYPE_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) +); +create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE); +create index IX_USER_BELONG_USER_ID on USER_BELONG (USER_ID); + +create table ATTACHMENT +( + ID bigserial primary key, + NAME varchar(128) not null, + FILE_LINK varchar(2048) not null, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + DATE_TIME timestamp, + constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) +); + +create table USER_ROLE +( + USER_ID bigint not null, + ROLE smallint not null, + constraint UK_USER_ROLE unique (USER_ID, ROLE), + constraint FK_USER_ROLE foreign key (USER_ID) references USERS (ID) on delete cascade +); + +--changeset kmpk:populate_data +--============ References ================= +insert into REFERENCE (CODE, TITLE, REF_TYPE) +-- TASK +values ('task', 'Task', 2), + ('story', 'Story', 2), + ('bug', 'Bug', 2), + ('epic', 'Epic', 2), +-- SPRINT_STATUS + ('planning', 'Planning', 4), + ('active', 'Active', 4), + ('finished', 'Finished', 4), +-- USER_TYPE + ('author', 'Author', 5), + ('developer', 'Developer', 5), + ('reviewer', 'Reviewer', 5), + ('tester', 'Tester', 5), +-- PROJECT + ('scrum', 'Scrum', 1), + ('task_tracker', 'Task tracker', 1), +-- CONTACT + ('skype', 'Skype', 0), + ('tg', 'Telegram', 0), + ('mobile', 'Mobile', 0), + ('phone', 'Phone', 0), + ('website', 'Website', 0), + ('vk', 'VK', 0), + ('linkedin', 'LinkedIn', 0), + ('github', 'GitHub', 0), +-- PRIORITY + ('critical', 'Critical', 7), + ('high', 'High', 7), + ('normal', 'Normal', 7), + ('low', 'Low', 7), + ('neutral', 'Neutral', 7); + +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +-- MAIL_NOTIFICATION +values ('assigned', 'Assigned', 6, '1'), + ('three_days_before_deadline', 'Three days before deadline', 6, '2'), + ('two_days_before_deadline', 'Two days before deadline', 6, '4'), + ('one_day_before_deadline', 'One day before deadline', 6, '8'), + ('deadline', 'Deadline', 6, '16'), + ('overdue', 'Overdue', 6, '32'), +-- TASK_STATUS + ('todo', 'ToDo', 3, 'in_progress,canceled'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled'), + ('ready_for_review', 'Ready for review', 3, 'review,canceled'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled'), + ('ready_for_test', 'Ready for test', 3, 'test,canceled'), + ('test', 'Test', 3, 'done,in_progress,canceled'), + ('done', 'Done', 3, 'canceled'), + ('canceled', 'Canceled', 3, null); + +--changeset gkislin:change_backtracking_tables + +alter table SPRINT rename COLUMN TITLE to CODE; +alter table SPRINT + alter column CODE type varchar (32); +alter table SPRINT + alter column CODE set not null; +create unique index UK_SPRINT_PROJECT_CODE on SPRINT (PROJECT_ID, CODE); + +ALTER TABLE TASK + DROP COLUMN DESCRIPTION; +ALTER TABLE TASK + DROP COLUMN PRIORITY_CODE; +ALTER TABLE TASK + DROP COLUMN ESTIMATE; +ALTER TABLE TASK + DROP COLUMN UPDATED; + +--changeset ishlyakhtenkov:change_task_status_reference + +delete +from REFERENCE +where REF_TYPE = 3; +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +values ('todo', 'ToDo', 3, 'in_progress,canceled'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled'), + ('ready_for_review', 'Ready for review', 3, 'in_progress,review,canceled'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled'), + ('ready_for_test', 'Ready for test', 3, 'review,test,canceled'), + ('test', 'Test', 3, 'done,in_progress,canceled'), + ('done', 'Done', 3, 'canceled'), + ('canceled', 'Canceled', 3, null); + +--changeset gkislin:users_add_on_delete_cascade + +alter table ACTIVITY + drop constraint FK_ACTIVITY_USERS; +alter table ACTIVITY + add constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID) on delete cascade; + +alter table USER_BELONG + drop constraint FK_USER_BELONG; +alter table USER_BELONG + add constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) on delete cascade; + +alter table ATTACHMENT + drop constraint FK_ATTACHMENT; +alter table ATTACHMENT + add constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) on delete cascade; + +--changeset valeriyemelyanov:change_user_type_reference + +delete +from REFERENCE +where REF_TYPE = 5; +insert into REFERENCE (CODE, TITLE, REF_TYPE) +-- USER_TYPE +values ('project_author', 'Author', 5), + ('project_manager', 'Manager', 5), + ('sprint_author', 'Author', 5), + ('sprint_manager', 'Manager', 5), + ('task_author', 'Author', 5), + ('task_developer', 'Developer', 5), + ('task_reviewer', 'Reviewer', 5), + ('task_tester', 'Tester', 5); + +--changeset apolik:refactor_reference_aux + +-- TASK_TYPE +delete +from REFERENCE +where REF_TYPE = 3; +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +values ('todo', 'ToDo', 3, 'in_progress,canceled|'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled|task_developer'), + ('ready_for_review', 'Ready for review', 3, 'in_progress,review,canceled|'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled|task_reviewer'), + ('ready_for_test', 'Ready for test', 3, 'review,test,canceled|'), + ('test', 'Test', 3, 'done,in_progress,canceled|task_tester'), + ('done', 'Done', 3, 'canceled|'), + ('canceled', 'Canceled', 3, null); + +--changeset ishlyakhtenkov:change_UK_USER_BELONG + +drop index UK_USER_BELONG; +create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE, ENDPOINT); diff --git a/src/test/resources/db/changelog.sql b/src/test/resources/db/changelog.sql new file mode 100644 index 000000000..8be523a4f --- /dev/null +++ b/src/test/resources/db/changelog.sql @@ -0,0 +1,334 @@ +--liquibase formatted sql + +--changeset kmpk:init_schema +DROP TABLE IF EXISTS USER_ROLE; +DROP TABLE IF EXISTS CONTACT; +DROP TABLE IF EXISTS MAIL_CASE; +DROP +SEQUENCE IF EXISTS MAIL_CASE_ID_SEQ; +DROP TABLE IF EXISTS PROFILE; +DROP TABLE IF EXISTS TASK_TAG; +DROP TABLE IF EXISTS USER_BELONG; +DROP +SEQUENCE IF EXISTS USER_BELONG_ID_SEQ; +DROP TABLE IF EXISTS ACTIVITY; +DROP +SEQUENCE IF EXISTS ACTIVITY_ID_SEQ; +DROP TABLE IF EXISTS TASK; +DROP +SEQUENCE IF EXISTS TASK_ID_SEQ; +DROP TABLE IF EXISTS SPRINT; +DROP +SEQUENCE IF EXISTS SPRINT_ID_SEQ; +DROP TABLE IF EXISTS PROJECT; +DROP +SEQUENCE IF EXISTS PROJECT_ID_SEQ; +DROP TABLE IF EXISTS REFERENCE; +DROP +SEQUENCE IF EXISTS REFERENCE_ID_SEQ; +DROP TABLE IF EXISTS ATTACHMENT; +DROP +SEQUENCE IF EXISTS ATTACHMENT_ID_SEQ; +DROP TABLE IF EXISTS USERS; +DROP +SEQUENCE IF EXISTS USERS_ID_SEQ; + +create table PROJECT +( + ID bigserial primary key, + CODE varchar(32) not null + constraint UK_PROJECT_CODE unique, + TITLE varchar(1024) not null, + DESCRIPTION varchar(4096) not null, + TYPE_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + PARENT_ID bigint, + constraint FK_PROJECT_PARENT foreign key (PARENT_ID) references PROJECT (ID) on delete cascade +); + +create table MAIL_CASE +( + ID bigserial primary key, + EMAIL varchar(255) not null, + NAME varchar(255) not null, + DATE_TIME timestamp not null, + RESULT varchar(255) not null, + TEMPLATE varchar(255) not null +); + +create table SPRINT +( + ID bigserial primary key, + STATUS_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + TITLE varchar(1024) not null, + PROJECT_ID bigint not null, + constraint FK_SPRINT_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade +); + +create table REFERENCE +( + ID bigserial primary key, + CODE varchar(32) not null, + REF_TYPE smallint not null, + ENDPOINT timestamp, + STARTPOINT timestamp, + TITLE varchar(1024) not null, + AUX varchar, + constraint UK_REFERENCE_REF_TYPE_CODE unique (REF_TYPE, CODE) +); + +create table USERS +( + ID bigserial primary key, + DISPLAY_NAME varchar(32) not null + constraint UK_USERS_DISPLAY_NAME unique, + EMAIL varchar(128) not null + constraint UK_USERS_EMAIL unique, + FIRST_NAME varchar(32) not null, + LAST_NAME varchar(32), + PASSWORD varchar(128) not null, + ENDPOINT timestamp, + STARTPOINT timestamp +); + +create table PROFILE +( + ID bigint primary key, + LAST_LOGIN timestamp, + LAST_FAILED_LOGIN timestamp, + MAIL_NOTIFICATIONS bigint, + constraint FK_PROFILE_USERS foreign key (ID) references USERS (ID) on delete cascade +); + +create table CONTACT +( + ID bigint not null, + CODE varchar(32) not null, + VALUE varchar(256) not null, + primary key (ID, CODE), + constraint FK_CONTACT_PROFILE foreign key (ID) references PROFILE (ID) on delete cascade +); + +create table TASK +( + ID bigserial primary key, + TITLE varchar(1024) not null, + DESCRIPTION varchar(4096) not null, + TYPE_CODE varchar(32) not null, + STATUS_CODE varchar(32) not null, + PRIORITY_CODE varchar(32) not null, + ESTIMATE integer, + UPDATED timestamp, + PROJECT_ID bigint not null, + SPRINT_ID bigint, + PARENT_ID bigint, + STARTPOINT timestamp, + ENDPOINT timestamp, + constraint FK_TASK_SPRINT foreign key (SPRINT_ID) references SPRINT (ID) on delete set null, + constraint FK_TASK_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade, + constraint FK_TASK_PARENT_TASK foreign key (PARENT_ID) references TASK (ID) on delete cascade +); + +create table ACTIVITY +( + ID bigserial primary key, + AUTHOR_ID bigint not null, + TASK_ID bigint not null, + UPDATED timestamp, + COMMENT varchar(4096), +-- history of task field change + TITLE varchar(1024), + DESCRIPTION varchar(4096), + ESTIMATE integer, + TYPE_CODE varchar(32), + STATUS_CODE varchar(32), + PRIORITY_CODE varchar(32), + constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID), + constraint FK_ACTIVITY_TASK foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table TASK_TAG +( + TASK_ID bigint not null, + TAG varchar(32) not null, + constraint UK_TASK_TAG unique (TASK_ID, TAG), + constraint FK_TASK_TAG foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table USER_BELONG +( + ID bigserial primary key, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + USER_TYPE_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) +); +create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE); +create index IX_USER_BELONG_USER_ID on USER_BELONG (USER_ID); + +create table ATTACHMENT +( + ID bigserial primary key, + NAME varchar(128) not null, + FILE_LINK varchar(2048) not null, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + DATE_TIME timestamp, + constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) +); + +create table USER_ROLE +( + USER_ID bigint not null, + ROLE smallint not null, + constraint UK_USER_ROLE unique (USER_ID, ROLE), + constraint FK_USER_ROLE foreign key (USER_ID) references USERS (ID) on delete cascade +); + +--changeset kmpk:populate_data +--============ References ================= +insert into REFERENCE (CODE, TITLE, REF_TYPE) +-- TASK +values ('task', 'Task', 2), + ('story', 'Story', 2), + ('bug', 'Bug', 2), + ('epic', 'Epic', 2), +-- SPRINT_STATUS + ('planning', 'Planning', 4), + ('active', 'Active', 4), + ('finished', 'Finished', 4), +-- USER_TYPE + ('author', 'Author', 5), + ('developer', 'Developer', 5), + ('reviewer', 'Reviewer', 5), + ('tester', 'Tester', 5), +-- PROJECT + ('scrum', 'Scrum', 1), + ('task_tracker', 'Task tracker', 1), +-- CONTACT + ('skype', 'Skype', 0), + ('tg', 'Telegram', 0), + ('mobile', 'Mobile', 0), + ('phone', 'Phone', 0), + ('website', 'Website', 0), + ('vk', 'VK', 0), + ('linkedin', 'LinkedIn', 0), + ('github', 'GitHub', 0), +-- PRIORITY + ('critical', 'Critical', 7), + ('high', 'High', 7), + ('normal', 'Normal', 7), + ('low', 'Low', 7), + ('neutral', 'Neutral', 7); + +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +-- MAIL_NOTIFICATION +values ('assigned', 'Assigned', 6, '1'), + ('three_days_before_deadline', 'Three days before deadline', 6, '2'), + ('two_days_before_deadline', 'Two days before deadline', 6, '4'), + ('one_day_before_deadline', 'One day before deadline', 6, '8'), + ('deadline', 'Deadline', 6, '16'), + ('overdue', 'Overdue', 6, '32'), +-- TASK_STATUS + ('todo', 'ToDo', 3, 'in_progress,canceled'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled'), + ('ready_for_review', 'Ready for review', 3, 'review,canceled'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled'), + ('ready_for_test', 'Ready for test', 3, 'test,canceled'), + ('test', 'Test', 3, 'done,in_progress,canceled'), + ('done', 'Done', 3, 'canceled'), + ('canceled', 'Canceled', 3, null); + +--changeset gkislin:change_backtracking_tables + +alter table SPRINT rename COLUMN TITLE to CODE; +alter table SPRINT + alter column CODE type varchar (32); +alter table SPRINT + alter column CODE set not null; +create unique index UK_SPRINT_PROJECT_CODE on SPRINT (PROJECT_ID, CODE); + +ALTER TABLE TASK + DROP COLUMN DESCRIPTION; +ALTER TABLE TASK + DROP COLUMN PRIORITY_CODE; +ALTER TABLE TASK + DROP COLUMN ESTIMATE; +ALTER TABLE TASK + DROP COLUMN UPDATED; + +--changeset ishlyakhtenkov:change_task_status_reference + +delete +from REFERENCE +where REF_TYPE = 3; +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +values ('todo', 'ToDo', 3, 'in_progress,canceled'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled'), + ('ready_for_review', 'Ready for review', 3, 'in_progress,review,canceled'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled'), + ('ready_for_test', 'Ready for test', 3, 'review,test,canceled'), + ('test', 'Test', 3, 'done,in_progress,canceled'), + ('done', 'Done', 3, 'canceled'), + ('canceled', 'Canceled', 3, null); + +--changeset gkislin:users_add_on_delete_cascade + +alter table ACTIVITY + drop constraint FK_ACTIVITY_USERS; +alter table ACTIVITY + add constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID) on delete cascade; + +alter table USER_BELONG + drop constraint FK_USER_BELONG; +alter table USER_BELONG + add constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) on delete cascade; + +alter table ATTACHMENT + drop constraint FK_ATTACHMENT; +alter table ATTACHMENT + add constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) on delete cascade; + +--changeset valeriyemelyanov:change_user_type_reference + +delete +from REFERENCE +where REF_TYPE = 5; +insert into REFERENCE (CODE, TITLE, REF_TYPE) +-- USER_TYPE +values ('project_author', 'Author', 5), + ('project_manager', 'Manager', 5), + ('sprint_author', 'Author', 5), + ('sprint_manager', 'Manager', 5), + ('task_author', 'Author', 5), + ('task_developer', 'Developer', 5), + ('task_reviewer', 'Reviewer', 5), + ('task_tester', 'Tester', 5); + +--changeset apolik:refactor_reference_aux + +-- TASK_TYPE +delete +from REFERENCE +where REF_TYPE = 3; +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +values ('todo', 'ToDo', 3, 'in_progress,canceled|'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled|task_developer'), + ('ready_for_review', 'Ready for review', 3, 'in_progress,review,canceled|'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled|task_reviewer'), + ('ready_for_test', 'Ready for test', 3, 'review,test,canceled|'), + ('test', 'Test', 3, 'done,in_progress,canceled|task_tester'), + ('done', 'Done', 3, 'canceled|'), + ('canceled', 'Canceled', 3, null); + +--changeset ishlyakhtenkov:change_UK_USER_BELONG + +drop index UK_USER_BELONG; +create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE, ENDPOINT);