From 96019e59f1433a235b9ce929b0499fe27ed8f76a Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 5 Sep 2025 23:09:34 +0300 Subject: [PATCH 1/2] Task --- .gitignore | 2 + Dockerfile | 9 + README.md | 9 +- pom.xml | 36 +- resources/mails/email-confirmation.html | 18 +- resources/mails/password-reset.html | 16 +- resources/view/index.html | 15 +- resources/view/login.html | 16 +- .../javarush/jira/JiraRushApplication.java | 1 - .../javarush/jira/bugtracking/Handlers.java | 19 +- .../jira/bugtracking/attachment/FileUtil.java | 30 +- .../bugtracking/task/ActivityService.java | 54 +++ .../jira/bugtracking/task/TaskController.java | 24 +- .../jira/bugtracking/task/TaskService.java | 64 +++- .../bugtracking/task/TaskUIController.java | 20 ++ .../task/mapper/TaskTagsMapper.java | 14 + .../jira/bugtracking/task/to/TaskToTags.java | 20 ++ .../common/internal/config/MvcConfig.java | 37 +- src/main/resources/application.yaml | 76 ++-- src/main/resources/data4dev/data.sql | 23 +- src/main/resources/db/changelog.sql | 47 ++- src/main/resources/messages.properties | 15 + src/main/resources/messages_ru.properties | 15 + src/main/resources/messages_uk.properties | 15 + .../javarush/jira/AbstractControllerTest.java | 2 +- .../web/ProfileRestControllerTest.java | 118 +++++++ .../profile/internal/web/ProfileTestData.java | 13 +- src/test/resources/application-test.yaml | 20 +- src/test/resources/changelog-test.sql | 326 ++++++++++++++++++ .../resources/{data.sql => data-test.sql} | 12 +- src/test/resources/secrets-test.properties | 3 + 31 files changed, 947 insertions(+), 142 deletions(-) create mode 100644 Dockerfile create mode 100644 src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskTagsMapper.java create mode 100644 src/main/java/com/javarush/jira/bugtracking/task/to/TaskToTags.java create mode 100644 src/main/resources/messages.properties create mode 100644 src/main/resources/messages_ru.properties create mode 100644 src/main/resources/messages_uk.properties create mode 100644 src/test/resources/changelog-test.sql rename src/test/resources/{data.sql => data-test.sql} (92%) create mode 100644 src/test/resources/secrets-test.properties diff --git a/.gitignore b/.gitignore index cd38e2e7b..2cb76c3be 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,7 @@ target logs attachments *.patch +secrets.properties +secrets-test.properties diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..3e39b9c47 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM eclipse-temurin:17-jdk-alpine + +WORKDIR /app + +COPY target/jira-1.0.jar app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/README.md b/README.md index 719b268f5..8cfaf79db 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,11 @@ - https://habr.com/ru/articles/259055/ Список выполненных задач: -... \ No newline at end of file +- Видалити соціальні мережі: vk, yandex; +- Винести чутливу інформацію до окремого проперті файлу; +- Написати тести для всіх публічних методів контролера ProfileRestController; +- Зробити рефакторинг методу com.javarush.jira.bugtracking.attachment.FileUtil#upload, щоб він використовував сучасний підхід для роботи з файловою системою; +- Додати новий функціонал: додавання тегів до завдання (REST API + реалізація на сервісі); +- Додати підрахунок часу: скільки завдання перебувало у роботі та тестуванні; +- Написати Dockerfile для основного сервера; +- Додати локалізацію мінімум двома мовами для шаблонів листів (mails) та стартовою сторінки index.html. \ No newline at end of file diff --git a/pom.xml b/pom.xml index f6c152c68..3631bf6ea 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,7 @@ 17 2.0.2 1.5.3.Final + 0.2.0 UTF-8 UTF-8 @@ -88,6 +89,13 @@ runtime + + com.h2database + h2 + 2.3.232 + test + + org.liquibase liquibase-core @@ -112,7 +120,7 @@ org.projectlombok lombok-mapstruct-binding - 0.2.0 + ${lombok-mapstruct-binding.version} org.mapstruct @@ -166,6 +174,7 @@ + org.apache.maven.plugins @@ -174,6 +183,31 @@ -Dfile.encoding=UTF-8 + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + 1.18.24 + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + + + + diff --git a/resources/mails/email-confirmation.html b/resources/mails/email-confirmation.html index 106e6129a..e2a16674f 100644 --- a/resources/mails/email-confirmation.html +++ b/resources/mails/email-confirmation.html @@ -1,13 +1,19 @@ - + - JiraRush - подтверждение почты + JiraRush - подтверждение почты -

-

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

+

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

-Подтвердить почту + Подтвердить почту + - \ No newline at end of file + diff --git a/resources/mails/password-reset.html b/resources/mails/password-reset.html index b37a49007..8d6b756fb 100644 --- a/resources/mails/password-reset.html +++ b/resources/mails/password-reset.html @@ -1,12 +1,18 @@ - + - JiraRush - установить новый пароль + JiraRush - установить новый пароль -

-

-Установить пароль +

+

+Установить пароль + \ No newline at end of file diff --git a/resources/view/index.html b/resources/view/index.html index e8656ef96..8a3deda9b 100644 --- a/resources/view/index.html +++ b/resources/view/index.html @@ -1,14 +1,21 @@ - + - + -

JiraRush Home page

+

JiraRush Home page

+
- +
+
diff --git a/resources/view/login.html b/resources/view/login.html index 8765ca8ff..58916bc53 100644 --- a/resources/view/login.html +++ b/resources/view/login.html @@ -48,14 +48,14 @@

Sign in

