diff --git a/.env b/.env new file mode 100644 index 000000000..13a97d096 --- /dev/null +++ b/.env @@ -0,0 +1,20 @@ +# Database +DB_URL=jdbc:postgresql://localhost:5432/jira +DB_USER_NAME=jira +DB_USER_PWD=JiraRush + +# OAuth2 - GitHub +GITHUB_CLIENT_ID=3d0d8738e65881fff266 +GITHUB_CLIENT_SECRET=0f97031ce6178bdf7b67a6af587f37e222a16120 + +# OAuth2 - Google +GOOGLE_CLIENT_ID=329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=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 + +# OAuth2 - GitLab +GITLAB_CLIENT_ID=b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494 +GITLAB_CLIENT_SECRET=e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004 + +# Mail +MAIL_USER_NAME=jira4jr@gmail.com +MAIL_PASSWORD=zdfzsrqvgimldzyj \ No newline at end of file diff --git a/.gitignore b/.gitignore index cd38e2e7b..ffc994e15 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ target logs attachments *.patch - - +#.env +pgdata/ +pgdata-test/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..5b09d9e8c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# Этап сборки +FROM maven:3.9.6-eclipse-temurin-17-alpine AS build + +WORKDIR /app + +COPY pom.xml . +RUN mvn dependency:go-offline + +COPY src ./src +COPY resources ./resources +RUN mvn clean package -DskipTests + +# Этап запуска +FROM eclipse-temurin:17-jre-alpine + +WORKDIR /app + +# Копируем JAR из этапа сборки +COPY --from=build /app/target/*.jar app.jar +COPY --from=build /app/resources ./resources + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 719b268f5..0671d4311 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,76 @@ - https://habr.com/ru/articles/259055/ Список выполненных задач: +## Команды загрузки +```bash +docker run -p 5432:5432 --name postgres-db -e POSTGRES_USER=jira -e POSTGRES_PASSWORD=JiraRush -e POSTGRES_DB=jira -e PGDATA=/var/lib/postgresql/data/pgdata -v $(pwd)/pgdata:/var/lib/postgresql/data -d postgres +``` +```bash +docker run -p 5433:5432 --name postgres-db-test -e POSTGRES_USER=jira -e POSTGRES_PASSWORD=JiraRush -e POSTGRES_DB=jira-test -e PGDATA=/var/lib/postgresql/data/pgdata -v $(pwd)/pgdata-test:/var/lib/postgresql/data -d postgres +``` + +1. Разобраться со структурой проекта (onboarding).✅ + +2. Удалить социальные сети: vk, yandex. `Easy task`✅ + +3. Вынести чувствительную информацию в отдельный проперти файл: ✅ + - логин + - пароль БД + - идентификаторы для OAuth регистрации/авторизации + - настройки почты + + Значения этих проперти должны считываться при старте сервера из переменных окружения машины. `Easy task` + - ``` + добавлен .env файл в проект + написан тест + проект учебный .env не добавлен в gitignore + ``` + +4. Переделать тесты так, чтоб во время тестов использовалась **in memory БД (H2)**, а не PostgreSQL. Для этого нужно определить 2 бина, и выборка какой из них использовать должно определяться активным профилем Spring. H2 не поддерживает все фичи, которые есть у PostgreSQL, поэтому тебе прийдется немного упростить скрипты с тестовыми данными. + +5. Написать тесты для всех публичных методов контроллера `ProfileRestController`. Хоть методов только 2, но тестовых методов должно быть больше, т.к. нужно проверить success and unsuccess path. ✅ + +6. Сделать рефакторинг метода `com.javarush.jira.bugtracking.attachment.FileUtil#upload` чтоб он использовал современный подход для работы с файловой системмой. `Easy task` ✅ + +7. Добавить новый функционал: добавления тегов к задаче (REST API + реализация на сервисе). Фронт делать необязательно. Таблица `task_tag` уже создана. + +8. Добавить подсчет времени сколько задача находилась в работе и тестировании. Написать 2 метода на уровне сервиса, которые параметром принимают задачу и возвращают затраченное время: ✅ + - Сколько задача находилась в работе (ready_for_review минус in_progress). + - Сколько задача находилась на тестировании (done минус ready_for_review). + + Для написания этого задания, нужно добавить в конец скрипта инициализации базы данных `changelog.sql` **3 записи** в таблицу `ACTIVITY`: + ``` + insert into ACTIVITY ( ID, AUTHOR_ID, TASK_ID, UPDATED, STATUS_CODE ) values ... + ``` +Со статусами: +- время начала работы над задачей – **in_progress** +- время окончания разработки - **ready_for_review** +- время конца тестирования - **done** + +9. Написать `Dockerfile` для основного сервера ✅ +``` +добавлен Dockerfile и docker-compose.yml +docker build --no-cache --progress=plain -t project-final-app . + +``` +```bash +docker build --no-cache --progress=plain -t project-final-app . +``` +```bash +docker-compose up -d +``` + +10. Написать `docker-compose` файл для запуска контейнера сервера вместе с БД и [nginx](https://nginx.org/ru/docs/). Для nginx используй конфиг-файл `config/nginx.conf`. При необходимости файл конфига можно редактировать. `Hard task` ✅ +```bash +docker-compose down -v +``` +```bash +docker-compose up -d --build +``` +```bash + +``` +11. Добавить локализацию минимум на двух языках для шаблонов писем (mails) и стартовой страницы `index.html`. + +12. Переделать механизм распознавания «свой-чужой» между фронтом и беком с `JSESSIONID` на `JWT`. Из сложностей – тебе придётся переделать отправку форм с фронта, чтоб добавлять хедер аутентификации. `Extra-hard task` ... \ No newline at end of file diff --git a/config/nginx.conf b/config/nginx.conf index 82b9e234d..0f6ac4ae1 100644 --- a/config/nginx.conf +++ b/config/nginx.conf @@ -2,39 +2,57 @@ # 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; - - # 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 X-Forwarded-For $proxy_add_x_forwarded_for; - root /opt/jirarush/resources; - - 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; - } + listen 80; + + 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; + + if ($request_uri ~ ';') { return 404; } + + location /static/ { + expires 30d; + access_log off; + } + + location /robots.txt { + access_log off; + } + + # API endpoints - должны идти первыми + location /api/ { + proxy_pass http://app:8080/api/; + proxy_connect_timeout 150s; + 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; + } + + location /actuator/ { + proxy_pass http://app:8080/actuator/; + proxy_connect_timeout 150s; + 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; + } + + # UI и основные страницы + location ~ (/$|/view/|/ui/|/oauth2/) { + expires 0m; + proxy_pass http://app:8080; + proxy_connect_timeout 30s; + 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; + } + + location / { + try_files /view/404.html = 404; + } } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..14d0479a2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,55 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: jira_postgres + environment: + POSTGRES_DB: jira + POSTGRES_USER: ${DB_USER_NAME} + POSTGRES_PASSWORD: ${DB_USER_PWD} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - jira_network + restart: unless-stopped + + app: + build: . + container_name: jira_app + env_file: + - .env + environment: + DB_URL: jdbc:postgresql://postgres:5432/jira + depends_on: + - postgres + networks: + - jira_network + restart: unless-stopped + + nginx: + image: nginx:alpine + container_name: jira_nginx + ports: + - "80:80" + volumes: + # Монтируем наш конфиг в sites-available + - ./config/nginx.conf:/etc/nginx/sites-available/jirarush.conf:ro + # Создаём симлинк в sites-enabled + - ./config/nginx.conf:/etc/nginx/sites-enabled/jirarush.conf:ro + # Монтируем статику + - ./resources:/opt/jirarush/resources:ro + depends_on: + - app + networks: + - jira_network + restart: unless-stopped + +volumes: + postgres_data: + +networks: + jira_network: + driver: bridge diff --git a/pom.xml b/pom.xml index f6c152c68..1b6de20ca 100644 --- a/pom.xml +++ b/pom.xml @@ -142,6 +142,12 @@ junit-platform-launcher test + + + me.paulschwarz + spring-dotenv + 4.0.0 + 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/bugtracking/attachment/FileUtil.java b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java index 6cffbe175..efd19255f 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,8 @@ 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 +23,15 @@ 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); + if (!Files.exists(dir)) { + Files.createDirectories(dir); } + Path file = dir.resolve(fileName); + Files.write(file, multipartFile.getBytes()); + } catch (IOException e) { + throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename()); } } 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..d5af54998 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java @@ -19,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; +import java.time.Duration; import java.time.LocalDateTime; import java.util.List; @@ -140,4 +141,62 @@ private void checkAssignmentActionPossible(long id, String userType, boolean ass throw new DataConflictException(String.format(assign ? CANNOT_ASSIGN : CANNOT_UN_ASSIGN, userType, task.getStatusCode())); } } + + private Long checkTaskTimeInWork(Task task) { + if(task == null) { + return 0L; + } + List activities = activityHandler.getRepository().findAllByTaskIdOrderByUpdatedDesc(task.getId()); + if(activities == null || activities.isEmpty()) { + return 0L; + } + LocalDateTime readyForReviewTime = null; + LocalDateTime inProgressTime = null; + for (Activity activity : activities) { + String statusCode = activity.getStatusCode(); + LocalDateTime updated = activity.getUpdated(); + + if ("in_progress".equals(statusCode)) { + inProgressTime = updated; + } else if ("ready_for_review".equals(statusCode)) { + readyForReviewTime = updated; + break; + } + } + + if (inProgressTime != null && readyForReviewTime != null) { + return Duration.between(inProgressTime, readyForReviewTime).toSeconds(); + } + return 0L; + } + + private Long checkTaskTimeInTesting(Task task) { + if(task == null) { + return 0L; + } + List activities = activityHandler.getRepository().findAllByTaskIdOrderByUpdatedDesc(task.getId()); + if(activities == null || activities.isEmpty()) { + return 0L; + } + + LocalDateTime readyForReviewTime = null; + LocalDateTime doneTime = null; + + for (Activity activity : activities) { + String statusCode = activity.getStatusCode(); + LocalDateTime updated = activity.getUpdated(); + + if ("ready_for_review".equals(statusCode)) { + readyForReviewTime = updated; + } else if ("done".equals(statusCode)) { + doneTime = updated; + break; + } + } + + if (readyForReviewTime != null && doneTime != null) { + return Duration.between(readyForReviewTime, doneTime).toSeconds(); + } + return 0L; + } } 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..b9184421d 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 @@ -12,6 +12,7 @@ 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.HandlerExceptionResolver; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; @@ -24,6 +25,9 @@ import java.time.Duration; import java.util.Properties; +import org.springframework.context.annotation.Primary; +import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; + //@EnableWebMvc : http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration @EnableAutoConfiguration @Configuration 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..d0e9b390c 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 @@ -12,18 +12,24 @@ import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import java.util.List; + @Component @AllArgsConstructor public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { - @Qualifier("handlerExceptionResolver") - private final HandlerExceptionResolver resolver; + private final List resolvers; private final RequestMappingHandlerMapping mapping; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws ServletException { try { HandlerExecutionChain handler = mapping.getHandler(request); - resolver.resolveException(request, response, handler == null ? null : handler.getHandler(), authException); + // Пробуем каждый резолвер, пока один не обработает исключение + for (HandlerExceptionResolver resolver : resolvers) { + if (resolver.resolveException(request, response, handler == null ? null : handler.getHandler(), authException) != null) { + break; + } + } } catch (Exception e) { throw new ServletException(e); } 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..a3a54580d 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -26,9 +26,9 @@ spring: # 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:jdbc:postgresql://localhost:5432/jira}' + username: '${DB_USER_NAME:jira}' + password: '${DB_USER_PWD:JiraRush}' 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:3d0d8738e65881fff266}' + client-secret: '${GITHUB_CLIENT_SECRET:0f97031ce6178b7dfb67a6af587f37e222a16120}' scope: - email google: - client-id: 329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com - client-secret: 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 + client-id: '${GOOGLE_CLIENT-ID:329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com}' + client-secret: '${GOOGLE_CLIENT_SECRET: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}' 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:b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494}' + client-secret: '${GITLAB_CLIENT_SECRET:e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004}' 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_USER_NAME:jira4jr@gmail.com}' + password: '${MAIL_PASSWORD:zdfzsrqvgimldzyj}' port: 587 thymeleaf.check-template-location: false diff --git a/src/main/resources/db/changelog.sql b/src/main/resources/db/changelog.sql index 68591336d..4a8a3f920 100644 --- a/src/main/resources/db/changelog.sql +++ b/src/main/resources/db/changelog.sql @@ -329,3 +329,9 @@ values ('todo', 'ToDo', 3, 'in_progress,canceled|'), drop index UK_USER_BELONG; create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE) where ENDPOINT is null; + +insert into ACTIVITY (AUTHOR_ID, TASK_ID, UPDATED, STATUS_CODE) +values + (1, 1, '2024-01-01 10:00:00', 'in_progress'), + (1, 1, '2024-01-01 15:00:00', 'ready_for_review'), + (1, 1, '2024-01-02 10:00:00', 'done'); \ No newline at end of file diff --git a/src/test/java/com/javarush/jira/LoadEnvTest.java b/src/test/java/com/javarush/jira/LoadEnvTest.java new file mode 100644 index 000000000..4457550d5 --- /dev/null +++ b/src/test/java/com/javarush/jira/LoadEnvTest.java @@ -0,0 +1,59 @@ +package com.javarush.jira; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.beans.factory.annotation.Value; + +@SpringBootTest +public class LoadEnvTest { + + @Value("${DB_URL:NOT_SET}") + private String dbUrl; + + @Value("${DB_USER_NAME:NOT_SET}") + private String dbUserName; + + @Value("${DB_USER_PWD:NOT_SET}") + private String dbUserPwd; + + @Value("${GITHUB_CLIENT_ID:NOT_SET}") + private String githubClientId; + + @Value("${GITHUB_CLIENT_SECRET:NOT_SET}") + private String githubClientSecret; + + @Value("${GOOGLE_CLIENT_ID:NOT_SET}") + private String googleClientId; + + @Value("${GOOGLE_CLIENT_SECRET:NOT_SET}") + private String googleClientSecret; + + @Value("${GITLAB_CLIENT_ID:NOT_SET}") + private String gitlabClientId; + + @Value("${GITLAB_CLIENT_SECRET:NOT_SET}") + private String gitlabClientSecret; + + @Value("${MAIL_USER_NAME:NOT_SET}") + private String mailUserName; + + @Value("${MAIL_PASSWORD:NOT_SET}") + private String mailPassword; + + @Test + void printAllEnvVariables() { + System.out.println("\n=== ENV VARIABLES CHECK ==="); + System.out.println("DB_URL: " + dbUrl); + System.out.println("DB_USER_NAME: " + dbUserName); + System.out.println("DB_USER_PWD: " + dbUserPwd); + System.out.println("GITHUB_CLIENT_ID: " + githubClientId); + System.out.println("GITHUB_CLIENT_SECRET: " + githubClientSecret); + System.out.println("GOOGLE_CLIENT_ID: " + googleClientId); + System.out.println("GOOGLE_CLIENT_SECRET: " + googleClientSecret); + System.out.println("GITLAB_CLIENT_ID: " + gitlabClientId); + System.out.println("GITLAB_CLIENT_SECRET: " + gitlabClientSecret); + System.out.println("MAIL_USER_NAME: " + mailUserName); + System.out.println("MAIL_PASSWORD: " + mailPassword); + System.out.println("============================\n"); + } +} 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..1a8841fa6 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,133 @@ package com.javarush.jira.profile.internal.web; import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.profile.ProfileTo; +import org.junit.jupiter.api.Test; +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 com.javarush.jira.profile.internal.web.ProfileRestController.REST_URL; +import static com.javarush.jira.profile.internal.web.ProfileTestData.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; class ProfileRestControllerTest extends AbstractControllerTest { + @Test + @WithUserDetails(value = USER_MAIL) + void get() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.mailNotifications").isArray()) + .andExpect(jsonPath("$.contacts").isArray()); + } + + @Test + void getUnAuth() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void update() throws Exception { + ProfileTo updatedTo = getUpdatedTo(); + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonWithContacts(updatedTo))) + .andDo(print()) + .andExpect(status().isNoContent()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateInvalid() throws Exception { + ProfileTo invalidTo = getInvalidTo(); + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonWithContacts(invalidTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateWithUnknownNotification() throws Exception { + ProfileTo invalidTo = getWithUnknownNotificationTo(); + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonWithContacts(invalidTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateWithUnknownContact() throws Exception { + ProfileTo invalidTo = getWithUnknownContactTo(); + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonWithContacts(invalidTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateWithHtmlUnsafeContact() throws Exception { + ProfileTo invalidTo = getWithContactHtmlUnsafeTo(); + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonWithContacts(invalidTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void updateUnAuth() throws Exception { + ProfileTo updatedTo = getUpdatedTo(); + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonWithContacts(updatedTo))) + .andExpect(status().isUnauthorized()); + } + + private String jsonWithContacts(ProfileTo profileTo) { + StringBuilder json = new StringBuilder(); + json.append("{"); + + if (profileTo.getId() != null) { + json.append("\"id\":").append(profileTo.getId()).append(","); + } + + if (profileTo.getMailNotifications() != null) { + json.append("\"mailNotifications\":["); + json.append(String.join(",", + profileTo.getMailNotifications().stream() + .map(s -> "\"" + s + "\"") + .toArray(String[]::new))); + json.append("],"); + } + + if (profileTo.getContacts() != null) { + json.append("\"contacts\":["); + json.append(profileTo.getContacts().stream() + .map(c -> String.format("{\"code\":\"%s\",\"value\":\"%s\"}", + c.getCode(), c.getValue())) + .reduce((a, b) -> a + "," + b) + .orElse("")); + json.append("]"); + } + + String result = json.toString(); + if (result.endsWith(",")) { + result = result.substring(0, result.length() - 1); + } + result += "}"; + return result; + } } \ No newline at end of file diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 51137fd06..c78994ef3 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -4,5 +4,5 @@ spring: mode: always datasource: url: jdbc:postgresql://localhost:5433/jira-test - username: jira - password: JiraRush \ No newline at end of file + username: '${TEST_DB_USER_NAME:jira}' + password: '${TEST_DB_USER_PWD:JiraRush}' \ No newline at end of file