From 6b4c086e77f89b9d6cfbabba271c5938df283ff9 Mon Sep 17 00:00:00 2001 From: vvorozheykin Date: Sat, 20 Jun 2026 18:50:06 +0300 Subject: [PATCH 1/6] =?UTF-8?q?1.=20=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D1=81=D0=BE=D1=86=D0=B8=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D1=81=D0=B5=D1=82=D0=B8:=20vk,=20yandex.=20Easy=20tas?= =?UTF-8?q?k=202.=20=D0=92=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D1=87=D1=83=D0=B2=D1=81=D1=82=D0=B2=D0=B8=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=203.=20=D0=92=D1=8B=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=20=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=B0?= =?UTF-8?q?=20com.javarush.jira.bugtracking.attachment.FileUtil#upload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/_application-prod.yaml | 8 ++-- pom.xml | 5 ++ resources/static/fontawesome/css/all.css | 12 ----- resources/view/login.html | 8 ---- resources/view/unauth/register.html | 8 ---- .../jira/bugtracking/attachment/FileUtil.java | 20 ++++---- .../handler/VkOAuth2UserDataHandler.java | 35 -------------- .../handler/YandexOAuth2UserDataHandler.java | 21 --------- src/main/resources/application.yaml | 46 +++++-------------- src/test/resources/application-test.yaml | 17 ++++++- 10 files changed, 43 insertions(+), 137 deletions(-) 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/config/_application-prod.yaml b/config/_application-prod.yaml index 67fd8b7c2..c7bceee00 100644 --- a/config/_application-prod.yaml +++ b/config/_application-prod.yaml @@ -3,7 +3,7 @@ app: host-url: http://localhost spring: datasource: - url: jdbc:postgresql://localhost:5432/jira - username: jira - password: JiraRush - + url: ${DATABASE_URL} + username: ${DATABASE_USERNAME} + password: ${DATABASE_PASSWORD} + \ No newline at end of file diff --git a/pom.xml b/pom.xml index f6c152c68..1ea9d4410 100644 --- a/pom.xml +++ b/pom.xml @@ -142,6 +142,11 @@ junit-platform-launcher test + + com.h2database + h2 + test + diff --git a/resources/static/fontawesome/css/all.css b/resources/static/fontawesome/css/all.css index af5980828..6a16cc2f0 100644 --- a/resources/static/fontawesome/css/all.css +++ b/resources/static/fontawesome/css/all.css @@ -8603,10 +8603,6 @@ readers do not read off random characters that represent icons */ content: "\f3e8"; } -.fa-vk:before { - content: "\f189"; -} - .fa-untappd:before { content: "\f405"; } @@ -9955,10 +9951,6 @@ readers do not read off random characters that represent icons */ content: "\f3bc"; } -.fa-yandex:before { - content: "\f413"; -} - .fa-readme:before { content: "\f4d5"; } @@ -10183,10 +10175,6 @@ 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/bugtracking/attachment/FileUtil.java b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java index 6cffbe175..c7bf0250f 100644 --- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java +++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java @@ -7,10 +7,7 @@ import org.springframework.core.io.UrlResource; import org.springframework.web.multipart.MultipartFile; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; +import java.io.*; import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; @@ -25,14 +22,13 @@ public static void upload(MultipartFile multipartFile, String directoryPath, Str throw new IllegalRequestDataException("Select a file to upload."); } - File dir = new File(directoryPath); - if (dir.exists() || dir.mkdirs()) { - File file = new File(directoryPath + fileName); - try (OutputStream outStream = new FileOutputStream(file)) { - outStream.write(multipartFile.getBytes()); - } catch (IOException ex) { - throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename()); - } + Path dirPath = Paths.get(directoryPath); + try { + Files.createDirectories(dirPath); + Path filePath = dirPath.resolve(fileName); + Files.write(filePath, multipartFile.getBytes()); + } catch (IOException e) { + throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename()); } } diff --git a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java b/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java deleted file mode 100644 index e8e05be05..000000000 --- a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.javarush.jira.login.internal.sociallogin.handler; - -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; - -@Component("vk") -public class VkOAuth2UserDataHandler implements OAuth2UserDataHandler { - @Override - public String getFirstName(OAuth2UserData oAuth2UserData) { - return getAttribute(oAuth2UserData, "first_name"); - } - - @Override - public String getLastName(OAuth2UserData oAuth2UserData) { - return getAttribute(oAuth2UserData, "last_name"); - } - - @Override - public String getEmail(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("email"); - } - - private String getAttribute(OAuth2UserData oAuth2UserData, String name) { - List> attributesResponse = oAuth2UserData.getData("response"); - if (attributesResponse != null) { - Map attributes = attributesResponse.get(0); - if (attributes != null) { - return (String) attributes.get(name); - } - } - return null; - } -} diff --git a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/YandexOAuth2UserDataHandler.java b/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/YandexOAuth2UserDataHandler.java deleted file mode 100644 index e8ea1ac1d..000000000 --- a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/YandexOAuth2UserDataHandler.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.javarush.jira.login.internal.sociallogin.handler; - -import org.springframework.stereotype.Component; - -@Component("yandex") -public class YandexOAuth2UserDataHandler implements OAuth2UserDataHandler { - @Override - public String getFirstName(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("first_name"); - } - - @Override - public String getLastName(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("last_name"); - } - - @Override - public String getEmail(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("default_email"); - } -} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7fcba1570..312e213b0 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -26,9 +26,9 @@ spring: # https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc jdbc.batch_size: 20 datasource: - url: jdbc:postgresql://localhost:5432/jira - username: jira - password: JiraRush + url: ${DATABASE_URL} + username: ${DATABASE_USERNAME} + password: ${DATABASE_PASSWORD} liquibase: changeLog: "classpath:db/changelog.sql" @@ -51,48 +51,24 @@ spring: client: registration: github: - client-id: 3d0d8738e65881fff266 - client-secret: 0f97031ce6178b7dfb67a6af587f37e222a16120 + client-id: ${OAUTH2_GITHUB_CLIENT_ID} + client-secret: ${OAUTH2_GITHUB_CLIENT_SECRET} scope: - email google: - client-id: 329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com - client-secret: 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 + client-id: ${OAUTH2_GOOGLE_CLIENT_ID} + client-secret: ${OAUTH2_GOOGLE_CLIENT_SECRET} scope: - email - profile - vk: - client-id: 51562377 - client-secret: jNM1YHQy1362Mqs49wUN - client-name: Vkontakte - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - client-authentication-method: client_secret_post - authorization-grant-type: authorization_code - scope: email - yandex: - client-id: 2f3395214ba84075956b76a34b231985 - client-secret: ed236c501e444a609b0f419e5e88f1e1 - client-name: Yandex - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - authorization-grant-type: authorization_code gitlab: - client-id: b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494 - client-secret: e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004 + client-id: ${OAUTH2_GITLAB_CLIENT_ID} + client-secret: ${OAUTH2_GITLAB_CLIENT_SECRET} client-name: GitLab redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" authorization-grant-type: authorization_code scope: read_user provider: - vk: - authorization-uri: https://oauth.vk.com/authorize - token-uri: https://oauth.vk.com/access_token - user-info-uri: https://api.vk.com/method/users.get?v=8.1 - user-name-attribute: response - yandex: - authorization-uri: https://oauth.yandex.ru/authorize - token-uri: https://oauth.yandex.ru/token - user-info-uri: https://login.yandex.ru/info - user-name-attribute: login gitlab: authorization-uri: https://gitlab.com/oauth/authorize token-uri: https://gitlab.com/oauth/token @@ -111,8 +87,8 @@ spring: enable: true auth: true host: smtp.gmail.com - username: jira4jr@gmail.com - password: zdfzsrqvgimldzyj + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} port: 587 thymeleaf.check-template-location: false diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 51137fd06..0d105b56e 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,8 +1,21 @@ -spring.cache.type: none spring: + cache: + type: none # ← Исправлено: внутри spring init: mode: always datasource: url: jdbc:postgresql://localhost:5433/jira-test username: jira - password: JiraRush \ No newline at end of file + password: JiraRush + mail: # ← Исправлено: на одном уровне с datasource + host: smtp.gmail.com + port: 587 + username: test@gmail.ru + password: test123 + protocol: smtp + properties: + mail: + smtp: + auth: true + starttls: + enable: true \ No newline at end of file From 0c83f0220b62cc8d769c77fd3e339121e3ead860 Mon Sep 17 00:00:00 2001 From: vvorozheykin Date: Sun, 21 Jun 2026 17:58:02 +0300 Subject: [PATCH 2/6] =?UTF-8?q?=D0=9D=D0=B0=D0=BF=D0=B8=D1=81=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B2=D1=81=D0=B5=D1=85=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D1=87?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=BE=D0=BB=D0=BB=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=20ProfileRestController?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/ProfileRestControllerTest.java | 90 +++++++++++++++++++ .../profile/internal/web/ProfileTestData.java | 3 + 2 files changed, 93 insertions(+) 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..369c64482 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,98 @@ package com.javarush.jira.profile.internal.web; import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.common.BaseHandler; +import com.javarush.jira.profile.ProfileTo; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import static com.javarush.jira.common.util.JsonUtil.writeValue; +import static com.javarush.jira.login.internal.web.UserTestData.USER_MAIL; +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 { + private static final String REST_URL_PROFILE = BaseHandler.REST_URL + "/profile"; + @Test + void getProfileUnauthorized() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL_PROFILE)) + .andDo(print()) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void getProfile() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL_PROFILE)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(PROFILE_TO_MATCHER.contentJson(USER_PROFILE_TO)); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateProfile() throws Exception { + ProfileTo updatedTo = getUpdatedTo(); + perform(MockMvcRequestBuilders.put(REST_URL_PROFILE) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isNoContent()); + + perform(MockMvcRequestBuilders.get(REST_URL_PROFILE)) + .andExpect(status().isOk()) + .andExpect(PROFILE_TO_MATCHER.contentJson(updatedTo)); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateProfileWithNoBody() throws Exception { + perform(MockMvcRequestBuilders.put(REST_URL_PROFILE) + .contentType(MediaType.APPLICATION_JSON) + .content("{}")) + .andDo(print()) + .andExpect(status().isNoContent()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateProfileWithUnknownContactTo() throws Exception { + ProfileTo profileToWithUnknownContactTo = getWithUnknownContactTo(); + + perform(MockMvcRequestBuilders.put(REST_URL_PROFILE) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(profileToWithUnknownContactTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateProfileWithUnknownNotificationTo() throws Exception { + ProfileTo profileToWithUnknownNotificationTo = getWithUnknownNotificationTo(); + + perform(MockMvcRequestBuilders.put(REST_URL_PROFILE) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(profileToWithUnknownNotificationTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateProfileWithInvalidData() throws Exception { + ProfileTo invalidTo = getInvalidTo(); + + perform(MockMvcRequestBuilders.put(REST_URL_PROFILE) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(invalidTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } } \ 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..9cb43496e 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 @@ -13,6 +13,9 @@ public class ProfileTestData { public static MatcherFactory.Matcher PROFILE_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(Profile.class, "user"); + public static MatcherFactory.Matcher PROFILE_TO_MATCHER = + MatcherFactory.usingIgnoringFieldsComparator(ProfileTo.class, "id"); + public static ProfileTo USER_PROFILE_TO = new ProfileTo(null, Set.of("assigned", "overdue", "deadline"), Set.of(new ContactTo("skype", "userSkype"), From 2772b9eaff32065a6f694b3db86341c923d3da6e Mon Sep 17 00:00:00 2001 From: vvorozheykin Date: Sun, 21 Jun 2026 19:18:01 +0300 Subject: [PATCH 3/6] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BF=D0=BE=D0=B4=D1=81=D1=87=D0=B5=D1=82=20=D0=B2?= =?UTF-8?q?=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=B8=20=D1=81=D0=BA=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BA=D0=BE=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=85=D0=BE=D0=B4=D0=B8=D0=BB=D0=B0=D1=81=D1=8C=20?= =?UTF-8?q?=D0=B2=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B5=20=D0=B8=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++- config/_application-prod.yaml | 1 - .../bugtracking/task/ActivityRepository.java | 7 ++++ .../jira/bugtracking/task/TaskService.java | 36 +++++++++++++++++++ src/main/resources/db/changelog.sql | 19 +++++----- .../java/com/javarush/jira/BaseTests.java | 2 +- .../bugtracking/task/TaskServiceTest.java | 29 +++++++++++++++ src/test/resources/application-test.yaml | 4 +-- src/test/resources/data.sql | 5 +++ 9 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 src/test/java/com/javarush/jira/bugtracking/task/TaskServiceTest.java diff --git a/README.md b/README.md index 719b268f5..07556c91c 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,9 @@ - https://habr.com/ru/articles/259055/ Список выполненных задач: -... \ No newline at end of file +2. Удалить социальные сети: vk, yandex. +3. Вынести чувствительную информацию в отдельный проперти файл +3. +5. Написать тесты для всех публичных методов контроллера ProfileRestController +6. Сделать рефакторинг метода com.javarush.jira.bugtracking.attachment.FileUtil#upload +8. Добавить подсчет времени сколько задача находилась в работе и тестировании \ No newline at end of file diff --git a/config/_application-prod.yaml b/config/_application-prod.yaml index c7bceee00..6efe26dc9 100644 --- a/config/_application-prod.yaml +++ b/config/_application-prod.yaml @@ -6,4 +6,3 @@ spring: url: ${DATABASE_URL} username: ${DATABASE_USERNAME} password: ${DATABASE_PASSWORD} - \ No newline at end of file diff --git a/src/main/java/com/javarush/jira/bugtracking/task/ActivityRepository.java b/src/main/java/com/javarush/jira/bugtracking/task/ActivityRepository.java index 3ce8a9386..aa6f710e2 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/ActivityRepository.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/ActivityRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.List; @Transactional(readOnly = true) @@ -13,4 +14,10 @@ public interface ActivityRepository extends BaseRepository { @Query("SELECT a FROM Activity a JOIN FETCH a.author WHERE a.taskId =:taskId AND a.comment IS NOT NULL ORDER BY a.updated DESC") List findAllComments(long taskId); + + @Query("SELECT a FROM Activity a JOIN FETCH a.author WHERE a.taskId =:taskId AND a.statusCode=:statusCode ORDER BY a.updated ASC") + List findByTaskIdAndStatusCodeOrderByUpdatedAsc(long taskId, String statusCode); + + @Query("SELECT a FROM Activity a JOIN FETCH a.author WHERE a.taskId =:taskId AND a.statusCode=:statusCode AND a.updated > :updatedAfter ORDER BY a.updated ASC") + List findByTaskIdAndStatusCodeAndUpdatedAfterOrderByUpdatedAsc(long taskId, String statusCode, LocalDateTime updatedAfter); } 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..4e128b970 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java @@ -19,8 +19,10 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; +import java.time.Duration; import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; import static com.javarush.jira.bugtracking.ObjectType.TASK; import static com.javarush.jira.bugtracking.task.TaskUtil.fillExtraFields; @@ -33,12 +35,17 @@ 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"; + static final String TASK_STATUS_IN_PROGRESS = "in_progress"; + static final String TASK_STATUS_READY_FOR_REVIEW = "ready_for_review"; + static final String TASK_STATUS_DONE = "done"; + private final Handlers.TaskExtHandler handler; private final Handlers.ActivityHandler activityHandler; private final TaskFullMapper fullMapper; private final SprintRepository sprintRepository; private final TaskExtMapper extMapper; private final UserBelongRepository userBelongRepository; + private final ActivityRepository activityRepository; @Transactional public void changeStatus(long taskId, String statusCode) { @@ -132,6 +139,16 @@ public void unAssign(long id, String userType, long userId) { assignment.setEndpoint(LocalDateTime.now()); } + @Transactional(readOnly = true) + public Duration getTimeInProgress(long id) { + return getTimeBetweenStatuses(id, TASK_STATUS_IN_PROGRESS, TASK_STATUS_READY_FOR_REVIEW); + } + + @Transactional(readOnly = true) + public Duration getTimeInTesting(long id) { + return getTimeBetweenStatuses(id, TASK_STATUS_READY_FOR_REVIEW, TASK_STATUS_DONE); + } + private void checkAssignmentActionPossible(long id, String userType, boolean assign) { Assert.notNull(userType, "userType must not be null"); Task task = handler.getRepository().getExisted(id); @@ -140,4 +157,23 @@ private void checkAssignmentActionPossible(long id, String userType, boolean ass throw new DataConflictException(String.format(assign ? CANNOT_ASSIGN : CANNOT_UN_ASSIGN, userType, task.getStatusCode())); } } + + private Duration getTimeBetweenStatuses(long id, String statusFrom, String statusTo) { + Optional timeFrom = activityRepository.findByTaskIdAndStatusCodeOrderByUpdatedAsc(id, statusFrom).stream() + .findFirst() + .map(Activity::getUpdated); + if (!timeFrom.isPresent()) { + return null; + } + + Optional timeTo = activityRepository.findByTaskIdAndStatusCodeAndUpdatedAfterOrderByUpdatedAsc(id, statusTo, timeFrom.get()).stream() + .findFirst() + .map(Activity::getUpdated); + + if (timeTo.isPresent()) { + Duration duration = Duration.between(timeFrom.get(), timeTo.get()); + return duration; + } + return null; + } } diff --git a/src/main/resources/db/changelog.sql b/src/main/resources/db/changelog.sql index 68591336d..46c8edec2 100644 --- a/src/main/resources/db/changelog.sql +++ b/src/main/resources/db/changelog.sql @@ -107,7 +107,7 @@ create table CONTACT ( ID bigint not null, CODE varchar(32) not null, - VALUE varchar(256) not null, + "value" varchar(256) not null, primary key (ID, CODE), constraint FK_CONTACT_PROFILE foreign key (ID) references PROFILE (ID) on delete cascade ); @@ -281,17 +281,16 @@ values ('todo', 'ToDo', 3, 'in_progress,canceled'), --changeset gkislin:users_add_on_delete_cascade -alter table ACTIVITY - drop constraint FK_ACTIVITY_USERS, - add constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID) 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, - add constraint FK_USER_BELONG foreign key (USER_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, - add constraint FK_ATTACHMENT 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 diff --git a/src/test/java/com/javarush/jira/BaseTests.java b/src/test/java/com/javarush/jira/BaseTests.java index b6ed2d1aa..2929012f1 100644 --- a/src/test/java/com/javarush/jira/BaseTests.java +++ b/src/test/java/com/javarush/jira/BaseTests.java @@ -5,5 +5,5 @@ @SpringBootTest @ActiveProfiles("test") -abstract class BaseTests { +public abstract class BaseTests { } diff --git a/src/test/java/com/javarush/jira/bugtracking/task/TaskServiceTest.java b/src/test/java/com/javarush/jira/bugtracking/task/TaskServiceTest.java new file mode 100644 index 000000000..cf90947ec --- /dev/null +++ b/src/test/java/com/javarush/jira/bugtracking/task/TaskServiceTest.java @@ -0,0 +1,29 @@ +package com.javarush.jira.bugtracking.task; + +import com.javarush.jira.BaseTests; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +class TaskServiceTest extends BaseTests { + + @Autowired + private TaskService taskService; + + @Test + void getTimeInProgress() { + Duration inProgress = taskService.getTimeInProgress(6L); + assertNotNull(inProgress); + assertEquals(781200, inProgress.getSeconds()); + } + + @Test + void getTimeInTesting() { + Duration inTesting = taskService.getTimeInTesting(6L); + assertNotNull(inTesting); + assertEquals(479100, inTesting.getSeconds()); + } +} \ No newline at end of file diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 0d105b56e..9d1da069f 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,13 +1,13 @@ spring: cache: - type: none # ← Исправлено: внутри spring + type: none init: mode: always datasource: url: jdbc:postgresql://localhost:5433/jira-test username: jira password: JiraRush - mail: # ← Исправлено: на одном уровне с datasource + mail: host: smtp.gmail.com port: 587 username: test@gmail.ru diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 5087dbddc..25bf86581 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -96,3 +96,8 @@ values (1, 2, 2, 'task_developer', '2023-06-14 08:35:10', '2023-06-14 08:55:00') (2, 2, 2, 'task_developer', '2023-06-08 07:10:00', null), (2, 2, 1, 'task_developer', '2023-06-09 14:48:00', null), (2, 2, 1, 'task_tester', '2023-06-10 16:37:00', null); + +insert into ACTIVITY ( AUTHOR_ID, TASK_ID, UPDATED, STATUS_CODE ) +values (2, 6, '2026-06-01 09:00:10', 'in_progress'), + (2, 6, '2026-06-10 10:00:10', 'ready_for_review'), + (2, 6, '2026-06-15 23:05:10', 'done'); From d20483c6fcc96ca701a8dcc8e029ac61b91d79df Mon Sep 17 00:00:00 2001 From: vvorozheykin Date: Mon, 22 Jun 2026 11:21:10 +0300 Subject: [PATCH 4/6] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84=D1=83=D0=BD?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB:=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=D0=B2=20=D0=BA=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87?= =?UTF-8?q?=D0=B5=20(REST=20API=20+=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BD=D0=B0=20=D1=81=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=B8=D1=81=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jira/bugtracking/task/TaskController.java | 46 +++++++ .../jira/bugtracking/task/TaskRepository.java | 4 + .../jira/bugtracking/task/TaskService.java | 118 +++++++++++++++++- .../task/exception/ValidationException.java | 7 ++ 4 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/javarush/jira/bugtracking/task/exception/ValidationException.java 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..628432047 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java @@ -13,6 +13,7 @@ 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; @@ -23,6 +24,7 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.Set; import static com.javarush.jira.common.BaseHandler.createdResponse; @@ -151,6 +153,50 @@ public void delete(@PathVariable long id) { activityService.delete(id); } + @GetMapping("/{id}/tags") + public Set getTags(@PathVariable(name = "id") Long taskId) { + return taskService.getTaskTags(taskId); + } + + @PostMapping("/{id}/tags") + @ResponseStatus(HttpStatus.CREATED) + public Set addTag( + @PathVariable(name = "id") Long taskId, + @Valid @RequestParam String tag) { + return taskService.addTagToTask(taskId, tag); + } + + @PostMapping("/{id}/tags/batch") + @ResponseStatus(HttpStatus.CREATED) + public Set addTags( + @PathVariable(name = "id") Long taskId, + @Valid @RequestBody Set<@Size(min = 2, max = 32) String> tags) { + return taskService.addTagsToTask(taskId, tags); + } + + @DeleteMapping("/{id}/tags") + public Set deleteTag( + @PathVariable(name = "id") Long taskId, + @RequestParam String tag) { + return taskService.removeTagFromTask(taskId, tag); + } + + @DeleteMapping("/{id}/tags/batch") + public Set deleteTags( + @PathVariable(name = "id") Long taskId, + @RequestBody Set<@Size(min = 2, max = 32) String> tags) { + log.info("Remove tags {} from task {}", tags, taskId); + return taskService.removeTagsFromTask(taskId, tags); + } + + @PutMapping("/{id}/tags/batch") + public Set replaceTags( + @PathVariable(name = "id") Long taskId, + @Valid @RequestBody Set<@Size(min = 2, max = 32) String> tags) { + log.info("Replace tags for task {} with {}", taskId, tags); + return taskService.replaceTaskTags(taskId, tags); + } + private record TaskTreeNode(TaskTo taskTo, List subNodes) implements ITreeNode { public TaskTreeNode(TaskTo taskTo) { this(taskTo, new LinkedList<>()); diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java index 982c4be8b..691f4855e 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Optional; +import java.util.Set; @Transactional(readOnly = true) public interface TaskRepository extends BaseRepository { @@ -37,4 +38,7 @@ WITH RECURSIVE task_with_subtasks AS ( WHERE id IN (SELECT child FROM task_with_subtasks) """, nativeQuery = true) void setTaskAndSubTasksSprint(long taskId, Long sprintId); + + @Query("SELECT t.tags FROM Task t WHERE t.id =:id") + Set getTaskTags(long 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 4e128b970..cae5d5c1c 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java @@ -5,6 +5,7 @@ import com.javarush.jira.bugtracking.UserBelongRepository; import com.javarush.jira.bugtracking.sprint.Sprint; import com.javarush.jira.bugtracking.sprint.SprintRepository; +import com.javarush.jira.bugtracking.task.exception.ValidationException; import com.javarush.jira.bugtracking.task.mapper.TaskExtMapper; import com.javarush.jira.bugtracking.task.mapper.TaskFullMapper; import com.javarush.jira.bugtracking.task.to.TaskToExt; @@ -15,20 +16,25 @@ import com.javarush.jira.login.AuthUser; import com.javarush.jira.ref.RefType; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import java.time.Duration; import java.time.LocalDateTime; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import static com.javarush.jira.bugtracking.ObjectType.TASK; import static com.javarush.jira.bugtracking.task.TaskUtil.fillExtraFields; import static com.javarush.jira.bugtracking.task.TaskUtil.makeActivity; import static com.javarush.jira.ref.ReferenceService.getRefTo; +@Slf4j @Service @RequiredArgsConstructor public class TaskService { @@ -46,6 +52,7 @@ public class TaskService { private final TaskExtMapper extMapper; private final UserBelongRepository userBelongRepository; private final ActivityRepository activityRepository; + private final TaskRepository taskRepository; @Transactional public void changeStatus(long taskId, String statusCode) { @@ -162,7 +169,7 @@ private Duration getTimeBetweenStatuses(long id, String statusFrom, String statu Optional timeFrom = activityRepository.findByTaskIdAndStatusCodeOrderByUpdatedAsc(id, statusFrom).stream() .findFirst() .map(Activity::getUpdated); - if (!timeFrom.isPresent()) { + if (timeFrom.isEmpty()) { return null; } @@ -170,10 +177,111 @@ private Duration getTimeBetweenStatuses(long id, String statusFrom, String statu .findFirst() .map(Activity::getUpdated); - if (timeTo.isPresent()) { - Duration duration = Duration.between(timeFrom.get(), timeTo.get()); - return duration; + return timeTo.map(localDateTime -> Duration.between(timeFrom.get(), localDateTime)).orElse(null); + } + + @Transactional(readOnly = true) + public Set getTaskTags(Long taskId) { + return taskRepository.getTaskTags(taskId); + } + + @Transactional + public Set addTagToTask(Long taskId, String tag) { + validateTag(tag); + Task task = handler.get(taskId); + + if (task != null && task.getTags().contains(tag)) { + log.warn("Tag '{}' already exists for task {}", tag, taskId); + return new HashSet<>(task.getTags()); + } + + task.getTags().add(tag); + handler.update(task, taskId); + log.info("Added tag '{}' to task {}", tag, taskId); + return new HashSet<>(task.getTags()); + } + + @Transactional + public Set addTagsToTask(Long taskId, Set tags) { + validateTags(tags); + Task task = handler.get(taskId); + + Set existingTags = task.getTags(); + Set tagsToAdd = tags.stream() + .filter(tag -> !existingTags.contains(tag)) + .collect(Collectors.toSet()); + + if (tagsToAdd.isEmpty()) { + log.warn("No new tags to add for task {}", taskId); + return new HashSet<>(existingTags); + } + + existingTags.addAll(tagsToAdd); + handler.update(task, taskId); + log.info("Added tags {} to task {}", tagsToAdd, taskId); + return new HashSet<>(existingTags); + } + + @Transactional + public Set removeTagFromTask(Long taskId, String tag) { + Task task = handler.get(taskId); + + if (!task.getTags().contains(tag)) { + log.warn("Tag '{}' not found for task {}", tag, taskId); + throw new NotFoundException("Tag '" + tag + "' not found for task " + taskId); + } + + task.getTags().remove(tag); + handler.update(task, taskId); + log.info("Removed tag '{}' from task {}", tag, taskId); + return new HashSet<>(task.getTags()); + } + + @Transactional + public Set removeTagsFromTask(Long taskId, Set tags) { + validateTags(tags); + Task task = handler.get(taskId); + + Set existingTags = task.getTags(); + Set tagsToRemove = tags.stream() + .filter(existingTags::contains) + .collect(Collectors.toSet()); + + if (tagsToRemove.isEmpty()) { + log.warn("No tags to remove for task {}", taskId); + return new HashSet<>(existingTags); + } + + existingTags.removeAll(tagsToRemove); + handler.update(task, taskId); + log.info("Removed tags {} from task {}", tagsToRemove, taskId); + return new HashSet<>(existingTags); + } + + @Transactional + public Set replaceTaskTags(Long taskId, Set newTags) { + validateTags(newTags); + Task task = handler.get(taskId); + + task.setTags(newTags); + handler.update(task, taskId); + log.info("Replaced tags for task {} with {}", taskId, newTags); + return new HashSet<>(newTags); + } + + private void validateTag(String tag) { + if (tag == null || tag.isBlank()) { + throw new ValidationException("Tag cannot be null or blank"); + } + if (tag.length() < 2 || tag.length() > 32) { + throw new ValidationException("Tag must be between 2 and 32 characters"); + } + } + + private void validateTags(Set tags) { + if (tags == null || tags.isEmpty()) { + throw new ValidationException("Tags set cannot be null or empty"); } - return null; + tags.forEach(this::validateTag); } } diff --git a/src/main/java/com/javarush/jira/bugtracking/task/exception/ValidationException.java b/src/main/java/com/javarush/jira/bugtracking/task/exception/ValidationException.java new file mode 100644 index 000000000..83fd8989c --- /dev/null +++ b/src/main/java/com/javarush/jira/bugtracking/task/exception/ValidationException.java @@ -0,0 +1,7 @@ +package com.javarush.jira.bugtracking.task.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } +} From 1a5503bac235cda68a08a1997eb01f441477219f Mon Sep 17 00:00:00 2001 From: vvorozheykin Date: Mon, 22 Jun 2026 11:27:20 +0300 Subject: [PATCH 5/6] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84=D1=83=D0=BD?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB:=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=D0=B2=20=D0=BA=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87?= =?UTF-8?q?=D0=B5=20(REST=20API=20+=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BD=D0=B0=20=D1=81=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=B8=D1=81=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 07556c91c..b5fd6613a 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Список выполненных задач: 2. Удалить социальные сети: vk, yandex. 3. Вынести чувствительную информацию в отдельный проперти файл -3. 5. Написать тесты для всех публичных методов контроллера ProfileRestController 6. Сделать рефакторинг метода com.javarush.jira.bugtracking.attachment.FileUtil#upload +7. Добавить новый функционал: добавления тегов к задаче (REST API + реализация на сервисе) 8. Добавить подсчет времени сколько задача находилась в работе и тестировании \ No newline at end of file From de80557d93c0030cb8422733328dd633d6b30837 Mon Sep 17 00:00:00 2001 From: vvorozheykin Date: Mon, 22 Jun 2026 12:19:15 +0300 Subject: [PATCH 6/6] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D1=82?= =?UTF-8?q?=D0=B0=D0=BA,=20=D1=87=D1=82=D0=BE=D0=B1=20=D0=B2=D0=BE=20?= =?UTF-8?q?=D0=B2=D1=80=D0=B5=D0=BC=D1=8F=20=D1=82=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BB=D0=B0=D1=81=D1=8C=20in=20memory=20=D0=91=D0=94=20(?= =?UTF-8?q?H2),=20=D0=B0=20=D0=BD=D0=B5=20PostgreSQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + .../javarush/jira/AbstractControllerTest.java | 2 +- src/test/resources/application-test.yaml | 41 ++++-- src/test/resources/data.sql | 128 ++++++++++++++---- src/test/resources/schema.sql | 126 +++++++++++++++++ 5 files changed, 260 insertions(+), 38 deletions(-) create mode 100644 src/test/resources/schema.sql diff --git a/README.md b/README.md index b5fd6613a..5b263d852 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Список выполненных задач: 2. Удалить социальные сети: vk, yandex. 3. Вынести чувствительную информацию в отдельный проперти файл +4. Переделать тесты так, чтоб во время тестов использовалась in memory БД (H2), а не PostgreSQL 5. Написать тесты для всех публичных методов контроллера ProfileRestController 6. Сделать рефакторинг метода com.javarush.jira.bugtracking.attachment.FileUtil#upload 7. Добавить новый функционал: добавления тегов к задаче (REST API + реализация на сервисе) diff --git a/src/test/java/com/javarush/jira/AbstractControllerTest.java b/src/test/java/com/javarush/jira/AbstractControllerTest.java index 5981bae53..02f21705e 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:schema.sql", "classpath:data.sql"}, config = @SqlConfig(encoding = "UTF-8")) @AutoConfigureMockMvc //https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-mock-environment public abstract class AbstractControllerTest extends BaseTests { diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 9d1da069f..3d7b0c379 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,12 +1,32 @@ spring: - cache: - type: none - init: - mode: always datasource: - url: jdbc:postgresql://localhost:5433/jira-test - username: jira - password: JiraRush + url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;NON_KEYWORDS=VALUE + driver-class-name: org.h2.Driver + username: sa + password: + + liquibase: + enabled: false + + sql: + init: + mode: always + schema-locations: classpath:schema.sql + data-locations: classpath:data.sql + continue-on-error: false + + jpa: + show-sql: true + hibernate: + ddl-auto: none + properties: + hibernate: + format_sql: true + + h2: + console: + enabled: true + mail: host: smtp.gmail.com port: 587 @@ -18,4 +38,9 @@ spring: smtp: auth: true starttls: - enable: true \ No newline at end of file + enable: true + +logging: + level: + org.springframework.jdbc: DEBUG + org.hibernate.SQL: DEBUG \ No newline at end of file diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 25bf86581..847e96aa6 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -1,32 +1,21 @@ ---------- 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 REFERENCE; +DELETE FROM USER_BELONG; +DELETE FROM ACTIVITY; +DELETE FROM TASK; +DELETE FROM SPRINT; +DELETE FROM PROJECT; +DELETE FROM CONTACT; +DELETE FROM PROFILE; +DELETE FROM USER_ROLE; +DELETE FROM USERS; -delete -from USERS; -alter -sequence USERS_ID_SEQ restart with 1; +ALTER TABLE USERS ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE PROJECT ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE SPRINT ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE TASK ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE ACTIVITY ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE USER_BELONG ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE REFERENCE 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'), @@ -48,7 +37,7 @@ insert into PROFILE (ID, LAST_FAILED_LOGIN, LAST_LOGIN, MAIL_NOTIFICATIONS) values (1, null, null, 49), (2, null, null, 14); -insert into CONTACT (ID, CODE, VALUE) +insert into CONTACT (ID, CODE, "VALUE") values (1, 'skype', 'userSkype'), (1, 'mobile', '+01234567890'), (1, 'website', 'user.com'), @@ -101,3 +90,84 @@ insert into ACTIVITY ( AUTHOR_ID, TASK_ID, UPDATED, STATUS_CODE ) values (2, 6, '2026-06-01 09:00:10', 'in_progress'), (2, 6, '2026-06-10 10:00:10', 'ready_for_review'), (2, 6, '2026-06-15 23:05:10', 'done'); + +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); + +-- 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); + +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); + diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 000000000..d9ce9929d --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,126 @@ +-- Таблица пользователей +CREATE TABLE IF NOT EXISTS USERS ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + EMAIL VARCHAR(255) NOT NULL UNIQUE, + PASSWORD VARCHAR(255) NOT NULL, + FIRST_NAME VARCHAR(100), + LAST_NAME VARCHAR(100), + DISPLAY_NAME VARCHAR(255), + ENABLED BOOLEAN DEFAULT TRUE, + REGISTERED TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + STARTPOINT TIMESTAMP, + ENDPOINT TIMESTAMP +); + +-- Таблица ролей пользователей +CREATE TABLE IF NOT EXISTS USER_ROLE ( + USER_ID BIGINT NOT NULL, + ROLE INTEGER NOT NULL, + PRIMARY KEY (USER_ID, ROLE), + FOREIGN KEY (USER_ID) REFERENCES USERS(ID) ON DELETE CASCADE +); + +-- Таблица профилей +CREATE TABLE IF NOT EXISTS PROFILE ( + ID BIGINT PRIMARY KEY, + LAST_FAILED_LOGIN TIMESTAMP, + LAST_LOGIN TIMESTAMP, + MAIL_NOTIFICATIONS INTEGER, + FOREIGN KEY (ID) REFERENCES USERS(ID) ON DELETE CASCADE +); + +-- Таблица контактов +CREATE TABLE IF NOT EXISTS CONTACT ( + ID BIGINT NOT NULL, + CODE VARCHAR(50) NOT NULL, + "VALUE" VARCHAR(255) NOT NULL, + PRIMARY KEY (ID, CODE), + FOREIGN KEY (ID) REFERENCES USERS(ID) ON DELETE CASCADE +); + +-- Таблица проектов +CREATE TABLE IF NOT EXISTS PROJECT ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + CODE VARCHAR(50) NOT NULL UNIQUE, + TITLE VARCHAR(255) NOT NULL, + DESCRIPTION TEXT, + TYPE_CODE VARCHAR(50) NOT NULL, + PARENT_ID BIGINT, + STARTPOINT TIMESTAMP, + ENDPOINT TIMESTAMP, + FOREIGN KEY (PARENT_ID) REFERENCES PROJECT(ID) +); + +-- Таблица спринтов +CREATE TABLE IF NOT EXISTS SPRINT ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + STATUS_CODE VARCHAR(50) NOT NULL, + STARTPOINT TIMESTAMP, + ENDPOINT TIMESTAMP, + CODE VARCHAR(50) NOT NULL, + PROJECT_ID BIGINT NOT NULL, + FOREIGN KEY (PROJECT_ID) REFERENCES PROJECT(ID) ON DELETE CASCADE, + UNIQUE (PROJECT_ID, CODE) +); + +-- Таблица задач +CREATE TABLE IF NOT EXISTS TASK ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + TITLE VARCHAR(255) NOT NULL, + TYPE_CODE VARCHAR(50) NOT NULL, + STATUS_CODE VARCHAR(50) NOT NULL, + PROJECT_ID BIGINT NOT NULL, + SPRINT_ID BIGINT, + STARTPOINT TIMESTAMP, + ENDPOINT TIMESTAMP DEFAULT NULL, + PARENT_ID BIGINT DEFAULT NULL, + FOREIGN KEY (PROJECT_ID) REFERENCES PROJECT(ID) ON DELETE CASCADE, + FOREIGN KEY (SPRINT_ID) REFERENCES SPRINT(ID), + FOREIGN KEY (PARENT_ID) REFERENCES TASK(ID) +); + +-- Таблица активности +CREATE TABLE IF NOT EXISTS ACTIVITY ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + AUTHOR_ID BIGINT NOT NULL, + TASK_ID BIGINT NOT NULL, + UPDATED TIMESTAMP NOT NULL, + COMMENT TEXT, + TITLE VARCHAR(255), + DESCRIPTION TEXT, + ESTIMATE INTEGER, + TYPE_CODE VARCHAR(50), + STATUS_CODE VARCHAR(50), + PRIORITY_CODE VARCHAR(50), + FOREIGN KEY (AUTHOR_ID) REFERENCES USERS(ID) ON DELETE CASCADE, + FOREIGN KEY (TASK_ID) REFERENCES TASK(ID) ON DELETE CASCADE +); + +-- Таблица принадлежности пользователей к задачам +CREATE TABLE IF NOT EXISTS USER_BELONG ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + OBJECT_ID BIGINT NOT NULL, + OBJECT_TYPE INTEGER NOT NULL, + USER_ID BIGINT NOT NULL, + USER_TYPE_CODE VARCHAR(50) NOT NULL, + STARTPOINT TIMESTAMP, + ENDPOINT TIMESTAMP, + FOREIGN KEY (USER_ID) REFERENCES USERS(ID) ON DELETE CASCADE +); +-- Таблица reference (справочники) +CREATE TABLE IF NOT EXISTS REFERENCE ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + CODE VARCHAR(255) NOT NULL, + TITLE VARCHAR(255), + AUX VARCHAR(255), + REF_TYPE VARCHAR(100), + STARTPOINT TIMESTAMP, + ENDPOINT TIMESTAMP, + UNIQUE (REF_TYPE, CODE) +); +CREATE TABLE IF NOT EXISTS TASK_TAG ( + TASK_ID BIGINT NOT NULL, + TAG VARCHAR(255) NOT NULL, + PRIMARY KEY (TASK_ID, TAG), + FOREIGN KEY (TASK_ID) REFERENCES TASK(ID) ON DELETE CASCADE +); \ No newline at end of file