From 06b2a284b32b41c8a9a060dd188683eb15d09e71 Mon Sep 17 00:00:00 2001
From: Yaroslav <6i4bbb@gmail.com>
Date: Sat, 5 Jul 2025 14:57:54 +0300
Subject: [PATCH 1/7] Add lombok version and configure annotation processions
in Maven
---
pom.xml | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/pom.xml b/pom.xml
index f6c152c68..966798402 100644
--- a/pom.xml
+++ b/pom.xml
@@ -97,12 +97,15 @@
org.projectlombok
lombok
true
+ 1.18.30
+
org.mapstruct
mapstruct
${mapstruct.version}
+
org.springframework.boot
spring-boot-configuration-processor
@@ -146,6 +149,26 @@
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.6.0.Beta1
+
+
+
+
+
org.springframework.boot
spring-boot-maven-plugin
From 95fc847020b34f0c70775496c92fc86553b8813d Mon Sep 17 00:00:00 2001
From: Yaroslav <6i4bbb@gmail.com>
Date: Mon, 21 Jul 2025 17:52:58 +0300
Subject: [PATCH 2/7] Remove VK & Yandex
---
compose.yaml | 14 ++++++++
config/nginx.conf | 2 +-
resources/static/fontawesome/css/all.css | 12 ++-----
resources/view/login.html | 8 -----
resources/view/unauth/register.html | 8 -----
.../handler/VkOAuth2UserDataHandler.java | 35 -------------------
.../handler/YandexOAuth2UserDataHandler.java | 21 -----------
src/main/resources/application.yaml | 24 -------------
src/main/resources/data4dev/data.sql | 3 +-
.../profile/internal/web/ProfileTestData.java | 2 --
src/test/resources/data.sql | 3 +-
11 files changed, 20 insertions(+), 112 deletions(-)
create mode 100644 compose.yaml
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/compose.yaml b/compose.yaml
new file mode 100644
index 000000000..d52c3716b
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,14 @@
+
+services:
+ postgres-db:
+ image: postgres
+ container_name: postgres-db
+ ports:
+ - "5432:5432"
+ environment:
+ POSTGRES_USER: jira
+ POSTGRES_PASSWORD: JiraRush
+ PGDATA: /var/lib/postgresql/data/pgdata
+ volumes:
+ - ./pgdata:/var/lib/postgresql/data
+ restart: unless-stopped
\ No newline at end of file
diff --git a/config/nginx.conf b/config/nginx.conf
index 82b9e234d..024e03cf5 100644
--- a/config/nginx.conf
+++ b/config/nginx.conf
@@ -1,4 +1,4 @@
-# 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 {
diff --git a/resources/static/fontawesome/css/all.css b/resources/static/fontawesome/css/all.css
index af5980828..6a347647c 100644
--- a/resources/static/fontawesome/css/all.css
+++ b/resources/static/fontawesome/css/all.css
@@ -8603,9 +8603,7 @@ readers do not read off random characters that represent icons */
content: "\f3e8";
}
-.fa-vk:before {
- content: "\f189";
-}
+
.fa-untappd:before {
content: "\f405";
@@ -9955,9 +9953,7 @@ readers do not read off random characters that represent icons */
content: "\f3bc";
}
-.fa-yandex:before {
- content: "\f413";
-}
+
.fa-readme:before {
content: "\f4d5";
@@ -10183,9 +10179,7 @@ readers do not read off random characters that represent icons */
content: "\f7c6";
}
-.fa-yandex-international:before {
- content: "\f414";
-}
+
.fa-cc-amex:before {
content: "\f1f3";
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/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
+
+
+ me.paulschwarz
+ spring-dotenv
+ 4.0.0
+
+
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index d9660eb05..c779529bc 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -1,11 +1,10 @@
-# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
app:
- host-url: http://localhost:8080
- test-mail: jira4jr@gmail.com
+ host-url: ${APP_HOST_URL:http://localhost:8080}
+ test-mail: ${APP_TEST_MAIL:jira4jr@gmail.com}
templates-update-cache: 5s
mail-sending-props:
- core-pool-size: 8
- max-pool-size: 100
+ core-pool-size: ${MAIL_CORE_POOL_SIZE:8}
+ max-pool-size: ${MAIL_MAX_POOL_SIZE:100}
spring:
init:
@@ -13,27 +12,21 @@ spring:
jpa:
show-sql: true
open-in-view: false
-
- # validate db by model
hibernate:
ddl-auto: validate
-
properties:
- # http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#configurations
hibernate:
format_sql: true
default_batch_fetch_size: 20
- # https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc
jdbc.batch_size: 20
datasource:
- url: jdbc:postgresql://localhost:5432/jira
- username: jira
- password: JiraRush
+ url: ${DB_URL:jdbc:postgresql://localhost:5432/jira}
+ username: ${DB_USERNAME:jira}
+ password: ${DB_PASSWORD:JiraRush}
liquibase:
changeLog: "classpath:db/changelog.sql"
- # Jackson Fields Serialization
jackson:
visibility:
field: any
@@ -41,7 +34,6 @@ spring:
setter: none
is-getter: none
- # https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties-cache
cache:
cache-names: users
caffeine.spec: maximumSize=10000,expireAfterAccess=5m
@@ -51,19 +43,19 @@ 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
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
@@ -86,10 +78,11 @@ spring:
starttls:
enable: true
auth: true
- host: smtp.gmail.com
- username: jira4jr@gmail.com
- password: zdfzsrqvgimldzyj
- port: 587
+ host: ${MAIL_HOST:smtp.gmail.com}
+ username: ${MAIL_USERNAME:jira4jr@gmail.com}
+ password: ${MAIL_PASSWORD}
+ port: ${MAIL_PORT:587}
+
thymeleaf.check-template-location: false
mvc.throw-exception-if-no-handler-found: true
@@ -103,11 +96,11 @@ logging:
org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: DEBUG
server:
- # https://springdoc.org/index.html#how-can-i-deploy-springdoc-openapi-ui-behind-a-reverse-proxy
forward-headers-strategy: framework
servlet:
encoding:
- charset: UTF-8 # Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly
- enabled: true # Enable http encoding support
+ charset: UTF-8
+ enabled: true
force: true
+
springdoc.swagger-ui.path: /doc
From 5c0bfc16b9c8b669e30a5bbc0943df171cb22510 Mon Sep 17 00:00:00 2001
From: Yaroslav <6i4bbb@gmail.com>
Date: Mon, 21 Jul 2025 18:14:16 +0300
Subject: [PATCH 4/7] Sensitive information hide
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index cd38e2e7b..7f9709787 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,6 @@ target
logs
attachments
*.patch
+.env
From 45393f9683a6dd5407b61d6e1f7c86784c6537c2 Mon Sep 17 00:00:00 2001
From: Yaroslav <6i4bbb@gmail.com>
Date: Tue, 29 Jul 2025 15:09:40 +0300
Subject: [PATCH 5/7] Add Swagger descriptions for improved API documentation
clarity
---
.../attachment/AttachmentController.java | 21 +++++++
.../project/ProjectController.java | 22 +++++++
.../report/ReportRestController.java | 5 ++
.../bugtracking/sprint/SprintController.java | 29 +++++++++
.../jira/bugtracking/task/TaskController.java | 61 +++++++++++++++++++
.../bugtracking/tree/TreeRestController.java | 17 ++++++
.../internal/web/AdminUserController.java | 33 ++++++++++
.../login/internal/web/UserController.java | 21 +++++++
.../internal/web/ProfileRestController.java | 9 +++
.../ref/internal/web/ReferenceController.java | 25 ++++++++
10 files changed, 243 insertions(+)
diff --git a/src/main/java/com/javarush/jira/bugtracking/attachment/AttachmentController.java b/src/main/java/com/javarush/jira/bugtracking/attachment/AttachmentController.java
index 2ddd79591..1cc1f7b5f 100644
--- a/src/main/java/com/javarush/jira/bugtracking/attachment/AttachmentController.java
+++ b/src/main/java/com/javarush/jira/bugtracking/attachment/AttachmentController.java
@@ -2,6 +2,7 @@
import com.javarush.jira.bugtracking.ObjectType;
import com.javarush.jira.login.AuthUser;
+import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
@@ -27,6 +28,10 @@ public class AttachmentController {
@Transactional
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+ @Operation(
+ summary = "Завантажити файл вкладення",
+ description = "Додає нове вкладення до об'єкта певного типу (наприклад, задачі чи проєкту) та зберігає його у файловій системі"
+ )
public ResponseEntity upload(@RequestPart MultipartFile file, @RequestParam ObjectType type,
@RequestParam Long objectId, @AuthenticationPrincipal AuthUser authUser) {
log.debug("upload file {} to folder {}", file.getOriginalFilename(), type.toString().toLowerCase());
@@ -41,6 +46,10 @@ public ResponseEntity upload(@RequestPart MultipartFile file, @Reque
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Видалити файл вкладення",
+ description = "Видаляє вкладення з бази даних та фізично з файлової системи за ID"
+ )
public void delete(@PathVariable Long id) {
log.debug("delete file id = {}", id);
Attachment attachment = repository.getExisted(id);
@@ -49,6 +58,10 @@ public void delete(@PathVariable Long id) {
}
@GetMapping(value = "/download/{id}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+ @Operation(
+ summary = "Завантажити файл вкладення",
+ description = "Повертає файл вкладення як ресурс для завантаження за його ID"
+ )
public ResponseEntity download(@PathVariable long id) {
log.debug("download file id = {}", id);
Attachment attachment = repository.getExisted(id);
@@ -59,12 +72,20 @@ public ResponseEntity download(@PathVariable long id) {
}
@GetMapping("/for-object")
+ @Operation(
+ summary = "Отримати всі вкладення для об'єкта",
+ description = "Повертає список усіх вкладень, прикріплених до вказаного об'єкта певного типу (за objectId та типом)"
+ )
public List getAllForObject(@RequestParam long objectId, @RequestParam ObjectType type) {
log.info("get all attachment by objectId = {}", objectId);
return repository.getAllForObject(objectId, type);
}
@GetMapping
+ @Operation(
+ summary = "Отримати всі вкладення",
+ description = "Повертає повний список всіх вкладень, збережених у системі"
+ )
public List getAll() {
log.info("get all attachment");
return repository.findAll();
diff --git a/src/main/java/com/javarush/jira/bugtracking/project/ProjectController.java b/src/main/java/com/javarush/jira/bugtracking/project/ProjectController.java
index 5ebd02ee3..4dc4cf1d6 100644
--- a/src/main/java/com/javarush/jira/bugtracking/project/ProjectController.java
+++ b/src/main/java/com/javarush/jira/bugtracking/project/ProjectController.java
@@ -2,6 +2,7 @@
import com.javarush.jira.bugtracking.Handlers;
import com.javarush.jira.bugtracking.project.to.ProjectTo;
+import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@@ -22,16 +23,29 @@ public class ProjectController {
private final Handlers.ProjectHandler handler;
@GetMapping("/projects")
+ @Operation(
+ summary = "Отримати список усіх проєктів",
+ description = "Повертає список усіх проєктів у порядку від найновіших до найстаріших"
+ )
+
public List getAll() {
return handler.getAllTos(ProjectRepository.NEWEST_FIRST);
}
@GetMapping("/projects/{id}")
+ @Operation(
+ summary = "Отримати проєкт за ID",
+ description = "Повертає повну інформацію про проєкт із зазначеним ID"
+ )
public ProjectTo getById(@PathVariable Long id) {
return handler.getTo(id);
}
@PostMapping(path = "/mngr/projects", consumes = MediaType.APPLICATION_JSON_VALUE)
+ @Operation(
+ summary = "Створити новий проєкт",
+ description = "Створює новий проєкт на основі переданих даних"
+ )
public ResponseEntity create(@Valid @RequestBody ProjectTo projectTo) {
Project created = handler.createWithBelong(projectTo, PROJECT, "project_author");
return createdResponse(REST_URL + "/projects", created);
@@ -39,12 +53,20 @@ public ResponseEntity create(@Valid @RequestBody ProjectTo projectTo) {
@PutMapping("/mngr/projects/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Оновити проєкт",
+ description = "Оновлює існуючий проєкт за його ID"
+ )
public void update(@Valid @RequestBody ProjectTo projectTo, @PathVariable Long id) {
handler.updateFromTo(projectTo, id);
}
@PatchMapping("/mngr/projects/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Увімкнути/вимкнути проєкт",
+ description = "Змінює статус активності проєкту: enabled = true/false"
+ )
public void enable(@PathVariable long id, @RequestParam boolean enabled) {
handler.enable(id, enabled);
}
diff --git a/src/main/java/com/javarush/jira/bugtracking/report/ReportRestController.java b/src/main/java/com/javarush/jira/bugtracking/report/ReportRestController.java
index a2b221d95..40cfa9c62 100644
--- a/src/main/java/com/javarush/jira/bugtracking/report/ReportRestController.java
+++ b/src/main/java/com/javarush/jira/bugtracking/report/ReportRestController.java
@@ -1,5 +1,6 @@
package com.javarush.jira.bugtracking.report;
+import io.swagger.v3.oas.annotations.Operation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
@@ -20,6 +21,10 @@ public class ReportRestController {
private final ReportRepository reportRepository;
@GetMapping
+ @Operation(
+ summary = "Отримати звіт по задачах спринту",
+ description = "Повертає список зведеної інформації про задачі у межах вказаного спринту, включаючи кількість, статуси та інші ключові метрики"
+ )
public List getTaskSummaries(@PathVariable long sprintId) {
log.info("get task summaries for sprint with id={}", sprintId);
return reportRepository.getTaskSummaries(sprintId);
diff --git a/src/main/java/com/javarush/jira/bugtracking/sprint/SprintController.java b/src/main/java/com/javarush/jira/bugtracking/sprint/SprintController.java
index 67611735f..c55ca2f8b 100644
--- a/src/main/java/com/javarush/jira/bugtracking/sprint/SprintController.java
+++ b/src/main/java/com/javarush/jira/bugtracking/sprint/SprintController.java
@@ -3,6 +3,7 @@
import com.javarush.jira.bugtracking.Handlers;
import com.javarush.jira.bugtracking.project.ProjectRepository;
import com.javarush.jira.bugtracking.sprint.to.SprintTo;
+import io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.Nullable;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
@@ -32,11 +33,19 @@ public class SprintController {
private final Handlers.SprintHandler handler;
@GetMapping("/sprints/{id}")
+ @Operation(
+ summary = "Отримати спринт за ID",
+ description = "Повертає повну інформацію про спринт із зазначеним ID"
+ )
public SprintTo get(@PathVariable long id) {
return handler.getTo(id);
}
@GetMapping("/sprints/by-project")
+ @Operation(
+ summary = "Отримати спринти за проєктом",
+ description = "Повертає список усіх спринтів для проєкту. Якщо enabled=true, то тільки активні"
+ )
public List getAllByProject(@RequestParam long projectId, @RequestParam @Nullable Boolean enabled) {
log.info("get all for project {} and enabled {}", projectId, enabled);
checkProjectExists(projectId);
@@ -52,6 +61,10 @@ private void checkProjectExists(long id) {
}
@GetMapping("/sprints/by-project-and-status")
+ @Operation(
+ summary = "Отримати спринти за статусом",
+ description = "Повертає спринти проєкту з певним статусом (наприклад: active, done)"
+ )
public List getAllByProjectAndStatus(@RequestParam long projectId, @NotBlank @RequestParam String statusCode) {
log.info("get all {} sprints for project with id={}", statusCode, projectId);
checkProjectExists(projectId);
@@ -60,6 +73,10 @@ public List getAllByProjectAndStatus(@RequestParam long projectId, @NotB
@PostMapping(path = "/mngr/sprints", consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.CREATED)
+ @Operation(
+ summary = "Створити спринт",
+ description = "Створює новий спринт у вказаному проєкті"
+ )
public ResponseEntity createWithLocation(@Valid @RequestBody SprintTo sprintTo) {
Sprint created = handler.createWithBelong(sprintTo, SPRINT, "sprint_author");
return createdResponse(REST_URL + "/sprints", created);
@@ -67,6 +84,10 @@ public ResponseEntity createWithLocation(@Valid @RequestBody SprintTo sp
@PutMapping(path = "/mngr/sprints/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Оновити спринт",
+ description = "Оновлює існуючий спринт за його ID"
+ )
public void update(@Validated @RequestBody SprintTo sprintTo, @PathVariable long id) {
handler.updateFromTo(sprintTo, id);
}
@@ -74,6 +95,10 @@ public void update(@Validated @RequestBody SprintTo sprintTo, @PathVariable long
@Transactional
@PatchMapping("/mngr/sprints/{id}/change-status")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Змінити статус спринта",
+ description = "Змінює статус спринта (наприклад, з active на done)"
+ )
public void changeStatusCode(@PathVariable long id, @RequestParam String statusCode) {
log.info("change statusCode of sprint {}", id);
Sprint sprint = handler.getRepository().getExisted(id);
@@ -82,6 +107,10 @@ public void changeStatusCode(@PathVariable long id, @RequestParam String statusC
@PatchMapping("/mngr/sprints/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Активувати/деактивувати спринт",
+ description = "Вмикає або вимикає спринт (enabled = true/false)"
+ )
public void enable(@PathVariable long id, @RequestParam boolean enabled) {
handler.enable(id, enabled);
}
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..ce248628f 100644
--- a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java
+++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java
@@ -10,6 +10,7 @@
import com.javarush.jira.bugtracking.tree.ITreeNode;
import com.javarush.jira.common.util.Util;
import com.javarush.jira.login.AuthUser;
+import io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.Nullable;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
@@ -42,12 +43,20 @@ public class TaskController {
@GetMapping("/{id}")
+ @Operation(
+ summary = "Отримати задачу за ID",
+ description = "Повертає повну інформацію про задачу з вказаним ID"
+ )
public TaskToFull get(@PathVariable long id) {
log.info("get task by id={}", id);
return taskService.get(id);
}
@GetMapping("/by-sprint")
+ @Operation(
+ summary = "Отримати задачі по спринту",
+ description = "Повертає всі задачі, що належать до вказаного спринту"
+ )
public List getAllBySprint(@RequestParam long sprintId) {
log.info("get all for sprint {}", sprintId);
return sortTasksAsTree(handler.getMapper().toToList(handler.getRepository().findAllBySprintId(sprintId)));
@@ -70,6 +79,10 @@ private List sortTasksAsTree(List tasks) {
}
@GetMapping("/by-project")
+ @Operation(
+ summary = "Отримати задачі по проєкту",
+ description = "Повертає всі задачі, що належать до вказаного проєкту"
+ )
public List getAllByProject(@RequestParam long projectId) {
log.info("get all for project {}", projectId);
return handler.getMapper().toToList(handler.getRepository().findAllByProjectId(projectId));
@@ -77,24 +90,40 @@ public List getAllByProject(@RequestParam long projectId) {
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.CREATED)
+ @Operation(
+ summary = "Створити нову задачу",
+ description = "Створює нову задачу на основі переданих даних"
+ )
public ResponseEntity createWithLocation(@Valid @RequestBody TaskToExt taskTo) {
return createdResponse(REST_URL, taskService.create(taskTo));
}
@PutMapping(path = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Оновити задачу",
+ description = "Оновлює існуючу задачу за її ID"
+ )
public void update(@Valid @RequestBody TaskToExt taskTo, @PathVariable long id) {
taskService.update(taskTo, id);
}
@PatchMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Активувати/деактивувати задачу",
+ description = "Встановлює статус 'enabled' для задачі"
+ )
public void enable(@PathVariable long id, @RequestParam boolean enabled) {
handler.enable(id, enabled);
}
@PatchMapping("/{id}/change-status")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Змінити статус задачі",
+ description = "Змінює статус задачі за допомогою коду статусу"
+ )
public void changeTaskStatus(@PathVariable long id, @NotBlank @RequestParam String statusCode) {
log.info("change task(id={}) status to {}", id, statusCode);
taskService.changeStatus(id, statusCode);
@@ -102,12 +131,20 @@ public void changeTaskStatus(@PathVariable long id, @NotBlank @RequestParam Stri
@PatchMapping("/{id}/change-sprint")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Змінити спринт задачі",
+ description = "Прив'язує або відв'язує задачу від конкретного спринту"
+ )
public void changeTaskSprint(@PathVariable long id, @Nullable @RequestParam Long sprintId) {
log.info("change task(id={}) sprint to {}", id, sprintId);
taskService.changeSprint(id, sprintId);
}
@GetMapping("/assignments/by-sprint")
+ @Operation(
+ summary = "Отримати призначення задач по спринту",
+ description = "Повертає список користувацьких призначень для задач у заданому спринті"
+ )
public List getTaskAssignmentsBySprint(@RequestParam long sprintId) {
log.info("get task assignments for user {} for sprint {}", AuthUser.authId(), sprintId);
return userBelongRepository.findActiveTaskAssignmentsForUserBySprint(AuthUser.authId(), sprintId);
@@ -115,6 +152,10 @@ public List getTaskAssignmentsBySprint(@RequestParam long sprintId)
@PatchMapping("/{id}/assign")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Призначити користувача до задачі",
+ description = "Призначає поточного користувача до задачі з вказаним типом ролі"
+ )
public void assign(@PathVariable long id, @NotBlank @RequestParam String userType) {
log.info("assign user {} as {} to task {}", AuthUser.authId(), userType, id);
taskService.assign(id, userType, AuthUser.authId());
@@ -122,12 +163,20 @@ public void assign(@PathVariable long id, @NotBlank @RequestParam String userTyp
@PatchMapping("/{id}/unassign")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Видалити призначення користувача",
+ description = "Видаляє призначення користувача з задачі"
+ )
public void unAssign(@PathVariable long id, @NotBlank @RequestParam String userType) {
log.info("unassign user {} as {} from task {}", AuthUser.authId(), userType, id);
taskService.unAssign(id, userType, AuthUser.authId());
}
@GetMapping("/{id}/comments")
+ @Operation(
+ summary = "Отримати коментарі до задачі",
+ description = "Повертає список коментарів, пов'язаних із задачою"
+ )
public List getComments(@PathVariable long id) {
log.info("get comments for task with id={}", id);
return activityHandler.getMapper().toToList(activityHandler.getRepository().findAllComments(id));
@@ -135,18 +184,30 @@ public List getComments(@PathVariable long id) {
@PostMapping(value = "/activities", consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.CREATED)
+ @Operation(
+ summary = "Створити активність",
+ description = "Додає нову активність або коментар до задачі"
+ )
public Activity create(@Valid @RequestBody ActivityTo activityTo) {
return activityService.create(activityTo);
}
@PutMapping(path = "/activities/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Оновити активність",
+ description = "Оновлює існуючу активність (наприклад, коментар)"
+ )
public void update(@Valid @RequestBody ActivityTo activityTo, @PathVariable long id) {
activityService.update(activityTo, id);
}
@DeleteMapping("/activities/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Видалити активність",
+ description = "Видаляє активність або коментар за ID"
+ )
public void delete(@PathVariable long id) {
activityService.delete(id);
}
diff --git a/src/main/java/com/javarush/jira/bugtracking/tree/TreeRestController.java b/src/main/java/com/javarush/jira/bugtracking/tree/TreeRestController.java
index a3a7da4ec..ea4ba98cf 100644
--- a/src/main/java/com/javarush/jira/bugtracking/tree/TreeRestController.java
+++ b/src/main/java/com/javarush/jira/bugtracking/tree/TreeRestController.java
@@ -9,6 +9,7 @@
import com.javarush.jira.bugtracking.task.TaskRepository;
import com.javarush.jira.bugtracking.task.mapper.TaskMapper;
import com.javarush.jira.common.util.Util;
+import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
@@ -44,12 +45,20 @@ private static List toTree(List list, Function mappe
}
@GetMapping("/projects")
+ @Operation(
+ summary = "Отримати дерево проєктів",
+ description = "Повертає всі проєкти у вигляді ієрархічного дерева NodeTo"
+ )
public List getProjects() {
log.info("get projects tree");
return toTree(projectMapper.toToList(projectRepository.getAll()), mapper::fromProject);
}
@GetMapping("/projects/{projectId}/sprints")
+ @Operation(
+ summary = "Отримати спринти проєкту та беклог",
+ description = "Повертає список спринтів проєкту у вигляді дерева, включаючи окремий вузол 'Backlog'"
+ )
public List getSprintsAndBacklog(@PathVariable long projectId) {
log.info("get project {} sprints", projectId);
List sprintTos = new ArrayList<>(sprintMapper.toToList(sprintRepository.getAllByProject(projectId)));
@@ -58,12 +67,20 @@ public List getSprintsAndBacklog(@PathVariable long projectId) {
}
@GetMapping("/sprints/{sprintId}/tasks")
+ @Operation(
+ summary = "Отримати задачі для спринту",
+ description = "Повертає задачі, прикріплені до певного спринту, у вигляді дерева"
+ )
public List getSprintTasks(@PathVariable long sprintId) {
log.info("get sprint {} tasks", sprintId);
return toTree(taskMapper.toToList(taskRepository.findAllBySprintId(sprintId)), mapper::fromTask);
}
@GetMapping("/projects/{projectId}/backlog/tasks")
+ @Operation(
+ summary = "Отримати задачі беклогу проєкту",
+ description = "Повертає задачі проєкту, які ще не додані до жодного спринту, у вигляді дерева"
+ )
public List getBacklogTasks(@PathVariable long projectId) {
log.info("get project {} backlog tasks", projectId);
return toTree(taskMapper.toToList(taskRepository.findAllByProjectIdAndSprintIsNull(projectId)), mapper::fromTask);
diff --git a/src/main/java/com/javarush/jira/login/internal/web/AdminUserController.java b/src/main/java/com/javarush/jira/login/internal/web/AdminUserController.java
index c29c17f7b..31b4580e1 100644
--- a/src/main/java/com/javarush/jira/login/internal/web/AdminUserController.java
+++ b/src/main/java/com/javarush/jira/login/internal/web/AdminUserController.java
@@ -5,6 +5,7 @@
import com.javarush.jira.login.User;
import com.javarush.jira.login.UserTo;
import io.swagger.v3.oas.annotations.Hidden;
+import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.constraints.Size;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.data.domain.Sort;
@@ -25,23 +26,39 @@ public class AdminUserController extends AbstractUserController {
static final String REST_URL = "/api/admin/users";
@GetMapping("/{id}")
+ @Operation(
+ summary = "Отримати користувача за ID",
+ description = "Повертає об'єкт користувача за вказаним ID"
+ )
public User get(@PathVariable long id) {
return handler.get(id);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Видалити користувача",
+ description = "Видаляє користувача з бази даних за ID"
+ )
// getByEmail with return old user until expired
public void delete(@PathVariable long id) {
handler.delete(id);
}
@GetMapping
+ @Operation(
+ summary = "Отримати всіх користувачів",
+ description = "Повертає список усіх користувачів, відсортований за email"
+ )
public List getAll() {
return handler.getAll(Sort.by(Sort.Direction.ASC, "email"));
}
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ @Operation(
+ summary = "Створити нового користувача",
+ description = "Створює нового користувача на основі переданих даних"
+ )
public ResponseEntity createWithLocation(@Validated(View.OnCreate.class) @RequestBody User user) {
User created = handler.create(user);
return createdResponse(REST_URL, created);
@@ -50,6 +67,10 @@ public ResponseEntity createWithLocation(@Validated(View.OnCreate.class) @
@PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.NO_CONTENT)
@CacheEvict(value = "users", key = "#user.email")
+ @Operation(
+ summary = "Оновити дані користувача",
+ description = "Оновлює дані користувача за його ID"
+ )
// In case of update email, getByEmail with old email return old user until expired
@JsonView(View.OnUpdate.class)
public void update(@Validated(View.OnUpdate.class) @RequestBody User user, @PathVariable long id) {
@@ -57,12 +78,20 @@ public void update(@Validated(View.OnUpdate.class) @RequestBody User user, @Path
}
@GetMapping("/by-email")
+ @Operation(
+ summary = "Отримати користувача за email",
+ description = "Повертає користувача, знайденого за адресою електронної пошти"
+ )
public User getByEmail(@RequestParam String email) {
return handler.getRepository().getExistedByEmail(email);
}
@PatchMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Активувати/деактивувати користувача",
+ description = "Змінює статус активності користувача"
+ )
// getByEmail with return old user until expired
public void enable(@PathVariable long id, @RequestParam boolean enabled) {
handler.enable(id, enabled);
@@ -70,6 +99,10 @@ public void enable(@PathVariable long id, @RequestParam boolean enabled) {
@PostMapping("/{id}/change_password")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Змінити пароль користувача",
+ description = "Оновлює пароль користувача, якщо вказано правильний поточний"
+ )
public void changePassword(@RequestParam String oldPassword, @Size(min = 5, max = 128) @RequestParam String newPassword, @PathVariable long id) {
changePassword0(oldPassword, newPassword, id);
}
diff --git a/src/main/java/com/javarush/jira/login/internal/web/UserController.java b/src/main/java/com/javarush/jira/login/internal/web/UserController.java
index 29cb0e1ce..8e191bb8a 100644
--- a/src/main/java/com/javarush/jira/login/internal/web/UserController.java
+++ b/src/main/java/com/javarush/jira/login/internal/web/UserController.java
@@ -5,6 +5,7 @@
import com.javarush.jira.login.AuthUser;
import com.javarush.jira.login.User;
import com.javarush.jira.login.UserTo;
+import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.constraints.Size;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
@@ -26,6 +27,10 @@ public class UserController extends AbstractUserController {
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.CREATED)
+ @Operation(
+ summary = "Створити нового користувача",
+ description = "Створює нового користувача на основі даних з тіла запиту"
+ )
public ResponseEntity createWithLocation(@Validated(View.OnCreate.class) @RequestBody UserTo userTo) {
User created = handler.createFromTo(userTo);
return createdResponse(REST_URL, created);
@@ -34,11 +39,19 @@ public ResponseEntity createWithLocation(@Validated(View.OnCreate.class) @
@PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.NO_CONTENT)
@JsonView(View.OnUpdate.class)
+ @Operation(
+ summary = "Оновити користувача",
+ description = "Оновлює дані поточного авторизованого користувача"
+ )
public void update(@Validated(View.OnUpdate.class) @RequestBody UserTo userTo, @AuthenticationPrincipal AuthUser authUser) {
authUser.setUser(handler.updateFromTo(userTo, authUser.id()));
}
@GetMapping
+ @Operation(
+ summary = "Отримати користувача",
+ description = "Повертає дані поточного авторизованого користувача"
+ )
public User get(@AuthenticationPrincipal AuthUser authUser) {
return handler.get(authUser.id());
}
@@ -46,6 +59,10 @@ public User get(@AuthenticationPrincipal AuthUser authUser) {
@DeleteMapping
@ResponseStatus(HttpStatus.NO_CONTENT)
@CacheEvict(key = "#authUser.user.email")
+ @Operation(
+ summary = "Видалити користувача",
+ description = "Видаляє поточного авторизованого користувача"
+ )
public void delete(@AuthenticationPrincipal AuthUser authUser) {
handler.delete(authUser.id());
}
@@ -53,6 +70,10 @@ public void delete(@AuthenticationPrincipal AuthUser authUser) {
@PostMapping("/change_password")
@ResponseStatus(HttpStatus.NO_CONTENT)
@CacheEvict(key = "#authUser.user.email")
+ @Operation(
+ summary = "Змінити пароль",
+ description = "Змінює пароль для поточного авторизованого користувача"
+ )
public void changePassword(@RequestParam String oldPassword, @Size(min = 5, max = 128) @RequestParam String newPassword, @AuthenticationPrincipal AuthUser authUser) {
changePassword0(oldPassword, newPassword, authUser.id());
}
diff --git a/src/main/java/com/javarush/jira/profile/internal/web/ProfileRestController.java b/src/main/java/com/javarush/jira/profile/internal/web/ProfileRestController.java
index ba0fcfcb5..c1b6a18a0 100644
--- a/src/main/java/com/javarush/jira/profile/internal/web/ProfileRestController.java
+++ b/src/main/java/com/javarush/jira/profile/internal/web/ProfileRestController.java
@@ -2,6 +2,7 @@
import com.javarush.jira.login.AuthUser;
import com.javarush.jira.profile.ProfileTo;
+import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -14,12 +15,20 @@ public class ProfileRestController extends AbstractProfileController {
public static final String REST_URL = "/api/profile";
@GetMapping
+ @Operation(
+ summary = "Отримати профіль користувача",
+ description = "Повертає дані профілю поточного автентифікованого користувача"
+ )
public ProfileTo get(@AuthenticationPrincipal AuthUser authUser) {
return super.get(authUser.id());
}
@PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Оновити профіль користувача",
+ description = "Оновлює профіль поточного автентифікованого користувача"
+ )
public void update(@Valid @RequestBody ProfileTo profileTo, @AuthenticationPrincipal AuthUser authUser) {
super.update(profileTo, authUser.id());
}
diff --git a/src/main/java/com/javarush/jira/ref/internal/web/ReferenceController.java b/src/main/java/com/javarush/jira/ref/internal/web/ReferenceController.java
index 487f8a24b..97ba82276 100644
--- a/src/main/java/com/javarush/jira/ref/internal/web/ReferenceController.java
+++ b/src/main/java/com/javarush/jira/ref/internal/web/ReferenceController.java
@@ -6,6 +6,7 @@
import com.javarush.jira.ref.internal.Reference;
import com.javarush.jira.ref.internal.ReferenceMapper;
import com.javarush.jira.ref.internal.ReferenceRepository;
+import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -32,17 +33,29 @@ public class ReferenceController {
private ReferenceRepository repository;
@GetMapping("/{type}")
+ @Operation(
+ summary = "Отримати всі значення довідника за типом",
+ description = "Повертає мапу всіх значень довідника певного типу у форматі RefTo"
+ )
public Map getRefsByType(@PathVariable RefType type) {
return ReferenceService.getRefs(type);
}
@GetMapping("/{type}/{code}")
+ @Operation(
+ summary = "Отримати значення довідника за типом і кодом",
+ description = "Повертає об'єкт RefTo, який відповідає вказаному типу довідника і коду"
+ )
public RefTo getRefByTypeByCode(@PathVariable RefType type, @PathVariable String code) {
return ReferenceService.getRefTo(type, code);
}
@DeleteMapping("/{type}/{code}")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @Operation(
+ summary = "Видалити значення довідника",
+ description = "Видаляє елемент довідника за типом і кодом та оновлює кеш"
+ )
public void delete(@PathVariable RefType type, @PathVariable String code) {
log.debug("delete with type {}, code {}", type, code);
RefTo ref = ReferenceService.getRefTo(type, code);
@@ -51,6 +64,10 @@ public void delete(@PathVariable RefType type, @PathVariable String code) {
}
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ @Operation(
+ summary = "Створити нове значення довідника",
+ description = "Створює нове значення довідника на основі переданих даних RefTo"
+ )
public ResponseEntity create(@Valid @RequestBody RefTo refTo) {
log.debug("create {}", refTo);
checkNew(refTo);
@@ -64,6 +81,10 @@ public ResponseEntity create(@Valid @RequestBody RefTo refTo) {
@PutMapping("/{type}/{code}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Transactional
+ @Operation(
+ summary = "Оновити назву значення довідника",
+ description = "Оновлює поле title для обраного елемента довідника за типом і кодом"
+ )
public void updateTitle(@PathVariable RefType type, @PathVariable String code, @RequestParam String title) {
log.debug("update Ref with type={}, code={} with title={}", title, code, title);
Reference ref = getExisted(type, code);
@@ -75,6 +96,10 @@ public void updateTitle(@PathVariable RefType type, @PathVariable String code, @
@PatchMapping("/{type}/{code}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Transactional
+ @Operation(
+ summary = "Активувати/деактивувати значення довідника",
+ description = "Змінює статус enabled (активний/неактивний) для елемента довідника"
+ )
public void enable(@PathVariable RefType type, @PathVariable String code, @RequestParam boolean enabled) {
log.debug("enable Ref with type={}, code={} with enabled={}", type, code, enabled);
Reference ref = getExisted(type, code);
From 8e48cd3e33afdce635ca6f70bdce302f4360c9cd Mon Sep 17 00:00:00 2001
From: Yaroslav <6i4bbb@gmail.com>
Date: Wed, 30 Jul 2025 15:16:58 +0300
Subject: [PATCH 6/7] add integration tests for ProfileRestController
---
.../web/ProfileRestControllerTest.java | 69 ++++++++++++++++++-
1 file changed, 68 insertions(+), 1 deletion(-)
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..36e40095d 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,75 @@
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_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 {
-}
\ No newline at end of file
+ @Autowired
+ private ProfileRepository repository;
+
+ @Test
+ @WithUserDetails(USER_MAIL)
+ void getProfile() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.mailNotifications.length()").value(USER_PROFILE_TO.getMailNotifications().size()))
+ .andExpect(jsonPath("$.contacts.length()").value(USER_PROFILE_TO.getContacts().size()));
+ }
+
+ @Test
+ void getProfileUnauthorized() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ @WithUserDetails(USER_MAIL)
+ void updateProfile() throws Exception {
+ ProfileTo updatedTo = getUpdatedTo();
+
+ perform(MockMvcRequestBuilders.put(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(writeValue(updatedTo)))
+ .andDo(print())
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ @WithUserDetails(USER_MAIL)
+ void updateInvalidProfile() throws Exception {
+ ProfileTo invalid = getInvalidTo();
+
+ perform(MockMvcRequestBuilders.put(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(writeValue(invalid)))
+ .andDo(print())
+ .andExpect(status().isUnprocessableEntity());
+ }
+
+ @Test
+ void updateProfileUnauthorized() throws Exception {
+ ProfileTo profile = getNewTo();
+
+ perform(MockMvcRequestBuilders.put(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(writeValue(profile)))
+ .andDo(print())
+ .andExpect(status().isUnauthorized());
+ }
+}
From 0b4b362b205a1452ad00ff1d7af037505ebce945 Mon Sep 17 00:00:00 2001
From: Yaroslav <6i4bbb@gmail.com>
Date: Wed, 30 Jul 2025 15:39:05 +0300
Subject: [PATCH 7/7] modernize FileUtil.upload using java.nio.file API
---
.../jira/bugtracking/attachment/FileUtil.java | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
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..dd9f42664 100644
--- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java
+++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java
@@ -25,14 +25,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 dirPath = Paths.get(directoryPath);
+ Files.createDirectories(dirPath);
+
+ Path filePath = dirPath.resolve(fileName);
+ Files.write(filePath, multipartFile.getBytes());
+
+ } catch (IOException ex) {
+ throw new IllegalRequestDataException("Failed to upload file " + multipartFile.getOriginalFilename());
}
}