type="button"> - - - - - - + + + + + + + + diff --git a/src/main/java/com/javarush/jira/JiraRushApplication.java b/src/main/java/com/javarush/jira/JiraRushApplication.java index 58be49fe7..96174f090 100644 --- a/src/main/java/com/javarush/jira/JiraRushApplication.java +++ b/src/main/java/com/javarush/jira/JiraRushApplication.java @@ -10,7 +10,6 @@ @EnableConfigurationProperties(AppProperties.class) @EnableCaching public class JiraRushApplication { - public static void main(String[] args) { SpringApplication.run(JiraRushApplication.class, args); } diff --git a/src/main/java/com/javarush/jira/bugtracking/Handlers.java b/src/main/java/com/javarush/jira/bugtracking/Handlers.java index 3c0d420e2..0003a1eeb 100644 --- a/src/main/java/com/javarush/jira/bugtracking/Handlers.java +++ b/src/main/java/com/javarush/jira/bugtracking/Handlers.java @@ -16,14 +16,8 @@ import com.javarush.jira.bugtracking.task.ActivityRepository; import com.javarush.jira.bugtracking.task.Task; import com.javarush.jira.bugtracking.task.TaskRepository; -import com.javarush.jira.bugtracking.task.mapper.ActivityMapper; -import com.javarush.jira.bugtracking.task.mapper.TaskExtMapper; -import com.javarush.jira.bugtracking.task.mapper.TaskFullMapper; -import com.javarush.jira.bugtracking.task.mapper.TaskMapper; -import com.javarush.jira.bugtracking.task.to.ActivityTo; -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.mapper.*; +import com.javarush.jira.bugtracking.task.to.*; import com.javarush.jira.common.BaseHandler; import com.javarush.jira.common.BaseMapper; import com.javarush.jira.common.BaseRepository; @@ -80,6 +74,13 @@ public TaskExtHandler(TaskRepository repository, TaskExtMapper mapper) { } } + @Component + public static class TaskTagsHandler extends UserBelongHandler { + public TaskTagsHandler(TaskRepository repository, TaskTagsMapper mapper) { + super(repository, mapper); + } + } + @Component public static class ActivityHandler extends BaseHandler { public ActivityHandler(ActivityRepository repository, ActivityMapper mapper) { @@ -126,4 +127,4 @@ public void createUserBelong(long id, ObjectType type, long userId, String userT } } } -} +} \ No newline at end of file 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..d1a206485 100644 --- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java +++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java @@ -7,14 +7,12 @@ 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.io.*; import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; @UtilityClass public class FileUtil { @@ -25,14 +23,24 @@ 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()); + if (directoryPath.isBlank()) { + throw new IllegalRequestDataException("Directory path is empty"); + } + + if (fileName.isBlank()) { + throw new IllegalRequestDataException("File name is empty"); + } + + try (InputStream in = multipartFile.getInputStream()) { + Path dir = Files.createDirectories(Paths.get(directoryPath)); + + Path file = dir.resolve(fileName); + if (!Files.exists(file)) { + Files.createFile(file); } + Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException ex) { + throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename()); } } diff --git a/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java b/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java index 7938541bb..8d00466af 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java @@ -3,11 +3,14 @@ import com.javarush.jira.bugtracking.Handlers; import com.javarush.jira.bugtracking.task.to.ActivityTo; import com.javarush.jira.common.error.DataConflictException; +import com.javarush.jira.common.error.NotFoundException; import com.javarush.jira.login.AuthUser; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.List; import static com.javarush.jira.bugtracking.task.TaskUtil.getLatestValue; @@ -73,4 +76,55 @@ private void updateTaskIfRequired(long taskId, String activityStatus, String act } } } + + public Duration getTimeInWork(long taskId) { + Task task = taskRepository.getExisted(taskId); + List activities = handler.getRepository().findAllByTaskIdOrderByUpdatedDesc(task.id()); + + LocalDateTime timeInProgress = getTimeTaskByStatus("in_progress", activities); + if (timeInProgress == null) { + throw new NotFoundException( + "Task with id=" + task.id() + " does not have status 'in_progress'" + ); + } + + LocalDateTime timeReadyForReview = getTimeTaskByStatus("ready_for_review", activities); + if (timeReadyForReview == null) { + throw new NotFoundException( + "Task with id=" + task.id() + " does not have status 'ready_for_review'" + ); + } + + return Duration.between(timeInProgress, timeReadyForReview); + } + + public Duration getTimeInTesting(long taskId) { + Task task = taskRepository.getExisted(taskId); + List activities = handler.getRepository().findAllByTaskIdOrderByUpdatedDesc(task.id()); + + LocalDateTime timeReadyForReview = getTimeTaskByStatus("ready_for_review", activities); + if (timeReadyForReview == null) { + throw new NotFoundException( + "Task with id=" + task.id() + " does not have status 'ready_for_review'" + ); + } + + LocalDateTime timeDone = getTimeTaskByStatus("done", activities); + if (timeDone == null) { + throw new NotFoundException( + "Task with id=" + task.id() + " does not have status 'done'" + ); + } + + return Duration.between(timeReadyForReview, timeDone); + } + + private LocalDateTime getTimeTaskByStatus(String status, List activities) { + for (Activity activity : activities) { + if (status.equals(activity.getStatusCode())) { + return activity.getUpdated(); + } + } + return null; + } } 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..a37a2381a 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java @@ -3,16 +3,14 @@ import com.javarush.jira.bugtracking.Handlers; import com.javarush.jira.bugtracking.UserBelong; import com.javarush.jira.bugtracking.UserBelongRepository; -import com.javarush.jira.bugtracking.task.to.ActivityTo; -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.*; import com.javarush.jira.bugtracking.tree.ITreeNode; import com.javarush.jira.common.util.Util; import com.javarush.jira.login.AuthUser; import jakarta.annotation.Nullable; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -156,4 +154,22 @@ public TaskTreeNode(TaskTo taskTo) { this(taskTo, new LinkedList<>()); } } + + @PatchMapping("/{id}/tags") + public ResponseEntity addTag( + @PathVariable long id, + @RequestParam @Size(min = 1, max = 30) String tag) { + return ResponseEntity.ok(taskService.addTag(id, tag)); + } + + @PatchMapping("/{id}/tags/{tag}") + public ResponseEntity removeTag(@PathVariable long id, @PathVariable String tag) { + return ResponseEntity.ok(taskService.removeTag(id, tag)); + } + + @GetMapping("/{id}/tags") + public ResponseEntity getTaskWithTags(@PathVariable long id) { + return ResponseEntity.ok(taskService.getTaskTags(id)); + } + } 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..53cc80e35 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java @@ -7,8 +7,10 @@ import com.javarush.jira.bugtracking.sprint.SprintRepository; import com.javarush.jira.bugtracking.task.mapper.TaskExtMapper; import com.javarush.jira.bugtracking.task.mapper.TaskFullMapper; +import com.javarush.jira.bugtracking.task.mapper.TaskTagsMapper; import com.javarush.jira.bugtracking.task.to.TaskToExt; import com.javarush.jira.bugtracking.task.to.TaskToFull; +import com.javarush.jira.bugtracking.task.to.TaskToTags; import com.javarush.jira.common.error.DataConflictException; import com.javarush.jira.common.error.NotFoundException; import com.javarush.jira.common.util.Util; @@ -20,7 +22,10 @@ import org.springframework.util.Assert; import java.time.LocalDateTime; +import java.util.Collections; +import java.util.HashSet; 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; @@ -33,17 +38,19 @@ public class TaskService { static final String CANNOT_ASSIGN = "Cannot assign as %s to task with status=%s"; static final String CANNOT_UN_ASSIGN = "Cannot unassign as %s from task with status=%s"; - private final Handlers.TaskExtHandler handler; + private final Handlers.TaskExtHandler extHandler; private final Handlers.ActivityHandler activityHandler; + private final Handlers.TaskTagsHandler tagsHandler; private final TaskFullMapper fullMapper; private final SprintRepository sprintRepository; private final TaskExtMapper extMapper; private final UserBelongRepository userBelongRepository; + private final TaskTagsMapper tagsMapper; @Transactional public void changeStatus(long taskId, String statusCode) { Assert.notNull(statusCode, "statusCode must not be null"); - Task task = handler.getRepository().getExisted(taskId); + Task task = extHandler.getRepository().getExisted(taskId); if (!statusCode.equals(task.getStatusCode())) { task.checkAndSetStatusCode(statusCode); Activity statusChangedActivity = new Activity(null, taskId, AuthUser.authId()); @@ -51,14 +58,14 @@ public void changeStatus(long taskId, String statusCode) { activityHandler.create(statusChangedActivity); String userType = getRefTo(RefType.TASK_STATUS, statusCode).getAux(1); if (userType != null) { - handler.createUserBelong(taskId, TASK, AuthUser.authId(), userType); + extHandler.createUserBelong(taskId, TASK, AuthUser.authId(), userType); } } } @Transactional public void changeSprint(long taskId, Long sprintId) { - Task task = handler.getRepository().getExisted(taskId); + Task task = extHandler.getRepository().getExisted(taskId); if (task.getParentId() != null) { throw new DataConflictException("Can't change subtask sprint"); } @@ -68,12 +75,12 @@ public void changeSprint(long taskId, Long sprintId) { throw new DataConflictException("Target sprint must belong to the same project"); } } - handler.getRepository().setTaskAndSubTasksSprint(taskId, sprintId); + extHandler.getRepository().setTaskAndSubTasksSprint(taskId, sprintId); } @Transactional public Task create(TaskToExt taskTo) { - Task created = handler.createWithBelong(taskTo, TASK, "task_author"); + Task created = extHandler.createWithBelong(taskTo, TASK, "task_author"); activityHandler.create(makeActivity(created.id(), taskTo)); return created; } @@ -81,13 +88,13 @@ public Task create(TaskToExt taskTo) { @Transactional public void update(TaskToExt taskTo, long id) { if (!taskTo.equals(get(taskTo.id()))) { - handler.updateFromTo(taskTo, id); + extHandler.updateFromTo(taskTo, id); activityHandler.create(makeActivity(id, taskTo)); } } public TaskToFull get(long id) { - Task task = Util.checkExist(id, handler.getRepository().findFullById(id)); + Task task = Util.checkExist(id, extHandler.getRepository().findFullById(id)); TaskToFull taskToFull = fullMapper.toTo(task); List activities = activityHandler.getRepository().findAllByTaskIdOrderByUpdatedDesc(id); fillExtraFields(taskToFull, activities); @@ -110,7 +117,7 @@ public TaskToExt getNewWithProject(long projectId) { } public TaskToExt getNewWithParent(long parentId) { - Task parent = handler.getRepository().getExisted(parentId); + Task parent = extHandler.getRepository().getExisted(parentId); Task newTask = new Task(); newTask.setParentId(parentId); newTask.setSprintId(parent.getSprintId()); @@ -120,7 +127,7 @@ public TaskToExt getNewWithParent(long parentId) { public void assign(long id, String userType, long userId) { checkAssignmentActionPossible(id, userType, true); - handler.createUserBelong(id, TASK, userId, userType); + extHandler.createUserBelong(id, TASK, userId, userType); } @Transactional @@ -134,10 +141,45 @@ public void unAssign(long id, String userType, long userId) { private void checkAssignmentActionPossible(long id, String userType, boolean assign) { Assert.notNull(userType, "userType must not be null"); - Task task = handler.getRepository().getExisted(id); + Task task = extHandler.getRepository().getExisted(id); String possibleUserType = getRefTo(RefType.TASK_STATUS, task.getStatusCode()).getAux(1); if (!userType.equals(possibleUserType)) { throw new DataConflictException(String.format(assign ? CANNOT_ASSIGN : CANNOT_UN_ASSIGN, userType, task.getStatusCode())); } } + + public TaskToTags getTaskTags(long taskId) { + Task task = tagsHandler.getRepository().getExisted(taskId); + return tagsMapper.toTo(task); + } + + @Transactional + public TaskToTags addTag(long taskId, String tag) { + Assert.notNull(tag, "tag must not be null"); + + Task task = tagsHandler.getRepository().getExisted(taskId); + + Set newTags = copyTaskTags(task); + newTags.add(tag); + + task.setTags(Collections.unmodifiableSet(newTags)); + return tagsMapper.toTo(task); + } + + @Transactional + public TaskToTags removeTag(long taskId, String tag) { + Assert.notNull(tag, "tag must not be null"); + + Task task = tagsHandler.getRepository().getExisted(taskId); + + Set newTags = copyTaskTags(task); + newTags.remove(tag); + + task.setTags(Collections.unmodifiableSet(newTags)); + return tagsMapper.toTo(task); + } + + private Set copyTaskTags(Task task) { + return new HashSet<>(task.getTags()); + } } diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskUIController.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskUIController.java index fbcc46aa2..172907bb7 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskUIController.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskUIController.java @@ -6,8 +6,10 @@ 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.TaskToTags; import com.javarush.jira.ref.RefTo; import jakarta.validation.Valid; +import jakarta.validation.constraints.Size; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; @@ -103,6 +105,24 @@ public String createOrUpdate(@Valid @ModelAttribute("task") TaskToExt taskTo, Bi } return "redirect:/ui/tasks/" + taskId; } + + @PostMapping("/add-tag") + public String addTag(@RequestParam long taskId, + @RequestParam @Size(min = 1, max = 30) String tagName, + Model model) { + log.info("add tag {} to task {}", tagName, taskId); + TaskToTags taskToTags = service.addTag(taskId, tagName); + model.addAttribute("tags", taskToTags.getTags()); + return "redirect:/ui/tasks/" + taskId; + } + + @PostMapping("/remove-tag") + public String removeTag(@RequestParam long taskId, @RequestParam String tagName, Model model) { + log.info("remove tag {} from task {}", tagName, taskId); + TaskToTags taskToTags = service.removeTag(taskId, tagName); + model.addAttribute("tags", taskToTags.getTags()); + return "redirect:/ui/tasks/" + taskId; + } private void addTaskInfo(Model model, TaskToFull taskTo) { List comments = getComments(taskTo.getActivityTos()); diff --git a/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskTagsMapper.java b/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskTagsMapper.java new file mode 100644 index 000000000..50e9357f9 --- /dev/null +++ b/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskTagsMapper.java @@ -0,0 +1,14 @@ +package com.javarush.jira.bugtracking.task.mapper; + +import com.javarush.jira.bugtracking.task.Task; +import com.javarush.jira.bugtracking.task.to.TaskToTags; +import com.javarush.jira.common.BaseMapper; +import com.javarush.jira.common.TimestampMapper; +import org.mapstruct.Mapper; + +@Mapper(config = TimestampMapper.class) +public interface TaskTagsMapper extends BaseMapper { + + @Override + TaskToTags toTo(Task task); +} diff --git a/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToTags.java b/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToTags.java new file mode 100644 index 000000000..060d60982 --- /dev/null +++ b/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToTags.java @@ -0,0 +1,20 @@ +package com.javarush.jira.bugtracking.task.to; + +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.Set; + +@Getter +public class TaskToTags extends TaskToExt { + Set tags; + + public TaskToTags(Long id, String code, String title, String description, + String typeCode, String statusCode, String priorityCode, + LocalDateTime updated, Integer estimate, Long parentId, + long projectId, Long sprintId, Set tags) { + super(id, code, title, description, typeCode, statusCode, priorityCode, updated, estimate, parentId, projectId, sprintId); + + this.tags = (tags == null) ? Set.of() : Set.copyOf(tags); + } +} diff --git a/src/main/java/com/javarush/jira/common/internal/config/MvcConfig.java b/src/main/java/com/javarush/jira/common/internal/config/MvcConfig.java index 8a434a807..b4f09f490 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 @@ -19,9 +19,14 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.handler.WebRequestHandlerInterceptorAdapter; +import org.springframework.web.servlet.i18n.CookieLocaleResolver; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import org.springframework.web.servlet.i18n.SessionLocaleResolver; import org.springframework.web.servlet.mvc.UrlFilenameViewController; +import org.springframework.web.servlet.LocaleResolver; import java.time.Duration; +import java.util.Locale; import java.util.Properties; //@EnableWebMvc : http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration @@ -53,11 +58,6 @@ public void preHandle(WebRequest request) { } }); - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(authInterceptor).excludePathPatterns("/api/**"); - } - // http://www.codejava.net/frameworks/spring/spring-mvc-url-based-view-resolution-with-urlfilenameviewcontroller-example @Bean public SimpleUrlHandlerMapping getUrlHandlerMapping() { @@ -95,4 +95,31 @@ public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { .setReadTimeout(Duration.ofSeconds(10)) .build(); } + + @Bean + public LocaleResolver localeResolver() { + SessionLocaleResolver resolver = new SessionLocaleResolver(); + resolver.setDefaultLocale(Locale.ENGLISH); + return resolver; + } + + @Bean + public LocaleChangeInterceptor localeChangeInterceptor() { + LocaleChangeInterceptor lci = new LocaleChangeInterceptor(); + lci.setParamName("lang"); + return lci; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authInterceptor).excludePathPatterns("/api/**"); + registry.addInterceptor(localeChangeInterceptor()); + } + +// @Bean +// public LocaleResolver localeResolver() { +// CookieLocaleResolver resolver = new CookieLocaleResolver(); +// resolver.setDefaultLocale(Locale.ENGLISH); +// return resolver; +// } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7fcba1570..ab37d484d 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,13 +1,18 @@ # 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 + test-mail: ${MAIL_TEST} templates-update-cache: 5s mail-sending-props: core-pool-size: 8 max-pool-size: 100 spring: + messages: + basename: messages + encoding: UTF-8 + config: + import: optional:classpath:secrets.properties init: mode: never jpa: @@ -27,11 +32,12 @@ 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" + contexts: prod # Jackson Fields Serialization jackson: @@ -51,48 +57,48 @@ spring: client: registration: github: - client-id: 3d0d8738e65881fff266 - client-secret: 0f97031ce6178b7dfb67a6af587f37e222a16120 + client-id: ${OAUTH_GITHUB_CLIENT_ID} + client-secret: ${OAUTH_GITHUB_CLIENT_SECRET} scope: - email google: - client-id: 329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com - client-secret: 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 + client-id: ${OAUTH_EMAIL_GOOGLE_CLIENT_ID} + client-secret: ${OAUTH_EMAIL_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 +# vk: +# client-id: ${OAUTH_EMAIL_GOOGLE_CLIENT_ID} +# client-secret: ${OAUTH_EMAIL_GOOGLE_CLIENT_SECRET} +# 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: ${OAUTH_YANDEX_CLIENT_ID} +# client-secret: ${OAUTH_YANDEX_CLIENT_SECRET} +# client-name: Yandex +# redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" +# authorization-grant-type: authorization_code gitlab: - client-id: b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494 - client-secret: e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004 + client-id: ${OAUTH_GITLAB_CLIENT_ID} + client-secret: ${OAUTH_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 +# 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 +117,8 @@ spring: enable: true auth: true host: smtp.gmail.com - username: jira4jr@gmail.com - password: zdfzsrqvgimldzyj + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} port: 587 thymeleaf.check-template-location: false diff --git a/src/main/resources/data4dev/data.sql b/src/main/resources/data4dev/data.sql index a7d43cbad..df4788bc9 100644 --- a/src/main/resources/data4dev/data.sql +++ b/src/main/resources/data4dev/data.sql @@ -2,7 +2,7 @@ DELETE FROM USERS; alter -sequence USERS_ID_SEQ restart with 1; + 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'), @@ -60,7 +60,7 @@ values (1, 'skype', 'userSkype'), delete from ATTACHMENT; alter -sequence ATTACHMENT_ID_SEQ restart with 1; + sequence ATTACHMENT_ID_SEQ restart with 1; insert into ATTACHMENT (name, file_link, object_id, object_type, user_id, date_time) values ('Снимок экрана 1.png', './attachments/project/1_Снимок экрана 1.png', 2, 0, 4, '2023-05-04 22:28:50.215429'), ('Снимок экрана 2.png', './attachments/project/2_Снимок экрана 2.png', 2, 0, 4, '2023-05-04 22:28:53.687600'), @@ -71,26 +71,26 @@ values ('Снимок экрана 1.png', './attachments/project/1_Снимок ('Ежедневный-чеклист.xlsx', './attachments/task/3_Ежедневный-чеклист.xlsx', 38, 2, 4, '2023-05-04 22:28:50.215429'); alter -sequence ATTACHMENT_ID_SEQ restart with 1000; + sequence ATTACHMENT_ID_SEQ restart with 1000; -- populate tasks delete from TASK; alter -sequence TASK_ID_SEQ restart with 1; + sequence TASK_ID_SEQ restart with 1; delete from SPRINT; alter -sequence SPRINT_ID_SEQ restart with 1; + sequence SPRINT_ID_SEQ restart with 1; delete from PROJECT; alter -sequence PROJECT_ID_SEQ restart with 1; + sequence PROJECT_ID_SEQ restart with 1; delete from ACTIVITY; alter -sequence ACTIVITY_ID_SEQ restart with 1; + sequence ACTIVITY_ID_SEQ restart with 1; insert into PROJECT (code, title, description, type_code, parent_id) values ('JiraRush', 'JiraRush', '«Mini-JIRA» app : project management system tutorial app', 'task_tracker', null), @@ -98,7 +98,7 @@ values ('JiraRush', 'JiraRush', '«Mini-JIRA» app : project management system t ('Test_Project_2', 'Test Project 2', 'Just test project 2', 'task_tracker', null), ('JiraRush sub', 'JiraRush subproject', 'subproject', 'task_tracker', 1); alter -sequence PROJECT_ID_SEQ restart with 1000; + sequence PROJECT_ID_SEQ restart with 1000; insert into SPRINT (status_code, startpoint, endpoint, code, project_id) values ('active', null, null, 'Sprint-2', 1), @@ -107,7 +107,7 @@ values ('active', null, null, 'Sprint-2', 1), ('active', '2023-04-05 14:25:43', '2023-06-10 13:00:00', 'Sprint-3', 2), ('active', null, null, 'Sprint-1', 4); alter -sequence SPRINT_ID_SEQ restart with 1000; + sequence SPRINT_ID_SEQ restart with 1000; ---- project 1 ------------- INSERT INTO TASK (TITLE, TYPE_CODE, STATUS_CODE, PROJECT_ID, SPRINT_ID, STARTPOINT) @@ -313,7 +313,7 @@ values ('Add role manager and filters in security', 'task', 'done', 1, 1, 1, ('Subproject backlog subtask', 'task', 'in_progress', 4, null, 88, now() + random() * interval '5 minutes' + random() * interval '20 seconds'); alter -sequence TASK_ID_SEQ restart with 1000; + sequence TASK_ID_SEQ restart with 1000; ---task 1------ INSERT INTO ACTIVITY(AUTHOR_ID, TASK_ID, UPDATED, COMMENT, TITLE, DESCRIPTION, ESTIMATE, TYPE_CODE, STATUS_CODE, @@ -323,5 +323,4 @@ values (6, 1, '2023-05-15 09:05:10', null, 'Data', null, 3, 'epic', 'in_progress (6, 1, '2023-05-15 14:05:10', null, 'Data', null, 4, null, null, null), ---task 118---- (11, 118, '2023-05-16 10:05:10', null, 'UI tab of tasks', null, 4, 'task', 'in_progress', 'normal'), (5, 118, '2023-05-16 11:10:10', null, 'UI tab of tasks', null, null, null, null, 'high'), - (11, 118, '2023-05-16 12:30:10', null, 'UI tab of tasks', null, 2, null, null, null); - + (11, 118, '2023-05-16 12:30:10', null, 'UI tab of tasks', null, 2, null, null, null); \ No newline at end of file diff --git a/src/main/resources/db/changelog.sql b/src/main/resources/db/changelog.sql index 68591336d..f3dbbbe03 100644 --- a/src/main/resources/db/changelog.sql +++ b/src/main/resources/db/changelog.sql @@ -5,33 +5,33 @@ 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; + 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; + SEQUENCE IF EXISTS USER_BELONG_ID_SEQ; DROP TABLE IF EXISTS ACTIVITY; DROP -SEQUENCE IF EXISTS ACTIVITY_ID_SEQ; + SEQUENCE IF EXISTS ACTIVITY_ID_SEQ; DROP TABLE IF EXISTS TASK; DROP -SEQUENCE IF EXISTS TASK_ID_SEQ; + SEQUENCE IF EXISTS TASK_ID_SEQ; DROP TABLE IF EXISTS SPRINT; DROP -SEQUENCE IF EXISTS SPRINT_ID_SEQ; + SEQUENCE IF EXISTS SPRINT_ID_SEQ; DROP TABLE IF EXISTS PROJECT; DROP -SEQUENCE IF EXISTS PROJECT_ID_SEQ; + SEQUENCE IF EXISTS PROJECT_ID_SEQ; DROP TABLE IF EXISTS REFERENCE; DROP -SEQUENCE IF EXISTS REFERENCE_ID_SEQ; + SEQUENCE IF EXISTS REFERENCE_ID_SEQ; DROP TABLE IF EXISTS ATTACHMENT; DROP -SEQUENCE IF EXISTS ATTACHMENT_ID_SEQ; + SEQUENCE IF EXISTS ATTACHMENT_ID_SEQ; DROP TABLE IF EXISTS USERS; DROP -SEQUENCE IF EXISTS USERS_ID_SEQ; + SEQUENCE IF EXISTS USERS_ID_SEQ; create table PROJECT ( @@ -250,19 +250,19 @@ values ('assigned', 'Assigned', 6, '1'), alter table SPRINT rename COLUMN TITLE to CODE; alter table SPRINT - alter column CODE type varchar (32); +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; +DROP COLUMN DESCRIPTION; ALTER TABLE TASK - DROP COLUMN PRIORITY_CODE; +DROP COLUMN PRIORITY_CODE; ALTER TABLE TASK - DROP COLUMN ESTIMATE; +DROP COLUMN ESTIMATE; ALTER TABLE TASK - DROP COLUMN UPDATED; +DROP COLUMN UPDATED; --changeset ishlyakhtenkov:change_task_status_reference @@ -282,15 +282,15 @@ values ('todo', 'ToDo', 3, 'in_progress,canceled'), --changeset gkislin:users_add_on_delete_cascade alter table ACTIVITY - drop constraint FK_ACTIVITY_USERS, +drop constraint FK_ACTIVITY_USERS, add constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID) on delete cascade; alter table USER_BELONG - drop constraint FK_USER_BELONG, +drop constraint FK_USER_BELONG, add constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) on delete cascade; alter table ATTACHMENT - drop constraint FK_ATTACHMENT, +drop constraint FK_ATTACHMENT, add constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) on delete cascade; --changeset valeriyemelyanov:change_user_type_reference @@ -329,3 +329,16 @@ 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; + +-- --changeset alex:activity-status-1 +-- INSERT INTO ACTIVITY (AUTHOR_ID, TASK_ID, UPDATED, STATUS_CODE) +-- VALUES (13, 200, '2025-08-16 10:05:10', 'in_progress'); +-- +-- --changeset alex:activity-status-2 +-- INSERT INTO ACTIVITY (AUTHOR_ID, TASK_ID, UPDATED, STATUS_CODE) +-- VALUES (13, 200, '2025-09-16 11:10:10', 'ready_for_review'); +-- +-- --changeset alex:activity-status-3 +-- INSERT INTO ACTIVITY (AUTHOR_ID, TASK_ID, UPDATED, STATUS_CODE) +-- VALUES (13, 200, '2025-10-16 12:30:10', 'done'); + diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties new file mode 100644 index 000000000..b91052c22 --- /dev/null +++ b/src/main/resources/messages.properties @@ -0,0 +1,15 @@ +index.title=JiraRush: mini bugtracking system +index.heading=JiraRush Home page +index.logout=Logout + +mail.confirm.title=JiraRush - Email confirmation +mail.confirm.greeting=Hello, {0}. +mail.confirm.text=To complete your account setup and start using JiraRush, please confirm that you entered your email correctly. +mail.confirm.button=Confirm email + +mail.reset.title=JiraRush - Reset password +mail.reset.greeting=Hello, {0}. +mail.reset.text=We received a request to reset your JiraRush password for the account: {0}. +mail.reset.button=Set new password + +language.text=Language: \ No newline at end of file diff --git a/src/main/resources/messages_ru.properties b/src/main/resources/messages_ru.properties new file mode 100644 index 000000000..c00331afc --- /dev/null +++ b/src/main/resources/messages_ru.properties @@ -0,0 +1,15 @@ +index.title=JiraRush: \u043C\u0438\u043D\u0438 \u0441\u0438\u0441\u0442\u0435\u043C\u0430 \u043E\u0442\u0441\u043B\u0435\u0436\u0438\u0432\u0430\u043D\u0438\u044F \u0431\u0430\u0433\u043E\u0432 +index.heading=\u0413\u043B\u0430\u0432\u043D\u0430\u044F \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430 JiraRush +index.logout=\u0412\u044B\u0439\u0442\u0438 + +mail.confirm.title=JiraRush - \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u043E\u0447\u0442\u044B +mail.confirm.greeting=\u041F\u0440\u0438\u0432\u0435\u0442, {0}. +mail.confirm.text=\u0427\u0442\u043E\u0431\u044B \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0443 \u0443\u0447\u0435\u0442\u043D\u043E\u0439 \u0437\u0430\u043F\u0438\u0441\u0438 \u0438 \u043D\u0430\u0447\u0430\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C\u0441\u044F JiraRush, \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435, \u0447\u0442\u043E \u0432\u044B \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u043E \u0443\u043A\u0430\u0437\u0430\u043B\u0438 \u0432\u0430\u0448\u0443 \u044D\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u0443\u044E \u043F\u043E\u0447\u0442\u0443. +mail.confirm.button=\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u043F\u043E\u0447\u0442\u0443 + +mail.reset.title=JiraRush - \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u043D\u043E\u0432\u044B\u0439 \u043F\u0430\u0440\u043E\u043B\u044C +mail.reset.greeting=\u041F\u0440\u0438\u0432\u0435\u0442, {0}. +mail.reset.text=\u041C\u044B \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u0438 \u0437\u0430\u043F\u0440\u043E\u0441 \u043D\u0430 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0443 \u043D\u043E\u0432\u043E\u0433\u043E \u043F\u0430\u0440\u043E\u043B\u044F JiraRush \u0434\u043B\u044F \u0443\u0447\u0435\u0442\u043D\u043E\u0439 \u0437\u0430\u043F\u0438\u0441\u0438: {0}. +mail.reset.button=\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u043F\u0430\u0440\u043E\u043B\u044C + +language.text=\u042F\u0437\u044B\u043A: \ No newline at end of file diff --git a/src/main/resources/messages_uk.properties b/src/main/resources/messages_uk.properties new file mode 100644 index 000000000..430c6fbb2 --- /dev/null +++ b/src/main/resources/messages_uk.properties @@ -0,0 +1,15 @@ +index.title=JiraRush: \u043C\u0456\u043D\u0456 \u0441\u0438\u0441\u0442\u0435\u043C\u0430 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043D\u043D\u044F \u0431\u0430\u0433\u0456\u0432 +index.heading=\u0414\u043E\u043C\u0430\u0448\u043D\u044F \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0430 JiraRush +index.logout=\u0412\u0438\u0439\u0442\u0438 + +mail.confirm.title=JiraRush - \u041F\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043D\u043D\u044F \u043F\u043E\u0448\u0442\u0438 +mail.confirm.greeting=\u0412\u0456\u0442\u0430\u0454\u043C\u043E, {0}. +mail.confirm.text=\u0429\u043E\u0431 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043D\u0430\u043B\u0430\u0448\u0442\u0443\u0432\u0430\u043D\u043D\u044F \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u043E\u0433\u043E \u0437\u0430\u043F\u0438\u0441\u0443 \u0442\u0430 \u043F\u043E\u0447\u0430\u0442\u0438 \u043A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0442\u0438\u0441\u044F JiraRush, \u043F\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u044C\u0442\u0435, \u0449\u043E \u0432\u0438 \u0432\u0432\u0435\u043B\u0438 \u0430\u0434\u0440\u0435\u0441\u0443 \u0435\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u043E\u0457 \u043F\u043E\u0448\u0442\u0438 \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u043E. +mail.confirm.button=\u041F\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438 \u043F\u043E\u0448\u0442\u0443 + +mail.reset.title=JiraRush - \u0412\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0438 \u043D\u043E\u0432\u0438\u0439 \u043F\u0430\u0440\u043E\u043B\u044C +mail.reset.greeting=\u0412\u0456\u0442\u0430\u0454\u043C\u043E, {0}. +mail.reset.text=\u041C\u0438 \u043E\u0442\u0440\u0438\u043C\u0430\u043B\u0438 \u0437\u0430\u043F\u0438\u0442 \u043D\u0430 \u0432\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044F \u043D\u043E\u0432\u043E\u0433\u043E \u043F\u0430\u0440\u043E\u043B\u044E JiraRush \u0434\u043B\u044F \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u043E\u0433\u043E \u0437\u0430\u043F\u0438\u0441\u0443: {0}. +mail.reset.button=\u0412\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0438 \u043F\u0430\u0440\u043E\u043B\u044C + +language.text=\u041C\u043E\u0432\u0430: \ No newline at end of file diff --git a/src/test/java/com/javarush/jira/AbstractControllerTest.java b/src/test/java/com/javarush/jira/AbstractControllerTest.java index 5981bae53..0548d840c 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.sql", "classpath:data-test.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/profile/internal/web/ProfileRestControllerTest.java b/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java index a6fd5e3bf..65612df1f 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,126 @@ 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 com.javarush.jira.profile.internal.model.Profile; +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.GUEST_MAIL; +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.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class ProfileRestControllerTest extends AbstractControllerTest { + @Autowired + private ProfileRepository profileRepository; + @Test + @WithUserDetails(value = USER_MAIL) + void getUserProfile() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(PROFILE_TO_MATCHER.contentJson(USER_PROFILE_TO)); + } + + @Test + @WithUserDetails(value = GUEST_MAIL) + void getGuestProfile() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(PROFILE_TO_MATCHER.contentJson(GUEST_PROFILE_EMPTY_TO)); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateValid() throws Exception { + ProfileTo updatedTo = ProfileTestData.getUpdatedTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isNoContent()); + + Profile updatedProfile = profileRepository.getExisted(updatedTo.getId()); + PROFILE_MATCHER.assertMatch(updatedProfile, ProfileTestData.getUpdated(updatedProfile.getId())); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateInValid() throws Exception { + ProfileTo invalidTo = ProfileTestData.getInvalidTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(invalidTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateWithUnknownNotification() throws Exception { + ProfileTo invalidNotificationTo = ProfileTestData.getWithUnknownNotificationTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(invalidNotificationTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateWithUnknownContact() throws Exception { + ProfileTo invalidContactTo = ProfileTestData.getWithUnknownContactTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(invalidContactTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateWithHtmlUnsafeContact() throws Exception { + ProfileTo unsafeContactTo = ProfileTestData.getWithContactHtmlUnsafeTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(unsafeContactTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void getUnauthorized() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andDo(print()) + .andExpect(status().isUnauthorized()); + } + + @Test + void updateUnauthorized() throws Exception { + ProfileTo updatedTo = ProfileTestData.getUpdatedTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isUnauthorized()); + } } \ No newline at end of file diff --git a/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java b/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java index fb4407268..0a2c5a4f9 100644 --- a/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java +++ b/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java @@ -10,15 +10,22 @@ import java.util.Set; public class ProfileTestData { + public static final long USER_ID = 1L; + public static final long GUEST_ID = 3L; + + public static MatcherFactory.Matcher PROFILE_TO_MATCHER = + MatcherFactory.usingIgnoringFieldsComparator(ProfileTo.class, "id"); + public static MatcherFactory.Matcher PROFILE_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(Profile.class, "user"); - public static ProfileTo USER_PROFILE_TO = new ProfileTo(null, + public static ProfileTo USER_PROFILE_TO = new ProfileTo(USER_ID, Set.of("assigned", "overdue", "deadline"), Set.of(new ContactTo("skype", "userSkype"), new ContactTo("mobile", "+01234567890"), new ContactTo("website", "user.com"))); - public static ProfileTo GUEST_PROFILE_EMPTY_TO = new ProfileTo(null, + + public static ProfileTo GUEST_PROFILE_EMPTY_TO = new ProfileTo(GUEST_ID, Set.of(), Set.of()); @@ -37,7 +44,7 @@ public static Profile getNew(long id) { } public static ProfileTo getUpdatedTo() { - return new ProfileTo(null, + return new ProfileTo(USER_ID, Set.of("assigned", "three_days_before_deadline", "two_days_before_deadline", "one_day_before_deadline", "deadline", "overdue"), Set.of(new ContactTo("skype", "newSkype"), new ContactTo("mobile", "+380987654321"), diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 51137fd06..3cfe08422 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,8 +1,24 @@ spring.cache.type: none spring: + config: + import: optional:classpath:secrets-test.properties init: mode: always datasource: url: jdbc:postgresql://localhost:5433/jira-test - username: jira - password: JiraRush \ No newline at end of file + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + +#spring: +# datasource: +# driver-class-name: org.h2.Driver +# url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +# username: sa +# password: +# jpa: +# hibernate: +# ddl-auto: create-drop +# database-platform: org.hibernate.dialect.H2Dialect +# liquibase: +# contexts: test +# enabled: false diff --git a/src/test/resources/changelog-test.sql b/src/test/resources/changelog-test.sql new file mode 100644 index 000000000..3bcb6ee9e --- /dev/null +++ b/src/test/resources/changelog-test.sql @@ -0,0 +1,326 @@ +--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 bigint generated by default as identity 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 bigint generated by default as identity 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 bigint generated by default as identity 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 bigint generated by default as identity 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 bigint generated by default as identity 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, + CONTACT_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 bigint generated by default as identity 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 bigint generated by default as identity 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 bigint generated by default as identity 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 bigint generated by default as identity 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 drop column TITLE; +alter table SPRINT add column CODE 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) where ENDPOINT is null; diff --git a/src/test/resources/data.sql b/src/test/resources/data-test.sql similarity index 92% rename from src/test/resources/data.sql rename to src/test/resources/data-test.sql index 5087dbddc..ba459710c 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data-test.sql @@ -9,24 +9,24 @@ from PROFILE; delete from ACTIVITY; alter -sequence ACTIVITY_ID_SEQ restart with 1; + sequence ACTIVITY_ID_SEQ restart with 1; delete from TASK; alter -sequence TASK_ID_SEQ restart with 1; + sequence TASK_ID_SEQ restart with 1; delete from SPRINT; alter -sequence SPRINT_ID_SEQ restart with 1; + sequence SPRINT_ID_SEQ restart with 1; delete from PROJECT; alter -sequence PROJECT_ID_SEQ restart with 1; + sequence PROJECT_ID_SEQ restart with 1; delete from USERS; alter -sequence USERS_ID_SEQ restart with 1; + 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'), @@ -95,4 +95,4 @@ values (1, 2, 2, 'task_developer', '2023-06-14 08:35:10', '2023-06-14 08:55:00') (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); + (2, 2, 1, 'task_tester', '2023-06-10 16:37:00', null); \ No newline at end of file diff --git a/src/test/resources/secrets-test.properties b/src/test/resources/secrets-test.properties new file mode 100644 index 000000000..fd8aa8e33 --- /dev/null +++ b/src/test/resources/secrets-test.properties @@ -0,0 +1,3 @@ +# DB +DB_USERNAME=jira +DB_PASSWORD=jira-test \ No newline at end of file From 945c30bf044993b0575a4a27d5934d773aa82951 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 7 Sep 2025 12:14:51 +0300 Subject: [PATCH 2/2] Task Rework 1 --- src/main/resources/application.yaml | 2 +- src/main/resources/data4dev/data.sql | 2 +- src/main/resources/db/changelog.sql | 15 ++--- .../web/ProfileRestControllerTest.java | 59 ++++++++++++------- .../profile/internal/web/ProfileTestData.java | 9 ++- src/test/resources/application-test.yaml | 6 +- src/test/resources/changelog-test.sql | 55 ++++++++--------- src/test/resources/data-test.sql | 51 ++++++++-------- 8 files changed, 102 insertions(+), 97 deletions(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index ab37d484d..8377720be 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -37,7 +37,7 @@ spring: liquibase: changeLog: "classpath:db/changelog.sql" - contexts: prod +# contexts: prod # Jackson Fields Serialization jackson: diff --git a/src/main/resources/data4dev/data.sql b/src/main/resources/data4dev/data.sql index df4788bc9..4cdbd09b7 100644 --- a/src/main/resources/data4dev/data.sql +++ b/src/main/resources/data4dev/data.sql @@ -323,4 +323,4 @@ values (6, 1, '2023-05-15 09:05:10', null, 'Data', null, 3, 'epic', 'in_progress (6, 1, '2023-05-15 14:05:10', null, 'Data', null, 4, null, null, null), ---task 118---- (11, 118, '2023-05-16 10:05:10', null, 'UI tab of tasks', null, 4, 'task', 'in_progress', 'normal'), (5, 118, '2023-05-16 11:10:10', null, 'UI tab of tasks', null, null, null, null, 'high'), - (11, 118, '2023-05-16 12:30:10', null, 'UI tab of tasks', null, 2, null, null, null); \ No newline at end of file + (11, 118, '2023-05-16 12:30:10', null, 'UI tab of tasks', null, 2, null, null, null); diff --git a/src/main/resources/db/changelog.sql b/src/main/resources/db/changelog.sql index f3dbbbe03..171b980d5 100644 --- a/src/main/resources/db/changelog.sql +++ b/src/main/resources/db/changelog.sql @@ -330,15 +330,8 @@ 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; --- --changeset alex:activity-status-1 +-- --changeset alex04:insert_activity -- INSERT INTO ACTIVITY (AUTHOR_ID, TASK_ID, UPDATED, STATUS_CODE) --- VALUES (13, 200, '2025-08-16 10:05:10', 'in_progress'); --- --- --changeset alex:activity-status-2 --- INSERT INTO ACTIVITY (AUTHOR_ID, TASK_ID, UPDATED, STATUS_CODE) --- VALUES (13, 200, '2025-09-16 11:10:10', 'ready_for_review'); --- --- --changeset alex:activity-status-3 --- INSERT INTO ACTIVITY (AUTHOR_ID, TASK_ID, UPDATED, STATUS_CODE) --- VALUES (13, 200, '2025-10-16 12:30:10', 'done'); - +-- VALUES(11, 118, now() - interval '5 hours', 'in_progress'), +-- (11, 118, now() - interval '2 hours', 'ready_for_review'), +-- (11, 118, now() - interval '30 minutes', 'done'); 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 65612df1f..7d08bb618 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 @@ -4,6 +4,7 @@ import com.javarush.jira.profile.ProfileTo; import com.javarush.jira.profile.internal.ProfileRepository; import com.javarush.jira.profile.internal.model.Profile; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; @@ -19,6 +20,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@Slf4j class ProfileRestControllerTest extends AbstractControllerTest { @Autowired private ProfileRepository profileRepository; @@ -43,10 +45,34 @@ void getGuestProfile() throws Exception { .andExpect(PROFILE_TO_MATCHER.contentJson(GUEST_PROFILE_EMPTY_TO)); } + @Test + @WithUserDetails(value = USER_MAIL) + void updateProfile() throws Exception { + ProfileTo newTo = ProfileTestData.getNewTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(newTo))) + .andDo(print()) + .andExpect(status().isNoContent()); + + Profile expected = ProfileTestData.getNew(USER_ID); + Profile actual = profileRepository.getExisted(USER_ID); + PROFILE_MATCHER.assertMatch(actual, expected); + } + + @Test + void getUnauthorized() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andDo(print()) + .andExpect(status().isUnauthorized()); + } + @Test @WithUserDetails(value = USER_MAIL) void updateValid() throws Exception { ProfileTo updatedTo = ProfileTestData.getUpdatedTo(); + updatedTo.setId(ProfileTestData.USER_ID); perform(MockMvcRequestBuilders.put(REST_URL) .contentType(MediaType.APPLICATION_JSON) @@ -54,8 +80,8 @@ void updateValid() throws Exception { .andDo(print()) .andExpect(status().isNoContent()); - Profile updatedProfile = profileRepository.getExisted(updatedTo.getId()); - PROFILE_MATCHER.assertMatch(updatedProfile, ProfileTestData.getUpdated(updatedProfile.getId())); + Profile updatedProfile = profileRepository.getExisted(ProfileTestData.USER_ID); + PROFILE_MATCHER.assertMatch(updatedProfile, ProfileTestData.getUpdated(ProfileTestData.USER_ID)); } @Test @@ -70,6 +96,17 @@ void updateInValid() throws Exception { .andExpect(status().isUnprocessableEntity()); } + @Test + void updateUnauthorized() throws Exception { + ProfileTo updatedTo = ProfileTestData.getUpdatedTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isUnauthorized()); + } + @Test @WithUserDetails(value = USER_MAIL) void updateWithUnknownNotification() throws Exception { @@ -105,22 +142,4 @@ void updateWithHtmlUnsafeContact() throws Exception { .andDo(print()) .andExpect(status().isUnprocessableEntity()); } - - @Test - void getUnauthorized() throws Exception { - perform(MockMvcRequestBuilders.get(REST_URL)) - .andDo(print()) - .andExpect(status().isUnauthorized()); - } - - @Test - void updateUnauthorized() throws Exception { - ProfileTo updatedTo = ProfileTestData.getUpdatedTo(); - - perform(MockMvcRequestBuilders.put(REST_URL) - .contentType(MediaType.APPLICATION_JSON) - .content(writeValue(updatedTo))) - .andDo(print()) - .andExpect(status().isUnauthorized()); - } } \ No newline at end of file diff --git a/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java b/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java index 0a2c5a4f9..f3359e211 100644 --- a/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java +++ b/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java @@ -11,7 +11,6 @@ public class ProfileTestData { public static final long USER_ID = 1L; - public static final long GUEST_ID = 3L; public static MatcherFactory.Matcher PROFILE_TO_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(ProfileTo.class, "id"); @@ -19,13 +18,13 @@ public class ProfileTestData { public static MatcherFactory.Matcher PROFILE_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(Profile.class, "user"); - public static ProfileTo USER_PROFILE_TO = new ProfileTo(USER_ID, + public static ProfileTo USER_PROFILE_TO = new ProfileTo(null, Set.of("assigned", "overdue", "deadline"), Set.of(new ContactTo("skype", "userSkype"), new ContactTo("mobile", "+01234567890"), new ContactTo("website", "user.com"))); - public static ProfileTo GUEST_PROFILE_EMPTY_TO = new ProfileTo(GUEST_ID, + public static ProfileTo GUEST_PROFILE_EMPTY_TO = new ProfileTo(null, Set.of(), Set.of()); @@ -44,7 +43,7 @@ public static Profile getNew(long id) { } public static ProfileTo getUpdatedTo() { - return new ProfileTo(USER_ID, + return new ProfileTo(null, Set.of("assigned", "three_days_before_deadline", "two_days_before_deadline", "one_day_before_deadline", "deadline", "overdue"), Set.of(new ContactTo("skype", "newSkype"), new ContactTo("mobile", "+380987654321"), @@ -92,4 +91,4 @@ public static ProfileTo getWithContactHtmlUnsafeTo() { Collections.emptySet(), Set.of(new ContactTo("tg", ""))); } -} +} \ No newline at end of file diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 3cfe08422..55a6b7906 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -17,8 +17,10 @@ spring: # password: # jpa: # hibernate: -# ddl-auto: create-drop +# ddl-auto: none # database-platform: org.hibernate.dialect.H2Dialect # liquibase: +# change-log: classpath:changelog-test.sql # contexts: test -# enabled: false +# enabled: true + diff --git a/src/test/resources/changelog-test.sql b/src/test/resources/changelog-test.sql index 3bcb6ee9e..43ac95c29 100644 --- a/src/test/resources/changelog-test.sql +++ b/src/test/resources/changelog-test.sql @@ -3,35 +3,36 @@ --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 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 SEQUENCE IF EXISTS USER_BELONG_ID_SEQ; + DROP TABLE IF EXISTS ACTIVITY; -DROP - SEQUENCE IF EXISTS ACTIVITY_ID_SEQ; +DROP SEQUENCE IF EXISTS ACTIVITY_ID_SEQ; + DROP TABLE IF EXISTS TASK; -DROP - SEQUENCE IF EXISTS TASK_ID_SEQ; +DROP SEQUENCE IF EXISTS TASK_ID_SEQ; + DROP TABLE IF EXISTS SPRINT; -DROP - SEQUENCE IF EXISTS SPRINT_ID_SEQ; +DROP SEQUENCE IF EXISTS SPRINT_ID_SEQ; + DROP TABLE IF EXISTS PROJECT; -DROP - SEQUENCE IF EXISTS PROJECT_ID_SEQ; +DROP SEQUENCE IF EXISTS PROJECT_ID_SEQ; + DROP TABLE IF EXISTS REFERENCE; -DROP - SEQUENCE IF EXISTS REFERENCE_ID_SEQ; +DROP SEQUENCE IF EXISTS REFERENCE_ID_SEQ; + DROP TABLE IF EXISTS ATTACHMENT; -DROP - SEQUENCE IF EXISTS ATTACHMENT_ID_SEQ; +DROP SEQUENCE IF EXISTS ATTACHMENT_ID_SEQ; + DROP TABLE IF EXISTS USERS; -DROP - SEQUENCE IF EXISTS USERS_ID_SEQ; +DROP SEQUENCE IF EXISTS USERS_ID_SEQ; create table PROJECT ( @@ -105,8 +106,8 @@ create table PROFILE create table CONTACT ( - ID bigint not null, - CODE varchar(32) not null, + ID bigint not null, + CODE varchar(32) not null, CONTACT_VALUE varchar(256) not null, primary key (ID, CODE), constraint FK_CONTACT_PROFILE foreign key (ID) references PROFILE (ID) on delete cascade @@ -253,14 +254,10 @@ alter table SPRINT add column CODE 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; +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 @@ -320,7 +317,7 @@ values ('todo', 'ToDo', 3, 'in_progress,canceled|'), ('done', 'Done', 3, 'canceled|'), ('canceled', 'Canceled', 3, null); ---changeset ishlyakhtenkov:change_UK_USER_BELONG +-- --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) where ENDPOINT is null; diff --git a/src/test/resources/data-test.sql b/src/test/resources/data-test.sql index ba459710c..b855ecd48 100644 --- a/src/test/resources/data-test.sql +++ b/src/test/resources/data-test.sql @@ -1,32 +1,27 @@ --------- users ---------------------- -delete -from USER_ROLE; -delete -from CONTACT; -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; +delete from USER_ROLE; +delete from CONTACT; +delete from PROFILE; + +delete from ACTIVITY; +alter sequence ACTIVITY_ID_SEQ restart with 1; +-- ALTER TABLE ACTIVITY ALTER COLUMN ID RESTART WITH 1; + +delete from TASK; +alter sequence TASK_ID_SEQ restart with 1; +-- ALTER TABLE TASK ALTER COLUMN ID RESTART WITH 1; + +delete from SPRINT; +alter sequence SPRINT_ID_SEQ restart with 1; +-- ALTER TABLE SPRINT ALTER COLUMN ID RESTART WITH 1; + +delete from PROJECT; +alter sequence PROJECT_ID_SEQ restart with 1; +-- ALTER TABLE PROJECT ALTER COLUMN ID RESTART WITH 1; + +delete from USERS; +alter sequence USERS_ID_SEQ restart with 1; +-- ALTER TABLE USERS ALTER COLUMN ID RESTART WITH 1; insert into USERS (EMAIL, PASSWORD, FIRST_NAME, LAST_NAME, DISPLAY_NAME) values ('user@gmail.com', '{noop}password', 'userFirstName', 'userLastName', 'userDisplayName'),