From fb3ca49d629d2ccdc58db24474301604eaffa925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Fri, 24 Oct 2025 09:27:48 +0300 Subject: [PATCH 01/15] Implemented compose.yaml for the database --- compose.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 compose.yaml diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..24ea6721d --- /dev/null +++ b/compose.yaml @@ -0,0 +1,16 @@ +services: + postgres: + image: postgres:17 + environment: + POSTGRES_DB: db + POSTGRES_PASSWORD: db + POSTGRES_USER: db + TZ: UTC + ports: + - '5432:5432' + volumes: + - postgres_data:/var/lib/postgresql/data # Persists database data + +volumes: + postgres_data: + redis_data: \ No newline at end of file From dbddf40d14e0b5b41cd9a87f8ffbb474d802fb97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Fri, 24 Oct 2025 09:29:28 +0300 Subject: [PATCH 02/15] add dependency for lombok and docker compose --- pom.xml | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f6c152c68..fc1d2b272 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.0.2 + 3.1.12 @@ -21,6 +21,8 @@ 1.5.3.Final UTF-8 UTF-8 + 21 + 21 @@ -40,6 +42,12 @@ org.springframework.boot spring-boot-starter-validation + + org.springframework.boot + spring-boot-docker-compose + runtime + true + @@ -96,7 +104,7 @@ org.projectlombok lombok - true + 1.18.30 org.mapstruct @@ -146,6 +154,29 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + 1.18.30 + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.springframework.boot + spring-boot-configuration-processor + 3.5.5 + + + + org.springframework.boot spring-boot-maven-plugin From 8bace9af10ddd41b09fa1921ba59711a618e1f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:10:22 +0200 Subject: [PATCH 03/15] =?UTF-8?q?2.=20=D0=92=D0=B8=D0=B4=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D1=82=D0=B8=20=D1=81=D0=BE=D1=86=D1=96=D0=B0=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=96=20=D0=BC=D0=B5=D1=80=D0=B5=D0=B6=D1=96:=20vk,=20yandex.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/static/fontawesome/css/all.css | 12 ------------ resources/view/login.html | 8 -------- resources/view/unauth/register.html | 8 -------- src/main/resources/data4dev/data.sql | 4 +--- src/main/resources/db/changelog.sql | 1 - .../jira/profile/internal/web/ProfileTestData.java | 2 -- src/test/resources/data.sql | 3 +-- 7 files changed, 2 insertions(+), 36 deletions(-) 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/resources/data4dev/data.sql b/src/main/resources/data4dev/data.sql index a7d43cbad..4a4bed843 100644 --- a/src/main/resources/data4dev/data.sql +++ b/src/main/resources/data4dev/data.sql @@ -54,9 +54,7 @@ values (1, 'skype', 'userSkype'), (1, 'mobile', '+01234567890'), (1, 'website', 'user.com'), (2, 'github', 'adminGitHub'), - (2, 'tg', 'adminTg'), - (2, 'vk', 'adminVk'); - + (2, 'tg', 'adminTg'); delete from ATTACHMENT; alter diff --git a/src/main/resources/db/changelog.sql b/src/main/resources/db/changelog.sql index 68591336d..1595f97d3 100644 --- a/src/main/resources/db/changelog.sql +++ b/src/main/resources/db/changelog.sql @@ -218,7 +218,6 @@ values ('task', 'Task', 2), ('mobile', 'Mobile', 0), ('phone', 'Phone', 0), ('website', 'Website', 0), - ('vk', 'VK', 0), ('linkedin', 'LinkedIn', 0), ('github', 'GitHub', 0), -- PRIORITY 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..cc0513971 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 @@ -44,7 +44,6 @@ public static ProfileTo getUpdatedTo() { new ContactTo("website", "new.com"), new ContactTo("github", "newGitHub"), new ContactTo("tg", "newTg"), - new ContactTo("vk", "newVk"), new ContactTo("linkedin", "newLinkedin"))); } @@ -57,7 +56,6 @@ public static Profile getUpdated(long id) { new Contact(id, "website", "new.com"), new Contact(id, "github", "newGitHub"), new Contact(id, "tg", "newTg"), - new Contact(id, "vk", "newVk"), new Contact(id, "linkedin", "newLinkedin"))); return profile; } diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 5087dbddc..bc26161e4 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -53,8 +53,7 @@ values (1, 'skype', 'userSkype'), (1, 'mobile', '+01234567890'), (1, 'website', 'user.com'), (2, 'github', 'adminGitHub'), - (2, 'tg', 'adminTg'), - (2, 'vk', 'adminVk'); + (2, 'tg', 'adminTg'); insert into PROJECT (code, title, description, type_code, parent_id) From 951904ab2929285d1ac106fb140e4aa903996239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:11:35 +0200 Subject: [PATCH 04/15] =?UTF-8?q?3.=20=D0=92=D0=B8=D0=BD=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B8=20=D1=87=D1=83=D1=82=D0=BB=D0=B8=D0=B2=D1=83=20=D1=96?= =?UTF-8?q?=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86=D1=96=D1=8E=20=D0=B4?= =?UTF-8?q?=D0=BE=20=D0=BE=D0=BA=D1=80=D0=B5=D0=BC=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=BF=D0=B5=D1=80=D1=82=D1=96=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D1=83:=20=20=20=20=D0=BB=D0=BE=D0=B3=D1=96=D0=BD?= =?UTF-8?q?=20=20=20=20=D0=BF=D0=B0=D1=80=D0=BE=D0=BB=D1=8C=20=D0=91=D0=94?= =?UTF-8?q?=20=20=20=20=D1=96=D0=B4=D0=B5=D0=BD=D1=82=D0=B8=D1=84=D1=96?= =?UTF-8?q?=D0=BA=D0=B0=D1=82=D0=BE=D1=80=D0=B8=20=D0=B4=D0=BB=D1=8F=20OAu?= =?UTF-8?q?th=20=D1=80=D0=B5=D1=94=D1=81=D1=82=D1=80=D0=B0=D1=86=D1=96?= =?UTF-8?q?=D1=97/=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D1=96=D1=97=20=20=20=20=D0=BD=D0=B0=D0=BB=D0=B0=D1=88=D1=82?= =?UTF-8?q?=D1=83=D0=B2=D0=B0=D0=BD=D0=BD=D1=8F=20=D0=BF=D0=BE=D1=88=D1=82?= =?UTF-8?q?=D0=B8=20=20=20=20=D0=97=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D1=8F=20=D1=86=D0=B8=D1=85=20=D0=BF=D1=80=D0=BE=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D1=82=D1=96=20=D0=BF=D0=BE=D0=B2=D0=B8=D0=BD=D0=BD=D1=96?= =?UTF-8?q?=20=D0=B7=D1=87=D0=B8=D1=82=D1=83=D0=B2=D0=B0=D1=82=D0=B8=D1=81?= =?UTF-8?q?=D1=8F=20=D0=BF=D1=80=D0=B8=20=D1=81=D1=82=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D1=96=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B0=20=D0=B7?= =?UTF-8?q?=D1=96=20=D0=B7=D0=BC=D1=96=D0=BD=D0=BD=D0=B8=D1=85=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D1=87=D0=B5=D0=BD=D0=BD=D1=8F=20=D0=BC=D0=B0=D1=88?= =?UTF-8?q?=D0=B8=D0=BD=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + compose.yaml | 11 +++-- pom.xml | 11 +++-- src/main/resources/application.yaml | 66 +++++++++-------------------- 4 files changed, 33 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index cd38e2e7b..7f9709787 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ target logs attachments *.patch +.env diff --git a/compose.yaml b/compose.yaml index 24ea6721d..4c24f9b47 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,15 +2,14 @@ services: postgres: image: postgres:17 environment: - POSTGRES_DB: db - POSTGRES_PASSWORD: db - POSTGRES_USER: db + POSTGRES_DB: ${POSTGRES_DB} # Зчитується з .env файлу + POSTGRES_USER: ${POSTGRES_USER} # Зчитується з .env файлу + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # Зчитується з .env файлу TZ: UTC ports: - '5432:5432' volumes: - - postgres_data:/var/lib/postgresql/data # Persists database data + - postgres_data:/var/lib/postgresql/data # Persists database data volumes: - postgres_data: - redis_data: \ No newline at end of file + postgres_data: \ No newline at end of file diff --git a/pom.xml b/pom.xml index fc1d2b272..e91cfe284 100644 --- a/pom.xml +++ b/pom.xml @@ -21,8 +21,6 @@ 1.5.3.Final UTF-8 UTF-8 - 21 - 21 @@ -42,6 +40,13 @@ org.springframework.boot spring-boot-starter-validation
+ + + io.github.cdimascio + java-dotenv + 5.2.2 + + org.springframework.boot spring-boot-docker-compose @@ -227,4 +232,4 @@ - + \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7fcba1570..d4e3d7f8c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,7 +1,8 @@ -# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# src/main/resources/application.yml + app: host-url: http://localhost:8080 - test-mail: jira4jr@gmail.com + test-mail: ${SPRING_MAIL_USERNAME} templates-update-cache: 5s mail-sending-props: core-pool-size: 8 @@ -14,26 +15,23 @@ spring: show-sql: true open-in-view: false - # validate db by model hibernate: ddl-auto: validate properties: - # http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#configurations hibernate: format_sql: true - default_batch_fetch_size: 20 - # https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc + default-batch-fetch-size: 20 jdbc.batch_size: 20 datasource: - url: jdbc:postgresql://localhost:5432/jira - username: jira - password: JiraRush + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + driver-class-name: org.postgresql.Driver liquibase: changeLog: "classpath:db/changelog.sql" - # Jackson Fields Serialization jackson: visibility: field: any @@ -41,7 +39,6 @@ spring: setter: none is-getter: none - # https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties-cache cache: cache-names: users caffeine.spec: maximumSize=10000,expireAfterAccess=5m @@ -51,48 +48,24 @@ spring: client: registration: github: - client-id: 3d0d8738e65881fff266 - client-secret: 0f97031ce6178b7dfb67a6af587f37e222a16120 + client-id: ${GITHUB_CLIENT_ID} + client-secret: ${GITHUB_CLIENT_SECRET} scope: - email google: - client-id: 329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com - client-secret: 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 + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} scope: - email - profile - vk: - client-id: 51562377 - client-secret: jNM1YHQy1362Mqs49wUN - client-name: Vkontakte - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - client-authentication-method: client_secret_post - authorization-grant-type: authorization_code - scope: email - yandex: - client-id: 2f3395214ba84075956b76a34b231985 - client-secret: ed236c501e444a609b0f419e5e88f1e1 - client-name: Yandex - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - authorization-grant-type: authorization_code gitlab: - client-id: b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494 - client-secret: e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004 + client-id: ${GITLAB_CLIENT_ID} + client-secret: ${GITLAB_CLIENT_SECRET} client-name: GitLab redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" authorization-grant-type: authorization_code scope: read_user provider: - vk: - authorization-uri: https://oauth.vk.com/authorize - token-uri: https://oauth.vk.com/access_token - user-info-uri: https://api.vk.com/method/users.get?v=8.1 - user-name-attribute: response - yandex: - authorization-uri: https://oauth.yandex.ru/authorize - token-uri: https://oauth.yandex.ru/token - user-info-uri: https://login.yandex.ru/info - user-name-attribute: login gitlab: authorization-uri: https://gitlab.com/oauth/authorize token-uri: https://gitlab.com/oauth/token @@ -111,8 +84,8 @@ spring: enable: true auth: true host: smtp.gmail.com - username: jira4jr@gmail.com - password: zdfzsrqvgimldzyj + username: ${SPRING_MAIL_USERNAME} + password: ${SPRING_MAIL_PASSWORD} port: 587 thymeleaf.check-template-location: false @@ -127,11 +100,10 @@ logging: org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: DEBUG server: - # https://springdoc.org/index.html#how-can-i-deploy-springdoc-openapi-ui-behind-a-reverse-proxy forward-headers-strategy: framework servlet: encoding: - charset: UTF-8 # Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly - enabled: true # Enable http encoding support + charset: UTF-8 + enabled: true force: true -springdoc.swagger-ui.path: /doc +springdoc.swagger-ui.path: /doc \ No newline at end of file From e427562168135b625c4cf27d880a7a2c7654eaad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:11:47 +0200 Subject: [PATCH 05/15] =?UTF-8?q?2.=20=D0=92=D0=B8=D0=B4=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D1=82=D0=B8=20=D1=81=D0=BE=D1=86=D1=96=D0=B0=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=96=20=D0=BC=D0=B5=D1=80=D0=B5=D0=B6=D1=96:=20vk,=20yandex.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/VkOAuth2UserDataHandler.java | 35 ------------------- .../handler/YandexOAuth2UserDataHandler.java | 21 ----------- 2 files changed, 56 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/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"); - } -} From 5a8b7d2fe33b1d78bf386f3c50fb679bd6d81b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:09:15 +0200 Subject: [PATCH 06/15] =?UTF-8?q?4.=20=D0=9F=D0=B5=D1=80=D0=B5=D1=80=D0=BE?= =?UTF-8?q?=D0=B1=D0=B8=D1=82=D0=B8=20=D1=82=D0=B5=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D1=82=D0=B0=D0=BA,=20=D1=89=D0=BE=D0=B1=20=D0=BF=D1=96=D0=B4?= =?UTF-8?q?=20=D1=87=D0=B0=D1=81=20=D1=82=D0=B5=D1=81=D1=82=D1=96=D0=B2=20?= =?UTF-8?q?=D0=B2=D0=B8=D0=BA=D0=BE=D1=80=D0=B8=D1=81=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=D1=83=D0=B2=D0=B0=D0=BB=D0=B0=D1=81=D1=8F=20in=20memory=20?= =?UTF-8?q?=D0=91=D0=94=20(H2),=20=D0=B0=20=D0=BD=D0=B5=20PostgreSQL.=20?= =?UTF-8?q?=D0=94=D0=BB=D1=8F=20=D1=86=D1=8C=D0=BE=D0=B3=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=82=D1=80=D1=96=D0=B1=D0=BD=D0=BE=20=D0=B2=D0=B8=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B8=D1=82=D0=B8=202=20=D0=B1=D1=96=D0=BD?= =?UTF-8?q?=D0=B0,=20=D1=96=20=D0=B2=D0=B8=D0=B1=D1=96=D1=80=D0=BA=D0=B0?= =?UTF-8?q?=20=D1=8F=D0=BA=D0=BE=D1=97=20=D0=B2=D0=B8=D0=BA=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=BE=D0=B2=D1=83=D0=B2=D0=B0=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2=D0=B8=D0=BD=D0=BD=D0=B0=20=D0=B2=D0=B8=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B0=D1=82=D0=B8=D1=81=D1=8F=20=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D0=B2=D0=BD=D0=B8=D0=BC=20=D0=BF=D1=80=D0=BE=D1=84?= =?UTF-8?q?=D1=96=D0=BB=D0=B5=D0=BC=20Spring.=20H2=20=D0=BD=D0=B5=20=D0=BF?= =?UTF-8?q?=D1=96=D0=B4=D1=82=D1=80=D0=B8=D0=BC=D1=83=D1=94=20=D0=B2=D1=81?= =?UTF-8?q?=D1=96=20=D1=84=D1=96=D1=87=D1=96,=20=D1=8F=D0=BA=D1=96=20?= =?UTF-8?q?=D1=94=20=D1=83=20PostgreSQL,=20=D1=82=D0=BE=D0=BC=D1=83=20?= =?UTF-8?q?=D1=82=D0=BE=D0=B1=D1=96=20=D0=B4=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D1=82=D1=8C=D1=81=D1=8F=20=D1=82=D1=80=D0=BE=D1=85=D0=B8=20?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=BE=D1=81=D1=82=D0=B8=D1=82=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D0=B8=20=D0=B7=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=B2=D0=B8=D0=BC=D0=B8=20=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compose.yaml | 8 +- pom.xml | 6 + .../jira/profile/internal/model/Contact.java | 2 +- src/main/resources/application.yaml | 4 + src/main/resources/data4dev/data.sql | 2 +- src/main/resources/db/changelog.sql | 2 +- .../javarush/jira/AbstractControllerTest.java | 2 +- src/test/resources/application-test.yaml | 26 +- src/test/resources/data.sql | 52 ++-- src/test/resources/db/changelog-test.sql | 251 ++++++++++++++++++ 10 files changed, 309 insertions(+), 46 deletions(-) create mode 100644 src/test/resources/db/changelog-test.sql diff --git a/compose.yaml b/compose.yaml index 4c24f9b47..726ac5b7a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,14 +2,14 @@ services: postgres: image: postgres:17 environment: - POSTGRES_DB: ${POSTGRES_DB} # Зчитується з .env файлу - POSTGRES_USER: ${POSTGRES_USER} # Зчитується з .env файлу - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # Зчитується з .env файлу + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} TZ: UTC ports: - '5432:5432' volumes: - - postgres_data:/var/lib/postgresql/data # Persists database data + - postgres_data:/var/lib/postgresql/data volumes: postgres_data: \ No newline at end of file diff --git a/pom.xml b/pom.xml index e91cfe284..17044851e 100644 --- a/pom.xml +++ b/pom.xml @@ -95,6 +95,12 @@ spring-boot-starter-thymeleaf + + com.h2database + h2 + test + + org.postgresql postgresql diff --git a/src/main/java/com/javarush/jira/profile/internal/model/Contact.java b/src/main/java/com/javarush/jira/profile/internal/model/Contact.java index e3f29674b..3052233cc 100644 --- a/src/main/java/com/javarush/jira/profile/internal/model/Contact.java +++ b/src/main/java/com/javarush/jira/profile/internal/model/Contact.java @@ -45,7 +45,7 @@ public class Contact implements HasId { @NotBlank @Size(min = 2, max = 256) - @Column(name = "value", nullable = false) + @Column(name = "val", nullable = false) @NoHtml private String value; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index d4e3d7f8c..819375e16 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -15,10 +15,12 @@ spring: show-sql: true open-in-view: false + # validate db by model hibernate: ddl-auto: validate properties: + # http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#configurations hibernate: format_sql: true default-batch-fetch-size: 20 @@ -32,6 +34,7 @@ spring: liquibase: changeLog: "classpath:db/changelog.sql" + # Jackson Fields Serialization jackson: visibility: field: any @@ -39,6 +42,7 @@ spring: setter: none is-getter: none + # https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties-cache cache: cache-names: users caffeine.spec: maximumSize=10000,expireAfterAccess=5m diff --git a/src/main/resources/data4dev/data.sql b/src/main/resources/data4dev/data.sql index 4a4bed843..f11a33804 100644 --- a/src/main/resources/data4dev/data.sql +++ b/src/main/resources/data4dev/data.sql @@ -49,7 +49,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, VAL) values (1, 'skype', 'userSkype'), (1, 'mobile', '+01234567890'), (1, 'website', 'user.com'), diff --git a/src/main/resources/db/changelog.sql b/src/main/resources/db/changelog.sql index 1595f97d3..37b8473a9 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, + VAL varchar(256) not null, primary key (ID, CODE), constraint FK_CONTACT_PROFILE foreign key (ID) references PROFILE (ID) on delete cascade ); diff --git a/src/test/java/com/javarush/jira/AbstractControllerTest.java b/src/test/java/com/javarush/jira/AbstractControllerTest.java index 5981bae53..2652be592 100644 --- a/src/test/java/com/javarush/jira/AbstractControllerTest.java +++ b/src/test/java/com/javarush/jira/AbstractControllerTest.java @@ -9,7 +9,7 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; //https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications -@Sql(scripts = {"classpath:db/changelog.sql", "classpath:data.sql"}, config = @SqlConfig(encoding = "UTF-8")) +@Sql(scripts = {"classpath:db/changelog-test.sql", "classpath:data.sql"}, config = @SqlConfig(encoding = "UTF-8")) @AutoConfigureMockMvc //https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-mock-environment public abstract class AbstractControllerTest extends BaseTests { diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 51137fd06..bd1916c6c 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,8 +1,24 @@ spring.cache.type: none spring: - init: - mode: always + # Настройка инициализации SQL для Spring. + # Установлено в 'never', чтобы избежать конфликтов с Liquibase, + # который управляет схемой и данными. + sql: + init: + mode: never datasource: - url: jdbc:postgresql://localhost:5433/jira-test - username: jira - password: JiraRush \ No newline at end of file + # URL для базы данных H2 в памяти. + # DB_CLOSE_DELAY=-1 предотвращает закрытие БД после последнего соединения. + # DB_CLOSE_ON_EXIT=FALSE предотвращает закрытие БД при выходе из JVM (полезно для отладки). + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: sa # Стандартный пользователь для H2 + password: # Стандартный пустой пароль для H2 + driver-class-name: org.h2.Driver # Правильный драйвер для H2 + jpa: + database-platform: org.hibernate.dialect.H2Dialect # Правильный диалект Hibernate для H2 + hibernate: + # Важно: Устанавливаем 'none', чтобы Hibernate не пытался + # автоматически управлять схемой базы данных, так как это делает Liquibase. + ddl-auto: none + liquibase: + change-log: "classpath:db/changelog-test.sql" # Путь к вашему файлу Liquibase changelog \ No newline at end of file diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index bc26161e4..d44e99876 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -1,32 +1,19 @@ --------- 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 PROFILE; +delete from USERS; +delete from ACTIVITY; +delete from TASK; +delete from SPRINT; +delete from PROJECT; +delete from USER_BELONG; +alter table PROJECT ALTER COLUMN ID restart with 1; +alter table USERS ALTER COLUMN ID restart with 1; +alter table ACTIVITY ALTER COLUMN ID restart with 1; +alter table TASK ALTER COLUMN ID restart with 1; +alter table SPRINT ALTER COLUMN ID restart with 1; +alter table SPRINT ALTER COLUMN ID restart with 1; +alter table USER_BELONG 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 +35,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, VAL) values (1, 'skype', 'userSkype'), (1, 'mobile', '+01234567890'), (1, 'website', 'user.com'), @@ -87,11 +74,10 @@ values (1, 1, '2023-05-15 09:05:10', null, 'Data', null, 3, 'epic', 'in_progress (1, 2, '2023-05-15 12:05:10', null, 'Trees', 'Trees desc', 4, 'epic', 'in_progress', 'normal'); insert into USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE, STARTPOINT, ENDPOINT) -values (1, 2, 2, 'task_developer', '2023-06-14 08:35:10', '2023-06-14 08:55:00'), +values (1, 2, 2, 'task_tester', '2023-06-14 08:35:10', '2023-06-14 08:55:00'), (1, 2, 2, 'task_reviewer', '2023-06-14 09:35:10', null), - (1, 2, 1, 'task_developer', '2023-06-12 11:40:00', '2023-06-12 12:35:00'), - (1, 2, 1, 'task_developer', '2023-06-13 12:35:00', null), + (1, 2, 1, 'task_developer', '2023-06-12 11:40:00', null), (1, 2, 1, 'task_tester', '2023-06-14 15:20:00', null), (2, 2, 2, 'task_developer', '2023-06-08 07:10:00', null), (2, 2, 1, 'task_developer', '2023-06-09 14:48:00', null), - (2, 2, 1, 'task_tester', '2023-06-10 16:37:00', null); + (2, 2, 1, 'task_tester', '2023-06-10 16:37:00', null); \ No newline at end of file diff --git a/src/test/resources/db/changelog-test.sql b/src/test/resources/db/changelog-test.sql new file mode 100644 index 000000000..66a82a2ae --- /dev/null +++ b/src/test/resources/db/changelog-test.sql @@ -0,0 +1,251 @@ +--liquibase formatted sql + +--changeset apuchinec:drop_all + +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; + +--changeset apuchinec:create_tables + +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, + CODE varchar(32) not null, + PROJECT_ID bigint not null, + constraint FK_SPRINT_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade, + constraint UK_SPRINT_PROJECT_CODE unique(PROJECT_ID, CODE) +); + +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 TASK +( + ID bigint generated by default as identity primary key, + TITLE varchar(1024) not null, + TYPE_CODE varchar(32) not null, + STATUS_CODE varchar(32) not null, + 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) on delete cascade , + 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) on delete cascade, + constraint UK_USER_BELONG unique(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) on DELETE cascade +); + +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 apuchinec: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 + ('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), +-- 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|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); +create table CONTACT +( + ID bigint not null, + CODE varchar(32) not null, + VAL varchar(256) not null, + primary key (ID, CODE), + constraint FK_CONTACT_PROFILE foreign key (ID) references PROFILE (ID) on delete cascade +); \ No newline at end of file From ee78cf057d5e229d962ef3dde2bd4a440173b349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:14:30 +0200 Subject: [PATCH 07/15] =?UTF-8?q?4.=20=D0=9F=D0=B5=D1=80=D0=B5=D1=80=D0=BE?= =?UTF-8?q?=D0=B1=D0=B8=D1=82=D0=B8=20=D1=82=D0=B5=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D1=82=D0=B0=D0=BA,=20=D1=89=D0=BE=D0=B1=20=D0=BF=D1=96=D0=B4?= =?UTF-8?q?=20=D1=87=D0=B0=D1=81=20=D1=82=D0=B5=D1=81=D1=82=D1=96=D0=B2=20?= =?UTF-8?q?=D0=B2=D0=B8=D0=BA=D0=BE=D1=80=D0=B8=D1=81=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=D1=83=D0=B2=D0=B0=D0=BB=D0=B0=D1=81=D1=8F=20in=20memory=20?= =?UTF-8?q?=D0=91=D0=94=20(H2),=20=D0=B0=20=D0=BD=D0=B5=20PostgreSQL.=20?= =?UTF-8?q?=D0=94=D0=BB=D1=8F=20=D1=86=D1=8C=D0=BE=D0=B3=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=82=D1=80=D1=96=D0=B1=D0=BD=D0=BE=20=D0=B2=D0=B8=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B8=D1=82=D0=B8=202=20=D0=B1=D1=96=D0=BD?= =?UTF-8?q?=D0=B0,=20=D1=96=20=D0=B2=D0=B8=D0=B1=D1=96=D1=80=D0=BA=D0=B0?= =?UTF-8?q?=20=D1=8F=D0=BA=D0=BE=D1=97=20=D0=B2=D0=B8=D0=BA=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=BE=D0=B2=D1=83=D0=B2=D0=B0=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2=D0=B8=D0=BD=D0=BD=D0=B0=20=D0=B2=D0=B8=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B0=D1=82=D0=B8=D1=81=D1=8F=20=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D0=B2=D0=BD=D0=B8=D0=BC=20=D0=BF=D1=80=D0=BE=D1=84?= =?UTF-8?q?=D1=96=D0=BB=D0=B5=D0=BC=20Spring.=20H2=20=D0=BD=D0=B5=20=D0=BF?= =?UTF-8?q?=D1=96=D0=B4=D1=82=D1=80=D0=B8=D0=BC=D1=83=D1=94=20=D0=B2=D1=81?= =?UTF-8?q?=D1=96=20=D1=84=D1=96=D1=87=D1=96,=20=D1=8F=D0=BA=D1=96=20?= =?UTF-8?q?=D1=94=20=D1=83=20PostgreSQL,=20=D1=82=D0=BE=D0=BC=D1=83=20?= =?UTF-8?q?=D1=82=D0=BE=D0=B1=D1=96=20=D0=B4=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D1=82=D1=8C=D1=81=D1=8F=20=D1=82=D1=80=D0=BE=D1=85=D0=B8=20?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=BE=D1=81=D1=82=D0=B8=D1=82=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D0=B8=20=D0=B7=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=B2=D0=B8=D0=BC=D0=B8=20=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/resources/application-test.yaml | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index bd1916c6c..a8c2f7ef2 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,24 +1,16 @@ spring.cache.type: none spring: - # Настройка инициализации SQL для Spring. - # Установлено в 'never', чтобы избежать конфликтов с Liquibase, - # который управляет схемой и данными. sql: init: mode: never datasource: - # URL для базы данных H2 в памяти. - # DB_CLOSE_DELAY=-1 предотвращает закрытие БД после последнего соединения. - # DB_CLOSE_ON_EXIT=FALSE предотвращает закрытие БД при выходе из JVM (полезно для отладки). url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE - username: sa # Стандартный пользователь для H2 - password: # Стандартный пустой пароль для H2 - driver-class-name: org.h2.Driver # Правильный драйвер для H2 + username: sa + password: + driver-class-name: org.h2.Driver jpa: - database-platform: org.hibernate.dialect.H2Dialect # Правильный диалект Hibernate для H2 + database-platform: org.hibernate.dialect.H2Dialect hibernate: - # Важно: Устанавливаем 'none', чтобы Hibernate не пытался - # автоматически управлять схемой базы данных, так как это делает Liquibase. ddl-auto: none liquibase: - change-log: "classpath:db/changelog-test.sql" # Путь к вашему файлу Liquibase changelog \ No newline at end of file + change-log: "classpath:db/changelog-test.sql" \ No newline at end of file From 3c2cdd007dbbb8a41c878cf635262888ef3ca4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:19:34 +0200 Subject: [PATCH 08/15] =?UTF-8?q?9.=20=D0=9D=D0=B0=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=20Dockerfile=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=81?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=81=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..0a2effd82 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1.7 +FROM eclipse-temurin:17-jdk-jammy AS builder +WORKDIR /app + +# Copy build files first to leverage Docker layer cache +COPY .mvn/ .mvn/ +COPY mvnw pom.xml ./ +RUN chmod +x mvnw +# Speed up builds by caching the Maven repo (BuildKit needed) +RUN --mount=type=cache,target=/root/.m2 ./mvnw -B -DskipTests dependency:go-offline + +# Now add sources and build +COPY src/ src/ +RUN --mount=type=cache,target=/root/.m2 ./mvnw -B -DskipTests clean package + +FROM eclipse-temurin:17-jre-jammy +WORKDIR /app + +# Run as non-root +RUN useradd -ms /bin/bash appuser +USER appuser + +# Copy the fat jar +COPY --from=builder /app/target/*.jar /app/app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java","-jar","/app/app.jar"] From cc3c2de0cd9422feb76bdde7989493f3498623fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:22:13 +0200 Subject: [PATCH 09/15] =?UTF-8?q?6.=20=D0=97=D1=80=D0=BE=D0=B1=D0=B8=D1=82?= =?UTF-8?q?=D0=B8=20=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D0=B3=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D1=83=20com.javar?= =?UTF-8?q?ush.jira.bugtracking.attachment.FileUtil#upload,=20=D1=89=D0=BE?= =?UTF-8?q?=D0=B1=20=D0=B2=D1=96=D0=BD=20=D0=B2=D0=B8=D0=BA=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=BE=D0=B2=D1=83=D0=B2=D0=B0=D0=B2=20=D1=81?= =?UTF-8?q?=D1=83=D1=87=D0=B0=D1=81=D0=BD=D0=B8=D0=B9=20=D0=BF=D1=96=D0=B4?= =?UTF-8?q?=D1=85=D1=96=D0=B4=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=BE=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B8=20=D0=B7=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE?= =?UTF-8?q?=D0=B2=D0=BE=D1=8E=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=BE?= =?UTF-8?q?=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jira/bugtracking/attachment/FileUtil.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java index 6cffbe175..434ad5501 100644 --- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java +++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java @@ -25,14 +25,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()); - } + try { + Path directory = Path.of(directoryPath); + Files.createDirectory(directory); + Path targetFile = directory.resolve(fileName); + multipartFile.transferTo(targetFile); + } catch (IOException e) { + throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename()); } } From 1c90f60cb13e82b975f7e0e5a0b176310bcb3841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:21:17 +0200 Subject: [PATCH 10/15] =?UTF-8?q?5.=20=D0=9D=D0=B0=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=20=D1=82=D0=B5=D1=81=D1=82=D0=B8=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B2=D1=81=D1=96=D1=85=20=D0=BF=D1=83=D0=B1=D0=BB?= =?UTF-8?q?=D1=96=D1=87=D0=BD=D0=B8=D1=85=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4?= =?UTF-8?q?=D1=96=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=BE=D0=BB=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=20ProfileRestController?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/ProfileRestControllerTest.java | 109 +++++++++++++++++- .../profile/internal/web/ProfileTestData.java | 3 + 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java b/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java index a6fd5e3bf..a8491b559 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,115 @@ 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.*; +import static com.javarush.jira.profile.internal.web.ProfileTestData.PROFILE_MATCHER; +import static com.javarush.jira.profile.internal.web.ProfileTestData.TO_MATCHER; +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 { -} \ No newline at end of file + @Autowired + private ProfileRepository profileRepository; + + private static final String REST_URL = ProfileRestController.REST_URL; + + @Test + @WithUserDetails(value = USER_MAIL) + void get_asUser_success() throws Exception { + ProfileTo expectedProfile = ProfileTestData.USER_PROFILE_TO; + expectedProfile.setId(USER_ID); + + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(TO_MATCHER.contentJson(expectedProfile)); + } + + @Test + @WithUserDetails(value = GUEST_MAIL) + void get_asGuest_emptyProfile() throws Exception { + ProfileTo expectedProfile = ProfileTestData.GUEST_PROFILE_EMPTY_TO; + expectedProfile.setId(GUEST_ID); + + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(TO_MATCHER.contentJson(expectedProfile)); + } + + @Test + void get_unauthorized() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void update_success() throws Exception { + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(ProfileTestData.getUpdatedTo()))) + .andDo(print()) + .andExpect(status().isNoContent()); + + Profile dbProfileAfter = profileRepository.getExisted(USER_ID); + Profile updated = ProfileTestData.getUpdated(USER_ID); + + PROFILE_MATCHER.assertMatch(dbProfileAfter, updated); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void update_invalidNotification() throws Exception { + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(ProfileTestData.getWithUnknownNotificationTo()))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void update_invalidUser() throws Exception { + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(ProfileTestData.getInvalidTo()))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void update_unknownContact() throws Exception { + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(ProfileTestData.getWithUnknownContactTo()))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void update_htmlUnsafeContact() throws Exception { + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(ProfileTestData.getWithContactHtmlUnsafeTo()))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } +} 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 cc0513971..83f14955d 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 TO_MATCHER = + MatcherFactory.usingIgnoringFieldsComparator(ProfileTo.class, "user"); + public static ProfileTo USER_PROFILE_TO = new ProfileTo(null, Set.of("assigned", "overdue", "deadline"), Set.of(new ContactTo("skype", "userSkype"), From a5361805745e826c2f3979833b96d9b5e69521db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:34:18 +0200 Subject: [PATCH 11/15] =?UTF-8?q?7.=20=D0=94=D0=BE=D0=B4=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=20=D0=BD=D0=BE=D0=B2=D0=B8=D0=B9=20=D1=84=D1=83=D0=BD=D0=BA?= =?UTF-8?q?=D1=86=D1=96=D0=BE=D0=BD=D0=B0=D0=BB:=20=D0=B4=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0=D0=B2=D0=B0=D0=BD=D0=BD=D1=8F=20=D1=82=D0=B5=D0=B3=D1=96?= =?UTF-8?q?=D0=B2=20=D0=B4=D0=BE=20=D0=B7=D0=B0=D0=B2=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8F=20(REST=20API=20+=20=D1=80=D0=B5=D0=B0=D0=BB=D1=96?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D1=96=D1=8F=20=D0=BD=D0=B0=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D1=96=D1=81=D1=96).=20=D0=A4=D1=80=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=20=D1=80=D0=BE=D0=B1=D0=B8=D1=82=D0=B8=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=BE=D0=B1=D0=BE=D0=B2'=D1=8F=D0=B7=D0=BA=D0=BE=D0=B2=D0=BE.?= =?UTF-8?q?=20=D0=A2=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=8F=20task=5Ftag=20?= =?UTF-8?q?=D0=B2=D0=B6=D0=B5=20=D1=81=D1=82=D0=B2=D0=BE=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jira/bugtracking/task/TaskController.java | 6 ++++++ .../jira/bugtracking/task/TaskService.java | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) 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..4fa2523af 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java @@ -156,4 +156,10 @@ public TaskTreeNode(TaskTo taskTo) { this(taskTo, new LinkedList<>()); } } + + @PostMapping("/{id}/tag") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void addTag(@PathVariable long id, @RequestBody String tag) { + taskService.addTag(id, tag); + } } 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..e58a8031d 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java @@ -10,6 +10,7 @@ import com.javarush.jira.bugtracking.task.to.TaskToExt; import com.javarush.jira.bugtracking.task.to.TaskToFull; import com.javarush.jira.common.error.DataConflictException; +import com.javarush.jira.common.error.IllegalRequestDataException; import com.javarush.jira.common.error.NotFoundException; import com.javarush.jira.common.util.Util; import com.javarush.jira.login.AuthUser; @@ -140,4 +141,19 @@ private void checkAssignmentActionPossible(long id, String userType, boolean ass throw new DataConflictException(String.format(assign ? CANNOT_ASSIGN : CANNOT_UN_ASSIGN, userType, task.getStatusCode())); } } + + @Transactional + public void addTag(long taskId, String tag) { + if (tag == null || tag.isEmpty()) { + throw new IllegalRequestDataException("Tag must not be null or empty"); + } + if (tag.length() > 32) { + throw new IllegalRequestDataException("Tag must not exceed 32 characters"); + } + + Task task = handler.getRepository().getExisted(taskId); + task.getTags().add(tag); + + handler.getRepository().saveAndFlush(task); + } } From f5852fa9a068d12692fa1882b4939ddbd0b6e428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:52:07 +0200 Subject: [PATCH 12/15] =?UTF-8?q?8.=20=D0=94=D0=BE=D0=B4=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=20=D0=BF=D1=96=D0=B4=D1=80=D0=B0=D1=85=D1=83=D0=BD=D0=BE=D0=BA?= =?UTF-8?q?=20=D1=87=D0=B0=D1=81=D1=83:=20=D1=81=D0=BA=D1=96=D0=BB=D1=8C?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B7=D0=B0=D0=B2=D0=B4=D0=B0=D0=BD=D0=BD=D1=8F?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B1=D1=83=D0=B2=D0=B0=D0=BB=D0=BE?= =?UTF-8?q?=20=D1=83=20=D1=80=D0=BE=D0=B1=D0=BE=D1=82=D1=96=20=D1=82=D0=B0?= =?UTF-8?q?=20=D1=82=D0=B5=D1=81=D1=82=D1=83=D0=B2=D0=B0=D0=BD=D0=BD=D1=96?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bugtracking/task/ActivityRepository.java | 4 ++ .../bugtracking/task/ActivityService.java | 45 ++++++++++++++++--- src/main/resources/db/changelog.sql | 7 +++ 3 files changed, 51 insertions(+), 5 deletions(-) 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..b86749dc9 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/ActivityRepository.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/ActivityRepository.java @@ -1,10 +1,12 @@ package com.javarush.jira.bugtracking.task; import com.javarush.jira.common.BaseRepository; +import jakarta.validation.constraints.Size; import org.springframework.data.jpa.repository.Query; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Optional; @Transactional(readOnly = true) public interface ActivityRepository extends BaseRepository { @@ -13,4 +15,6 @@ 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); + + Optional findActivityByTaskIdAndStatusCode(long taskId, @Size(min = 2, max = 32) String statusCode); } 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..308a16eab 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java @@ -8,6 +8,8 @@ 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; @@ -19,11 +21,9 @@ public class ActivityService { private final Handlers.ActivityHandler handler; - private static void checkBelong(HasAuthorId activity) { - if (activity.getAuthorId() != AuthUser.authId()) { - throw new DataConflictException("Activity " + activity.getId() + " doesn't belong to " + AuthUser.get()); - } - } + public static final String IN_PROGRESS = "in_progress"; + public static final String READY_FOR_REVIEW = "ready_for_review"; + public static final String DONE = "done"; @Transactional public Activity create(ActivityTo activityTo) { @@ -73,4 +73,39 @@ private void updateTaskIfRequired(long taskId, String activityStatus, String act } } } + + private static void checkBelong(HasAuthorId activity) { + if (activity.getAuthorId() != AuthUser.authId()) { + throw new DataConflictException("Activity " + activity.getId() + " doesn't belong to " + AuthUser.get()); + } + } + + public Duration progressTaskDuration(Task task) { + LocalDateTime readyForReview = getActivityUpdatedTime(task.id(), READY_FOR_REVIEW); + LocalDateTime inProgress = getActivityUpdatedTime(task.id(), IN_PROGRESS); + + if (readyForReview == null || inProgress == null) { + return Duration.ZERO; + } + + return Duration.between(inProgress, readyForReview); + } + + public Duration testingTaskDuration(Task task) { + LocalDateTime done = getActivityUpdatedTime(task.id(), DONE); + LocalDateTime readyForReview = getActivityUpdatedTime(task.id(), READY_FOR_REVIEW); + + if (readyForReview == null || done == null) { + return Duration.ZERO; + } + + return Duration.between(readyForReview, done); + } + + private LocalDateTime getActivityUpdatedTime(long taskId, String statusCode) { + return handler.getRepository() + .findActivityByTaskIdAndStatusCode(taskId, statusCode) + .map(Activity::getUpdated) + .orElse(null); + } } diff --git a/src/main/resources/db/changelog.sql b/src/main/resources/db/changelog.sql index 37b8473a9..0f75bebd0 100644 --- a/src/main/resources/db/changelog.sql +++ b/src/main/resources/db/changelog.sql @@ -328,3 +328,10 @@ 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 romanyehorov:add_user_activity + +insert into ACTIVITY (AUTHOR_ID, TASK_ID, UPDATED, STATUS_CODE) +values (7, 1, '2023-05-16 09:05:10.000000', 'in_progress'), + (7, 1, '2023-05-16 12:25:10.000000', 'ready_for_review'), + (7, 1, '2023-05-16 14:05:10.000000', 'done'); \ No newline at end of file From 057defe22cdea2dd47f37cd4e7f969bf4aef0b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:28:14 +0200 Subject: [PATCH 13/15] =?UTF-8?q?10.=20=D0=9D=D0=B0=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D1=82=D0=B8=20docker-compose=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA?= =?UTF-8?q?=D1=83=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B0=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=BE=D0=BC=20=D0=B7=20=D0=91=D0=94=20=D1=82=D0=B0?= =?UTF-8?q?=20nginx.=20=D0=94=D0=BB=D1=8F=20nginx=20=D0=B2=D0=B8=D0=BA?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D1=81=D1=82=D0=BE=D0=B2=D1=83=D0=B9=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=84=D1=96=D0=B3-=D1=84=D0=B0=D0=B9=D0=BB=20confi?= =?UTF-8?q?g/nginx.conf.=20=D0=97=D0=B0=20=D0=BF=D0=BE=D1=82=D1=80=D0=B5?= =?UTF-8?q?=D0=B1=D0=B8=20=D1=84=D0=B0=D0=B9=D0=BB=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=84=D1=96=D0=B3=D0=B0=20=D0=BC=D0=BE=D0=B6=D0=BD=D0=B0=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B0=D0=B3=D1=83=D0=B2=D0=B0=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compose.yaml | 52 +++++++++++++++++++++------ config/nginx.conf | 90 +++++++++++++++++++++++++++-------------------- 2 files changed, 93 insertions(+), 49 deletions(-) diff --git a/compose.yaml b/compose.yaml index 726ac5b7a..c54cb4ef2 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,15 +1,47 @@ services: - postgres: - image: postgres:17 - environment: - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - TZ: UTC + db: + image: postgres + container_name: postgres + env_file: + - ./.env ports: - - '5432:5432' + - "5433:5432" volumes: - - postgres_data:/var/lib/postgresql/data + - pgdata:/var/lib/postgresql/data + networks: + - app_network + + app: + build: + context: . + dockerfile: Dockerfile + container_name: app + env_file: + - ./.env + ports: + - "8080:8080" + depends_on: + - db + networks: + - app_network + + nginx: + image: nginx:latest + container_name: nginx + volumes: + - ./config/nginx.conf:/etc/nginx/nginx.conf + - ./resources/static:/opt/jirarush/resources/static + ports: + - "80:80" + depends_on: + - app + networks: + - app_network + +networks: + app_network: + driver: bridge volumes: - postgres_data: \ No newline at end of file + pgdata: + driver: local \ No newline at end of file diff --git a/config/nginx.conf b/config/nginx.conf index 82b9e234d..3ef4ca4f3 100644 --- a/config/nginx.conf +++ b/config/nginx.conf @@ -1,40 +1,52 @@ -# https://losst.ru/ustanovka-nginx-ubuntu-16-04 -# https://pai-bx.com/wiki/nginx/2332-useful-redirects-in-nginx/#1 -# sudo iptables -A INPUT ! -s 127.0.0.1 -p tcp -m tcp --dport 8080 -j DROP -server { - listen 80; - - # https://www.digitalocean.com/community/tutorials/how-to-optimize-nginx-configuration - gzip on; - gzip_types text/css application/javascript application/json; - gzip_min_length 2048; - - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - root /opt/jirarush/resources; - - if ($request_uri ~ ';') {return 404;} - - # proxy_cookie_flags ~ secure samesite=none; - - # static - location /static/ { - expires 30d; - access_log off; - } - location /robots.txt { - access_log off; - } - - location ~ (/$|/view/|/ui/|/oauth2/) { - expires 0m; - proxy_pass http://localhost:8080; - proxy_connect_timeout 30s; - } - location ~ (/api/|/doc|/swagger-ui/|/v3/api-docs/) { - proxy_pass http://localhost:8080; - proxy_connect_timeout 150s; - } - location / { - try_files /view/404.html = 404; - } +# Основний блок налаштувань +user nginx; +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Ваш серверний блок + server { + listen 80; + + # https://www.digitalocean.com/community/tutorials/how-to-optimize-nginx-configuration + gzip on; + gzip_types text/css application/javascript application/json; + gzip_min_length 2048; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + root /opt/jirarush/resources; + + if ($request_uri ~ ';') { return 404; } + + # Статичні файли + location /static/ { + expires 30d; + access_log off; + } + + location /robots.txt { + access_log off; + } + + location ~ (/$|/view/|/ui/|/oauth2/) { + expires 0m; + proxy_pass http://localhost:8080; + proxy_connect_timeout 30s; + } + + location ~ (/api/|/doc|/swagger-ui/|/v3/api-docs/) { + proxy_pass http://localhost:8080; + proxy_connect_timeout 150s; + } + + location / { + try_files /view/404.html '=' 404; + } + } } \ No newline at end of file From 363a9a8c03b02935b2a04a90eada1ffdce80e4cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:30:06 +0200 Subject: [PATCH 14/15] refactor: application-test.yaml and application.yaml for testing --- src/main/resources/application.yaml | 6 ++---- src/test/resources/application-test.yaml | 24 +++++++++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 819375e16..891c2e063 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,5 +1,3 @@ -# src/main/resources/application.yml - app: host-url: http://localhost:8080 test-mail: ${SPRING_MAIL_USERNAME} @@ -9,16 +7,16 @@ app: max-pool-size: 100 spring: + config: + import: "optional:.env" init: mode: never jpa: show-sql: true open-in-view: false - # validate db by model hibernate: ddl-auto: validate - properties: # http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#configurations hibernate: diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index a8c2f7ef2..9ea4d79b1 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,8 +1,4 @@ -spring.cache.type: none spring: - sql: - init: - mode: never datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE username: sa @@ -13,4 +9,22 @@ spring: hibernate: ddl-auto: none liquibase: - change-log: "classpath:db/changelog-test.sql" \ No newline at end of file + change-log: "classpath:db/changelog-test.sql" + cache: + type: none + mail: + username: test@example.com + password: testpassword + security: + oauth2: + client: + registration: + github: + client-id: test-github-client-id + client-secret: test-github-client-secret + google: + client-id: test-google-client-id + client-secret: test-google-client-secret + gitlab: + client-id: test-gitlab-client-id + client-secret: test-gitlab-client-secret \ No newline at end of file From 645ddc2dae95dcd64345deae39325065df045265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=93=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=B2?= <123542367+AGuliaiev@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:00:30 +0200 Subject: [PATCH 15/15] Adding to README.md --- README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 719b268f5..4a1cb9836 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,27 @@ - https://habr.com/ru/articles/259055/ Список выполненных задач: -... \ No newline at end of file + +# JiraRush Project: My Contributions & Enhancements + +This document outlines my key contributions and improvements to the JiraRush project, covering aspects of security, testing, refactoring, new features, and deployment. + +## My Contributions to the Project: + +Here's a breakdown of the specific tasks and features I implemented: + +1. 📘 **Project Structure Onboarding:** I successfully familiarized myself with the existing project structure and architecture, enabling effective subsequent modifications and new feature development. +2. 🚫 **Removal of Deprecated Social Integrations:** I removed outdated social media integrations (VK, Yandex) from the project, simplifying the codebase and mitigating potential security risks. +3. 🔑 **Externalization of Sensitive Information:** I moved sensitive data, such as database login credentials, OAuth registration/authorization identifiers, and email settings, into separate property files. These values are now securely loaded from machine environment variables upon server startup, significantly enhancing security and configuration flexibility. +4. 🧪 **Refactoring Tests for In-Memory H2 Database:** I adapted the test suite to utilize an in-memory H2 database instead of PostgreSQL. This involved defining two distinct Spring beans, with the active Spring profile dictating which database to use. Minor adjustments were also made to test data scripts to ensure compatibility with H2's features. +5. ✅ **Comprehensive `ProfileRestController` Test Coverage:** I developed a robust set of unit and integration tests for all public methods of the `ProfileRestController`. These tests meticulously validate both successful and various unsuccessful execution paths to guarantee the API's reliability and resilience. +6. 🔄 **Refactoring `FileUtil#upload` for Modern File System API:** I refactored the `com.javarush.jira.bugtracking.attachment.FileUtil#upload` method to leverage modern Java file system APIs, enhancing its efficiency, robustness, and maintainability for file operations. +7. 🏷️ **Implementation of Task Tagging System:** I introduced a new feature allowing users to add tags to tasks. This includes developing the dedicated REST API endpoint and implementing the corresponding service-layer logic, utilizing the existing `task_tag` database table. +8. ⏱️ **Adding Task Time Tracking Functionality:** I implemented two service-level methods to calculate the time a task spends in specific operational states: + * **Time in Work:** Calculated as the duration from `in_progress` to `ready_for_review` status. + * **Time in Testing:** Calculated as the duration from `ready_for_review` to `done` status. + To support this, I appended three critical `ACTIVITY` entries (with `in_progress`, `ready_for_review`, and `done` statuses) to the `changelog.sql` database initialization script. +9. 🐳 **Dockerfile for Application Server:** I created a `Dockerfile` to containerize the main application server, ensuring easy and consistent deployment across different environments. +10. 🚀 **Docker Compose for Full Stack Deployment:** I developed a `docker-compose.yml` file to orchestrate the entire application stack. This setup includes the application server, a PostgreSQL database, and an Nginx reverse proxy, leveraging the `config/nginx.conf` file (which can be modified as needed) for efficient routing and load balancing. + +--- \ No newline at end of file