From feee88ad740890d1e0b413ef8a4e2f5aae555153 Mon Sep 17 00:00:00 2001 From: Denys Date: Tue, 29 Jul 2025 20:24:50 +0300 Subject: [PATCH 1/8] Task 2(removed VK and Ya) --- resources/static/fontawesome/css/all.css | 9 ----- resources/view/login.html | 8 ----- resources/view/unauth/register.html | 8 ----- .../handler/VkOAuth2UserDataHandler.java | 35 ------------------- .../handler/YandexOAuth2UserDataHandler.java | 21 ----------- src/main/resources/application.yaml | 25 +------------ src/main/resources/data4dev/data.sql | 3 +- .../profile/internal/web/ProfileTestData.java | 2 -- src/test/resources/data.sql | 3 +- 9 files changed, 3 insertions(+), 111 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/resources/static/fontawesome/css/all.css b/resources/static/fontawesome/css/all.css index af5980828..c337f48dd 100644 --- a/resources/static/fontawesome/css/all.css +++ b/resources/static/fontawesome/css/all.css @@ -8603,9 +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,9 +9952,6 @@ readers do not read off random characters that represent icons */ content: "\f3bc"; } -.fa-yandex:before { - content: "\f413"; -} .fa-readme:before { content: "\f4d5"; @@ -10183,9 +10177,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/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..87a6186ff 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -61,20 +61,7 @@ spring: 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 @@ -83,16 +70,6 @@ spring: 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 diff --git a/src/main/resources/data4dev/data.sql b/src/main/resources/data4dev/data.sql index f101ab9b1..d9225f640 100644 --- a/src/main/resources/data4dev/data.sql +++ b/src/main/resources/data4dev/data.sql @@ -52,8 +52,7 @@ values (1, 'skype', 'userSkype'), (1, 'mobile', '+01234567890'), (1, 'website', 'user.com'), (2, 'github', 'adminGitHub'), - (2, 'tg', 'adminTg'), - (2, 'vk', 'adminVk'); + (2, 'tg', 'adminTg'); TRUNCATE TABLE ATTACHMENT; alter 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 a04439d6c..758d8dc6e 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -45,8 +45,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 8af74cb9beb214ddacbe08f15513311d9e77e693 Mon Sep 17 00:00:00 2001 From: Denys Date: Fri, 1 Aug 2025 18:00:38 +0300 Subject: [PATCH 2/8] Task 3 - Introduce sensitive information to a specific file --- .gitignore | 1 + pom.xml | 6 ++++++ 2 files changed, 7 insertions(+) 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/pom.xml b/pom.xml index 4556ac639..c555db575 100644 --- a/pom.xml +++ b/pom.xml @@ -152,6 +152,12 @@ true + + me.paulschwarz + spring-dotenv + 4.0.0 + + From 4e69308c2ca53a334a545a7b85f98e94788f7464 Mon Sep 17 00:00:00 2001 From: Roman Yehorov Date: Fri, 1 Aug 2025 19:30:47 +0300 Subject: [PATCH 3/8] task 4: change of tests from PostgreSQL to in-memory database(attempt num 1) (H2). --- pom.xml | 6 + .../internal/config/H2DataSourceConfig.java | 22 ++ .../config/PostgreSqlDataSourceConfig.java | 22 ++ .../jira/profile/internal/model/Contact.java | 2 +- 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 | 5 +- src/test/resources/data.sql | 22 +- src/test/resources/db/changelog-test.sql | 251 ++++++++++++++++++ 10 files changed, 317 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/javarush/jira/common/internal/config/H2DataSourceConfig.java create mode 100644 src/main/java/com/javarush/jira/common/internal/config/PostgreSqlDataSourceConfig.java create mode 100644 src/test/resources/db/changelog-test.sql diff --git a/pom.xml b/pom.xml index c555db575..c42d83024 100644 --- a/pom.xml +++ b/pom.xml @@ -158,6 +158,12 @@ 4.0.0 + + + com.h2database + h2 + test + diff --git a/src/main/java/com/javarush/jira/common/internal/config/H2DataSourceConfig.java b/src/main/java/com/javarush/jira/common/internal/config/H2DataSourceConfig.java new file mode 100644 index 000000000..03aa6f26a --- /dev/null +++ b/src/main/java/com/javarush/jira/common/internal/config/H2DataSourceConfig.java @@ -0,0 +1,22 @@ +package com.javarush.jira.common.internal.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import javax.sql.DataSource; + +@Configuration +@Profile("nativeTest") +public class H2DataSourceConfig { + @Bean + public DataSource dataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.h2.Driver"); + dataSource.setUrl("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + return dataSource; + } +} diff --git a/src/main/java/com/javarush/jira/common/internal/config/PostgreSqlDataSourceConfig.java b/src/main/java/com/javarush/jira/common/internal/config/PostgreSqlDataSourceConfig.java new file mode 100644 index 000000000..4733b3a4e --- /dev/null +++ b/src/main/java/com/javarush/jira/common/internal/config/PostgreSqlDataSourceConfig.java @@ -0,0 +1,22 @@ +package com.javarush.jira.common.internal.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import javax.sql.DataSource; + +@Configuration +@Profile("native") +public class PostgreSqlDataSourceConfig { + @Bean + public DataSource dataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.postgresql.Driver"); + dataSource.setUrl("jdbc:postgresql://localhost:5432/jira"); + dataSource.setUsername("jira"); + dataSource.setPassword("JiraRush"); + return dataSource; + } +} \ No newline at end of file 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/data4dev/data.sql b/src/main/resources/data4dev/data.sql index d9225f640..c36155255 100644 --- a/src/main/resources/data4dev/data.sql +++ b/src/main/resources/data4dev/data.sql @@ -47,7 +47,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 68591336d..7bcee9da1 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..4061935dd 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,7 +1,8 @@ spring.cache.type: none spring: - init: - mode: always + sql: + init: + mode: always datasource: url: jdbc:postgresql://localhost:5433/jira-test username: jira diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 758d8dc6e..f2a55f443 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -2,23 +2,19 @@ TRUNCATE TABLE USER_ROLE; TRUNCATE TABLE CONTACT; TRUNCATE TABLE PROFILE; - TRUNCATE TABLE ACTIVITY; -alter -sequence ACTIVITY_ID_SEQ restart with 1; TRUNCATE TABLE TASK; -alter -sequence TASK_ID_SEQ restart with 1; TRUNCATE TABLE SPRINT; -alter -sequence SPRINT_ID_SEQ restart with 1; TRUNCATE TABLE PROJECT; -alter -sequence PROJECT_ID_SEQ restart with 1; - TRUNCATE TABLE USERS; -alter -sequence USERS_ID_SEQ restart with 1; + +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'), @@ -40,7 +36,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/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 4b532f8cfdeca8fdf6c2bdb0d59b6a6608628a32 Mon Sep 17 00:00:00 2001 From: Denys Date: Tue, 5 Aug 2025 19:14:48 +0300 Subject: [PATCH 4/8] Task 5 - Write tests for all public methods of the ProfileRestController controller --- .../web/ProfileRestControllerTest.java | 107 ++++++++++++++++++ .../profile/internal/web/ProfileTestData.java | 4 + 2 files changed, 111 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..278247af3 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 { + @Autowired + private ProfileRepository profileRepository; + + private static final String REST_URL = ProfileRestController.REST_URL; + + @Test + @WithUserDetails(value = USER_MAIL) + void getProfileWithUserMailTest() 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 getProfileWithGuestMailTest() 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 getProfileWithUnauthorizedUserTest() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateProfileWithUserMailTest() 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 updateProfileWithUserMailUnknownNotificationTest() 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 updateProfileWithUserMailInvalidUserTest() 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 updateProfileWithUserMailUnknownContactTest() 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 updateProfileWithUserMailHtmlUnsafeTest() throws Exception { + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(ProfileTestData.getWithContactHtmlUnsafeTo()))) + .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 cc0513971..8324528cf 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,11 +13,15 @@ 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"), new ContactTo("mobile", "+01234567890"), new ContactTo("website", "user.com"))); + public static ProfileTo GUEST_PROFILE_EMPTY_TO = new ProfileTo(null, Set.of(), Set.of()); From 59b49c3a3c22e699b434538b84b1c7d785432797 Mon Sep 17 00:00:00 2001 From: Denys Date: Tue, 5 Aug 2025 19:25:15 +0300 Subject: [PATCH 5/8] Task 6 - Refactor the com.javarush.jira.bugtracking.attachment.FileUtil#upload method to use a modern approach to working with the file system. --- .../jira/bugtracking/attachment/FileUtil.java | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 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..cc52d8a80 100644 --- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java +++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java @@ -7,14 +7,9 @@ import org.springframework.core.io.UrlResource; import org.springframework.web.multipart.MultipartFile; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStream; import java.net.MalformedURLException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.nio.file.*; @UtilityClass public class FileUtil { @@ -25,14 +20,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(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException ex) { + throw new IllegalRequestDataException("Failed to upload file: " + multipartFile.getOriginalFilename()); } } @@ -40,22 +34,22 @@ public static Resource download(String fileLink) { Path path = Paths.get(fileLink); try { Resource resource = new UrlResource(path.toUri()); - if (resource.exists() || resource.isReadable()) { + if (resource.exists() && resource.isReadable()) { return resource; } else { - throw new IllegalRequestDataException("Failed to download file " + resource.getFilename()); + throw new IllegalRequestDataException("Failed to download file: " + resource.getFilename()); } } catch (MalformedURLException ex) { - throw new NotFoundException("File" + fileLink + " not found"); + throw new NotFoundException("File not found: " + fileLink); } } public static void delete(String fileLink) { Path path = Paths.get(fileLink); try { - Files.delete(path); + Files.deleteIfExists(path); } catch (IOException ex) { - throw new IllegalRequestDataException("File" + fileLink + " deletion failed."); + throw new IllegalRequestDataException("File deletion failed: " + fileLink); } } From 7d4ef2ddb1f5aa6e5cd0e4fbea37960e1a10908e Mon Sep 17 00:00:00 2001 From: Denys Date: Tue, 5 Aug 2025 19:41:31 +0300 Subject: [PATCH 6/8] Task 7 - Add new functionality: adding tags to a task (REST API + implementation on the service). --- .../jira/bugtracking/task/TaskController.java | 6 ++++++ .../jira/bugtracking/task/TaskService.java | 15 +++++++++++++++ 2 files changed, 21 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..ca9e5155e 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,18 @@ 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 c4330441562390d870e83f9932b557afcc8f803b Mon Sep 17 00:00:00 2001 From: Denys Date: Tue, 5 Aug 2025 19:58:20 +0300 Subject: [PATCH 7/8] Task 8 - Add time counting: how much time the task has been in work and testing. Write 2 methods at the service level that take a task as a parameter and return the time spent. --- .../bugtracking/task/ActivityService.java | 27 +++++++++++++++++++ src/main/resources/data4dev/data.sql | 8 ++++++ 2 files changed, 35 insertions(+) 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..897b8e5dd 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; @@ -73,4 +75,29 @@ private void updateTaskIfRequired(long taskId, String activityStatus, String act } } } + public Duration getDurationInProgress(Task task) { + LocalDateTime inProgress = null; + LocalDateTime readyForReview = null; + List activities = handler.getRepository().findAllByTaskIdOrderByUpdatedDesc(task.id()); + for (Activity activity : activities) { + if ("in_progress".equals(activity.getStatusCode())) inProgress=activity.getUpdated(); + if ("ready_for_review".equals(activity.getStatusCode())) readyForReview=activity.getUpdated(); + } + if (inProgress==null||readyForReview==null) return Duration.ZERO; + else return Duration.between(inProgress, readyForReview); + + } + + public Duration getDurationInTesting(Task task) { + LocalDateTime done = null; + LocalDateTime readyForReview = null; + List activities = handler.getRepository().findAllByTaskIdOrderByUpdatedDesc(task.id()); + for (Activity activity : activities) { + if ("done".equals(activity.getStatusCode())) done=activity.getUpdated(); + if ("ready_for_review".equals(activity.getStatusCode())) readyForReview=activity.getUpdated(); + } + if (done==null||readyForReview==null) return Duration.ZERO; + else return Duration.between(readyForReview, done); + } } + diff --git a/src/main/resources/data4dev/data.sql b/src/main/resources/data4dev/data.sql index c36155255..f5d49a099 100644 --- a/src/main/resources/data4dev/data.sql +++ b/src/main/resources/data4dev/data.sql @@ -317,3 +317,11 @@ values (6, 1, '2023-05-15 09:05:10', null, 'Data', null, 3, 'epic', 'in_progress (5, 118, '2023-05-16 11:10:10', null, 'UI tab of tasks', null, null, null, null, 'high'), (11, 118, '2023-05-16 12:30:10', null, 'UI tab of tasks', null, 2, null, null, null); +insert into public.activity (author_id, task_id, updated, comment, title, description, estimate, type_code, status_code, priority_code) +values (7, 1, current_timestamp + interval '1 hour', null, null, null, null, null, 'ready_for_review', null), + (7, 1, current_timestamp + interval '2 hour', null, null, null, null, null, 'review', null), + (7, 1, current_timestamp + interval '3 hour', null, null, null, null, null, 'ready_for_test', null), + (7, 1, current_timestamp + interval '4 hour', null, null, null, null, null, 'test', null), + (7, 1, current_timestamp + interval '5 hour', null, null, null, null, null, 'done', null); + + From deb92bd963853f536430039e5a974e06e7402b9a Mon Sep 17 00:00:00 2001 From: Denys Date: Tue, 5 Aug 2025 22:04:17 +0300 Subject: [PATCH 8/8] updated README --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 719b268f5..4acf2bc07 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,11 @@ - https://habr.com/ru/articles/259055/ Список выполненных задач: -... \ No newline at end of file +1. Understand the project structure (onboarding). +2. Removed VK and Ya. +3. Introduce sensitive information to a specific file. +4. Change of tests from PostgreSQL to in-memory database. +5. Write tests for all public methods of the ProfileRestController controller. +6. Refactor the com.javarush.jira.bugtracking.attachment.FileUtil#upload method to use a modern approach to working with the file system. +7. Add new functionality: adding tags to a task (REST API + implementation on the service). +8. Add time counting: how much time the task has been in work and testing. Write 2 methods at the service level that take a task as a parameter and return the time spent. \ No newline at end of file