From 2a772d801950e72fff3d4abdecb53c2da734c196 Mon Sep 17 00:00:00 2001 From: CCStepan Date: Tue, 9 Jun 2026 20:19:53 +0300 Subject: [PATCH 1/8] =?UTF-8?q?test:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= =?UTF-8?q?=20=D0=B8=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../javarush/jira/AbstractControllerTest.java | 2 +- .../jira/config/DataSourceConfig.java | 2 + .../internal/web/AdminUserControllerTest.java | 2 + .../internal/web/UserControllerTest.java | 13 + .../jira/login/internal/web/UserTestData.java | 2 +- src/test/resources/application-test.yaml | 37 ++- src/test/resources/data.sql | 237 ++++++++++++------ src/test/resources/schema.sql | 126 ++++++++++ 8 files changed, 338 insertions(+), 83 deletions(-) create mode 100644 src/test/java/com/javarush/jira/config/DataSourceConfig.java create mode 100644 src/test/resources/schema.sql diff --git a/src/test/java/com/javarush/jira/AbstractControllerTest.java b/src/test/java/com/javarush/jira/AbstractControllerTest.java index 5981bae53..02f21705e 100644 --- a/src/test/java/com/javarush/jira/AbstractControllerTest.java +++ b/src/test/java/com/javarush/jira/AbstractControllerTest.java @@ -9,7 +9,7 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; //https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications -@Sql(scripts = {"classpath:db/changelog.sql", "classpath:data.sql"}, config = @SqlConfig(encoding = "UTF-8")) +@Sql(scripts = {"classpath:schema.sql", "classpath:data.sql"}, config = @SqlConfig(encoding = "UTF-8")) @AutoConfigureMockMvc //https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-mock-environment public abstract class AbstractControllerTest extends BaseTests { diff --git a/src/test/java/com/javarush/jira/config/DataSourceConfig.java b/src/test/java/com/javarush/jira/config/DataSourceConfig.java new file mode 100644 index 000000000..6a191d547 --- /dev/null +++ b/src/test/java/com/javarush/jira/config/DataSourceConfig.java @@ -0,0 +1,2 @@ +package com.javarush.jira.config;public class DataSourceConfig { +} diff --git a/src/test/java/com/javarush/jira/login/internal/web/AdminUserControllerTest.java b/src/test/java/com/javarush/jira/login/internal/web/AdminUserControllerTest.java index 43a770b22..ce29f01cd 100644 --- a/src/test/java/com/javarush/jira/login/internal/web/AdminUserControllerTest.java +++ b/src/test/java/com/javarush/jira/login/internal/web/AdminUserControllerTest.java @@ -116,6 +116,8 @@ void update() throws Exception { @WithUserDetails(value = ADMIN_MAIL) void createWithLocation() throws Exception { User newUser = getNew(); + String uniqueEmail = "test_new_" + System.currentTimeMillis() + "@gmail.com"; + newUser.setEmail(uniqueEmail); ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL) .contentType(MediaType.APPLICATION_JSON) .content(jsonWithPassword(newUser, "newPass"))) diff --git a/src/test/java/com/javarush/jira/login/internal/web/UserControllerTest.java b/src/test/java/com/javarush/jira/login/internal/web/UserControllerTest.java index d6790ff3b..ba1d841f6 100644 --- a/src/test/java/com/javarush/jira/login/internal/web/UserControllerTest.java +++ b/src/test/java/com/javarush/jira/login/internal/web/UserControllerTest.java @@ -5,10 +5,12 @@ import com.javarush.jira.login.UserTo; import com.javarush.jira.login.internal.UserMapper; import com.javarush.jira.login.internal.UserRepository; +import jakarta.transaction.Transactional; 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.annotation.Rollback; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -23,6 +25,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Transactional +@Rollback class UserControllerTest extends AbstractControllerTest { @Autowired @@ -56,9 +61,13 @@ void getUnauthorized() throws Exception { .andExpect(status().isUnauthorized()); } + + @Transactional + @Rollback @Test void createWithLocation() throws Exception { UserTo newTo = mapper.toTo(getNew()); + newTo.setEmail("test_new_" + System.currentTimeMillis() + "@gmail.com"); ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL) .contentType(MediaType.APPLICATION_JSON) .content(jsonWithPassword(newTo, newTo.getPassword()))) @@ -67,7 +76,11 @@ void createWithLocation() throws Exception { User created = USER_MATCHER.readFromJson(action); long newId = created.id(); User newUser = getNew(); + newUser.setId(newId); + newUser.setEmail(newTo.getEmail()); + newUser.setRoles(created.getRoles()); + USER_MATCHER.assertMatch(created, newUser); USER_MATCHER.assertMatch(repository.getExisted(newId), newUser); } diff --git a/src/test/java/com/javarush/jira/login/internal/web/UserTestData.java b/src/test/java/com/javarush/jira/login/internal/web/UserTestData.java index f0ce55378..1bb5d73b6 100644 --- a/src/test/java/com/javarush/jira/login/internal/web/UserTestData.java +++ b/src/test/java/com/javarush/jira/login/internal/web/UserTestData.java @@ -31,7 +31,7 @@ public class UserTestData { public static final User manager = new User(MANAGER_ID, MANAGER_MAIL, "manager", "managerFirstName", "managerLastName", "managerDisplayName", Role.MANAGER); public static User getNew() { - return new User(null, "new@gmail.com", "newPassword", "newFirstName", "newLastName", "newDisplayName", Role.DEV); + return new User(null, "test_new@gmail.com", "newPass", "newFirstName", "newLastName", "newDisplayName"); } public static User getUpdated() { diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 51137fd06..1f0266f64 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,8 +1,33 @@ -spring.cache.type: none spring: - init: - mode: always datasource: - url: jdbc:postgresql://localhost:5433/jira-test - username: jira - password: JiraRush \ No newline at end of file + url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + driver-class-name: org.h2.Driver + username: sa + password: + + liquibase: + enabled: false + + sql: + init: + mode: always + schema-locations: classpath:schema.sql + data-locations: classpath:data.sql + continue-on-error: false + + jpa: + show-sql: true + hibernate: + ddl-auto: none + properties: + hibernate: + format_sql: true + + h2: + console: + enabled: true + +logging: + level: + org.springframework.jdbc: DEBUG + org.hibernate.SQL: DEBUG \ No newline at end of file diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 5087dbddc..96d73d702 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -1,77 +1,88 @@ ---------- 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; - -insert into USERS (EMAIL, PASSWORD, FIRST_NAME, LAST_NAME, DISPLAY_NAME) -values ('user@gmail.com', '{noop}password', 'userFirstName', 'userLastName', 'userDisplayName'), +-- ===================================================== +-- ОЧИСТКА ТАБЛИЦ (с сохранением порядка FK) +-- ===================================================== +DELETE FROM REFERENCE; +DELETE FROM USER_BELONG; +DELETE FROM ACTIVITY; +DELETE FROM TASK; +DELETE FROM SPRINT; +DELETE FROM PROJECT; +DELETE FROM CONTACT; +DELETE FROM PROFILE; +DELETE FROM USER_ROLE; +DELETE FROM USERS; + +-- ===================================================== +-- СБРОС АВТОИНКРЕМЕНТОВ +-- ===================================================== +ALTER TABLE USERS ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE PROJECT ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE SPRINT ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE TASK ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE ACTIVITY ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE USER_BELONG ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE REFERENCE ALTER COLUMN ID RESTART WITH 1; + +-- ===================================================== +-- ПОЛЬЗОВАТЕЛИ +-- ===================================================== +INSERT INTO USERS (EMAIL, PASSWORD, FIRST_NAME, LAST_NAME, DISPLAY_NAME) +VALUES ('user@gmail.com', '{noop}password', 'userFirstName', 'userLastName', 'userDisplayName'), ('admin@gmail.com', '{noop}admin', 'adminFirstName', 'adminLastName', 'adminDisplayName'), ('guest@gmail.com', '{noop}guest', 'guestFirstName', 'guestLastName', 'guestDisplayName'), ('manager@gmail.com', '{noop}manager', 'managerFirstName', 'managerLastName', 'managerDisplayName'); --- 0 DEV --- 1 ADMIN --- 2 MANAGER +-- ===================================================== +-- РОЛИ ПОЛЬЗОВАТЕЛЕЙ +-- ===================================================== +INSERT INTO USER_ROLE (USER_ID, ROLE) +VALUES (1, 0), -- user: DEV + (2, 0), -- admin: DEV + (2, 1), -- admin: ADMIN + (4, 2); -- manager: MANAGER -insert into USER_ROLE (USER_ID, ROLE) -values (1, 0), - (2, 0), - (2, 1), - (4, 2); +-- ===================================================== +-- ПРОФИЛИ +-- ===================================================== +INSERT INTO PROFILE (ID, LAST_FAILED_LOGIN, LAST_LOGIN, MAIL_NOTIFICATIONS) +VALUES (1, NULL, NULL, 49), + (2, NULL, NULL, 14); -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) -values (1, 'skype', 'userSkype'), +-- ===================================================== +-- КОНТАКТЫ +-- ===================================================== +INSERT INTO CONTACT (ID, CODE, "VALUE") +VALUES (1, 'skype', 'userSkype'), (1, 'mobile', '+01234567890'), (1, 'website', 'user.com'), (2, 'github', 'adminGitHub'), (2, 'tg', 'adminTg'), (2, 'vk', 'adminVk'); - -insert into PROJECT (code, title, description, type_code, parent_id) -values ('PR1', 'PROJECT-1', 'test project 1', 'task_tracker', null), +-- ===================================================== +-- ПРОЕКТЫ +-- ===================================================== +INSERT INTO PROJECT (CODE, TITLE, DESCRIPTION, TYPE_CODE, PARENT_ID) +VALUES ('PR1', 'PROJECT-1', 'test project 1', 'task_tracker', NULL), ('PR2', 'PROJECT-2', 'test project 2', 'task_tracker', 1); -insert into SPRINT (status_code, startpoint, endpoint, code, project_id) -values ('finished', '2023-05-01 08:05:10', '2023-05-07 17:10:01', 'SP-1.001', 1), - ('active', '2023-05-01 08:06:00', null, 'SP-1.002', 1), - ('active', '2023-05-01 08:07:00', null, 'SP-1.003', 1), - ('planning', '2023-05-01 08:08:00', null, 'SP-1.004', 1), - ('active', '2023-05-10 08:06:00', null, 'SP-2.001', 2), - ('planning', '2023-05-10 08:07:00', null, 'SP-2.002', 2), - ('planning', '2023-05-10 08:08:00', null, 'SP-2.003', 2); - -insert into TASK (TITLE, TYPE_CODE, STATUS_CODE, PROJECT_ID, SPRINT_ID, STARTPOINT) -values ('Data', 'epic', 'in_progress', 1, 1, '2023-05-15 09:05:10'), +-- ===================================================== +-- СПРИНТЫ +-- ===================================================== +INSERT INTO SPRINT (STATUS_CODE, STARTPOINT, ENDPOINT, CODE, PROJECT_ID) +VALUES ('finished', '2023-05-01 08:05:10', '2023-05-07 17:10:01', 'SP-1.001', 1), + ('active', '2023-05-01 08:06:00', NULL, 'SP-1.002', 1), + ('active', '2023-05-01 08:07:00', NULL, 'SP-1.003', 1), + ('planning', '2023-05-01 08:08:00', NULL, 'SP-1.004', 1), + ('active', '2023-05-10 08:06:00', NULL, 'SP-2.001', 2), + ('planning', '2023-05-10 08:07:00', NULL, 'SP-2.002', 2), + ('planning', '2023-05-10 08:08:00', NULL, 'SP-2.003', 2); + +-- ===================================================== +-- ЗАДАЧИ +-- ===================================================== +INSERT INTO TASK (TITLE, TYPE_CODE, STATUS_CODE, PROJECT_ID, SPRINT_ID, STARTPOINT) +VALUES ('Data', 'epic', 'in_progress', 1, 1, '2023-05-15 09:05:10'), ('Trees', 'epic', 'in_progress', 1, 1, '2023-05-15 12:05:10'), ('task-3', 'task', 'ready_for_test', 2, 5, '2023-06-14 09:28:10'), ('task-4', 'task', 'ready_for_review', 2, 5, '2023-06-14 09:28:10'), @@ -79,20 +90,96 @@ values ('Data', 'epic', 'in_progress', 1, 1, '2023-05-15 09:05:10'), ('task-6', 'task', 'done', 2, 5, '2023-06-14 09:28:10'), ('task-7', 'task', 'canceled', 2, 5, '2023-06-14 09:28:10'); +-- ===================================================== +-- АКТИВНОСТИ +-- ===================================================== +INSERT INTO ACTIVITY (AUTHOR_ID, TASK_ID, UPDATED, COMMENT, TITLE, DESCRIPTION, ESTIMATE, TYPE_CODE, STATUS_CODE, PRIORITY_CODE) +VALUES (1, 1, '2023-05-15 09:05:10', NULL, 'Data', NULL, 3, 'epic', 'in_progress', 'low'), + (2, 1, '2023-05-15 12:25:10', NULL, 'Data', NULL, NULL, NULL, NULL, 'normal'), + (1, 1, '2023-05-15 14:05:10', NULL, 'Data', NULL, 4, NULL, NULL, NULL), + (1, 2, '2023-05-15 12:05:10', NULL, 'Trees', 'Trees desc', 4, 'epic', 'in_progress', 'normal'); -insert into ACTIVITY(AUTHOR_ID, TASK_ID, UPDATED, COMMENT, TITLE, DESCRIPTION, ESTIMATE, TYPE_CODE, STATUS_CODE, - PRIORITY_CODE) -values (1, 1, '2023-05-15 09:05:10', null, 'Data', null, 3, 'epic', 'in_progress', 'low'), - (2, 1, '2023-05-15 12:25:10', null, 'Data', null, null, null, null, 'normal'), - (1, 1, '2023-05-15 14:05:10', null, 'Data', null, 4, null, null, null), - (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'), - (1, 2, 2, 'task_reviewer', '2023-06-14 09:35:10', null), +-- ===================================================== +-- ПРИВЯЗКИ ПОЛЬЗОВАТЕЛЕЙ К ЗАДАЧАМ +-- ===================================================== +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'), + (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_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); + (1, 2, 1, 'task_developer', '2023-06-13 12:35: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); + +-- ===================================================== +-- СПРАВОЧНИКИ (REFERENCE) — ВСЕ ТИПЫ +-- ===================================================== + +-- Типы контактов (REF_TYPE = 0) +INSERT INTO REFERENCE (CODE, TITLE, AUX, REF_TYPE, STARTPOINT, ENDPOINT) +VALUES ('skype', 'Skype', NULL, 0, NULL, NULL), + ('tg', 'Telegram', NULL, 0, NULL, NULL), + ('mobile', 'Mobile', NULL, 0, NULL, NULL), + ('phone', 'Phone', NULL, 0, NULL, NULL), + ('website', 'Website', NULL, 0, NULL, NULL), + ('vk', 'VK', NULL, 0, NULL, NULL), + ('linkedin', 'LinkedIn', NULL, 0, NULL, NULL), + ('github', 'GitHub', NULL, 0, NULL, NULL); + +-- Типы проектов (REF_TYPE = 1) +INSERT INTO REFERENCE (CODE, TITLE, AUX, REF_TYPE, STARTPOINT, ENDPOINT) +VALUES ('scrum', 'Scrum', NULL, 1, NULL, NULL), + ('task_tracker', 'Task tracker', NULL, 1, NULL, NULL); + +-- Типы задач (REF_TYPE = 2) +INSERT INTO REFERENCE (CODE, TITLE, AUX, REF_TYPE, STARTPOINT, ENDPOINT) +VALUES ('task', 'Task', NULL, 2, NULL, NULL), + ('story', 'Story', NULL, 2, NULL, NULL), + ('bug', 'Bug', NULL, 2, NULL, NULL), + ('epic', 'Epic', NULL, 2, NULL, NULL); + +-- Статусы задач (REF_TYPE = 3) с правилами переходов и ролями +INSERT INTO REFERENCE (CODE, TITLE, AUX, REF_TYPE, STARTPOINT, ENDPOINT) +VALUES ('todo', 'ToDo', 'in_progress,canceled|', 3, NULL, NULL), + ('in_progress', 'In progress', 'ready_for_review,canceled|task_developer', 3, NULL, NULL), + ('ready_for_review', 'Ready for review', 'in_progress,review,canceled|', 3, NULL, NULL), + ('review', 'Review', 'in_progress,ready_for_test,canceled|task_reviewer', 3, NULL, NULL), + ('ready_for_test', 'Ready for test', 'review,test,canceled|', 3, NULL, NULL), + ('test', 'Test', 'done,in_progress,canceled|task_tester', 3, NULL, NULL), + ('done', 'Done', 'canceled|', 3, NULL, NULL), + ('canceled', 'Canceled', NULL, 3, NULL, NULL); + +-- Статусы спринтов (REF_TYPE = 4) +INSERT INTO REFERENCE (CODE, TITLE, AUX, REF_TYPE, STARTPOINT, ENDPOINT) +VALUES ('planning', 'Planning', NULL, 4, NULL, NULL), + ('active', 'Active', NULL, 4, NULL, NULL), + ('finished', 'Finished', NULL, 4, NULL, NULL); + +-- Типы участников (REF_TYPE = 5) +INSERT INTO REFERENCE (CODE, TITLE, AUX, REF_TYPE, STARTPOINT, ENDPOINT) +VALUES ('project_author', 'Author', NULL, 5, NULL, NULL), + ('project_manager', 'Manager', NULL, 5, NULL, NULL), + ('sprint_author', 'Author', NULL, 5, NULL, NULL), + ('sprint_manager', 'Manager', NULL, 5, NULL, NULL), + ('task_author', 'Author', NULL, 5, NULL, NULL), + ('task_developer', 'Developer', NULL, 5, NULL, NULL), + ('task_reviewer', 'Reviewer', NULL, 5, NULL, NULL), + ('task_tester', 'Tester', NULL, 5, NULL, NULL); + +-- Типы уведомлений (REF_TYPE = 6) +INSERT INTO REFERENCE (CODE, TITLE, AUX, REF_TYPE, STARTPOINT, ENDPOINT) +VALUES ('assigned', 'Assigned', '1', 6, NULL, NULL), + ('three_days_before_deadline', 'Three days before deadline', '2', 6, NULL, NULL), + ('two_days_before_deadline', 'Two days before deadline', '4', 6, NULL, NULL), + ('one_day_before_deadline', 'One day before deadline', '8', 6, NULL, NULL), + ('deadline', 'Deadline', '16', 6, NULL, NULL), + ('overdue', 'Overdue', '32', 6, NULL, NULL); + +-- Приоритеты (REF_TYPE = 7) +INSERT INTO REFERENCE (CODE, TITLE, AUX, REF_TYPE, STARTPOINT, ENDPOINT) +VALUES ('critical', 'Critical', NULL, 7, NULL, NULL), + ('high', 'High', NULL, 7, NULL, NULL), + ('normal', 'Normal', NULL, 7, NULL, NULL), + ('low', 'Low', NULL, 7, NULL, NULL), + ('neutral', 'Neutral', NULL, 7, NULL, NULL); \ No newline at end of file diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 000000000..2a18cf839 --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,126 @@ +-- Таблица пользователей +CREATE TABLE IF NOT EXISTS USERS ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + EMAIL VARCHAR(255) NOT NULL UNIQUE, + PASSWORD VARCHAR(255) NOT NULL, + FIRST_NAME VARCHAR(100), + LAST_NAME VARCHAR(100), + DISPLAY_NAME VARCHAR(255), + ENABLED BOOLEAN DEFAULT TRUE, + REGISTERED TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + STARTPOINT TIMESTAMP, + ENDPOINT TIMESTAMP +); + +-- Таблица ролей пользователей +CREATE TABLE IF NOT EXISTS USER_ROLE ( + USER_ID BIGINT NOT NULL, + ROLE INTEGER NOT NULL, + PRIMARY KEY (USER_ID, ROLE), + FOREIGN KEY (USER_ID) REFERENCES USERS(ID) ON DELETE CASCADE +); + +-- Таблица профилей +CREATE TABLE IF NOT EXISTS PROFILE ( + ID BIGINT PRIMARY KEY, + LAST_FAILED_LOGIN TIMESTAMP, + LAST_LOGIN TIMESTAMP, + MAIL_NOTIFICATIONS INTEGER, + FOREIGN KEY (ID) REFERENCES USERS(ID) ON DELETE CASCADE +); + +-- Таблица контактов +CREATE TABLE IF NOT EXISTS CONTACT ( + ID BIGINT NOT NULL, + CODE VARCHAR(50) NOT NULL, + "VALUE" VARCHAR(255) NOT NULL, + PRIMARY KEY (ID, CODE), + FOREIGN KEY (ID) REFERENCES USERS(ID) ON DELETE CASCADE +); + +-- Таблица проектов +CREATE TABLE IF NOT EXISTS PROJECT ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + CODE VARCHAR(50) NOT NULL UNIQUE, + TITLE VARCHAR(255) NOT NULL, + DESCRIPTION TEXT, + TYPE_CODE VARCHAR(50) NOT NULL, + PARENT_ID BIGINT, + STARTPOINT TIMESTAMP, + ENDPOINT TIMESTAMP, + FOREIGN KEY (PARENT_ID) REFERENCES PROJECT(ID) +); + +-- Таблица спринтов +CREATE TABLE IF NOT EXISTS SPRINT ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + STATUS_CODE VARCHAR(50) NOT NULL, + STARTPOINT TIMESTAMP, + ENDPOINT TIMESTAMP, + CODE VARCHAR(50) NOT NULL, + PROJECT_ID BIGINT NOT NULL, + FOREIGN KEY (PROJECT_ID) REFERENCES PROJECT(ID) ON DELETE CASCADE, + UNIQUE (PROJECT_ID, CODE) +); + +-- Таблица задач +CREATE TABLE IF NOT EXISTS TASK ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + TITLE VARCHAR(255) NOT NULL, + TYPE_CODE VARCHAR(50) NOT NULL, + STATUS_CODE VARCHAR(50) NOT NULL, + PROJECT_ID BIGINT NOT NULL, + SPRINT_ID BIGINT, + STARTPOINT TIMESTAMP, + ENDPOINT TIMESTAMP DEFAULT NULL, + PARENT_ID BIGINT DEFAULT NULL, + FOREIGN KEY (PROJECT_ID) REFERENCES PROJECT(ID) ON DELETE CASCADE, + FOREIGN KEY (SPRINT_ID) REFERENCES SPRINT(ID), + FOREIGN KEY (PARENT_ID) REFERENCES TASK(ID) +); + +-- Таблица активности +CREATE TABLE IF NOT EXISTS ACTIVITY ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + AUTHOR_ID BIGINT NOT NULL, + TASK_ID BIGINT NOT NULL, + UPDATED TIMESTAMP NOT NULL, + COMMENT TEXT, + TITLE VARCHAR(255), + DESCRIPTION TEXT, + ESTIMATE INTEGER, + TYPE_CODE VARCHAR(50), + STATUS_CODE VARCHAR(50), + PRIORITY_CODE VARCHAR(50), + FOREIGN KEY (AUTHOR_ID) REFERENCES USERS(ID) ON DELETE CASCADE, + FOREIGN KEY (TASK_ID) REFERENCES TASK(ID) ON DELETE CASCADE +); + +-- Таблица принадлежности пользователей к задачам +CREATE TABLE IF NOT EXISTS USER_BELONG ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + OBJECT_ID BIGINT NOT NULL, + OBJECT_TYPE INTEGER NOT NULL, + USER_ID BIGINT NOT NULL, + USER_TYPE_CODE VARCHAR(50) NOT NULL, + STARTPOINT TIMESTAMP, + ENDPOINT TIMESTAMP, + FOREIGN KEY (USER_ID) REFERENCES USERS(ID) ON DELETE CASCADE +); +-- Таблица reference (справочники) +CREATE TABLE IF NOT EXISTS REFERENCE ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + CODE VARCHAR(255) NOT NULL, + TITLE VARCHAR(255), + AUX VARCHAR(255), + REF_TYPE VARCHAR(100), + STARTPOINT TIMESTAMP, + ENDPOINT TIMESTAMP, + UNIQUE (REF_TYPE, CODE) +); +CREATE TABLE IF NOT EXISTS TASK_TAG ( + TASK_ID BIGINT NOT NULL, + TAG VARCHAR(255) NOT NULL, + PRIMARY KEY (TASK_ID, TAG), + FOREIGN KEY (TASK_ID) REFERENCES TASK(ID) ON DELETE CASCADE +); \ No newline at end of file From 19de154f113e16bc9c88050b944710546c960dbb Mon Sep 17 00:00:00 2001 From: CCStepan Date: Wed, 10 Jun 2026 19:21:57 +0300 Subject: [PATCH 2/8] Remove VK and Yandex OAuth2 authentication --- pom.xml | 1 + resources/view/login.html | 8 ----- resources/view/unauth/register.html | 8 ----- .../handler/VkOAuth2UserDataHandler.java | 35 ------------------- .../handler/YandexOAuth2UserDataHandler.java | 21 ----------- src/main/resources/application.yaml | 24 ------------- 6 files changed, 1 insertion(+), 96 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/pom.xml b/pom.xml index f6c152c68..3a63e0768 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,7 @@ org.projectlombok lombok + 1.18.36 true 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..d9660eb05 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -61,20 +61,6 @@ 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 +69,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 From 370196235bfb27f501e82a8f8181885bece31035 Mon Sep 17 00:00:00 2001 From: CCStepan Date: Wed, 10 Jun 2026 19:32:22 +0300 Subject: [PATCH 3/8] add H2 in-memory database for testing --- pom.xml | 5 +++++ src/test/java/com/javarush/jira/config/DataSourceConfig.java | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3a63e0768..0f62221b3 100644 --- a/pom.xml +++ b/pom.xml @@ -143,6 +143,11 @@ junit-platform-launcher test
+ + com.h2database + h2 + test + diff --git a/src/test/java/com/javarush/jira/config/DataSourceConfig.java b/src/test/java/com/javarush/jira/config/DataSourceConfig.java index 6a191d547..60e5858a3 100644 --- a/src/test/java/com/javarush/jira/config/DataSourceConfig.java +++ b/src/test/java/com/javarush/jira/config/DataSourceConfig.java @@ -1,2 +1,3 @@ -package com.javarush.jira.config;public class DataSourceConfig { +package com.javarush.jira.config; +public class DataSourceConfig { } From 62e5f7575922064cab1290ee702d0b3d6d2b99a8 Mon Sep 17 00:00:00 2001 From: CCStepan Date: Wed, 10 Jun 2026 22:46:19 +0300 Subject: [PATCH 4/8] Refactoring the com.javarush.jira.bugtracking.attachment.FileUtil#upload method --- .../jira/bugtracking/attachment/FileUtil.java | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 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..601dcb999 100644 --- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java +++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java @@ -7,10 +7,8 @@ import org.springframework.core.io.UrlResource; import org.springframework.web.multipart.MultipartFile; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStream; +import java.io.InputStream; import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; @@ -25,14 +23,24 @@ public static void upload(MultipartFile multipartFile, String directoryPath, Str throw new IllegalRequestDataException("Select a file to upload."); } - File dir = new File(directoryPath); - if (dir.exists() || dir.mkdirs()) { - File file = new File(directoryPath + fileName); - try (OutputStream outStream = new FileOutputStream(file)) { - outStream.write(multipartFile.getBytes()); - } catch (IOException ex) { - throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename()); + try { + Path directory = Paths.get(directoryPath); + Files.createDirectories(directory); + + Path filePath = directory.resolve(fileName).normalize(); + + if (!filePath.startsWith(directory)) { + throw new IllegalRequestDataException("Invalid file path: " + fileName); } + + try (InputStream inputStream = multipartFile.getInputStream()) { + Files.copy(inputStream, filePath); + } + + } catch (IOException ex) { + throw new IllegalRequestDataException( + "Failed to upload file: " + multipartFile.getOriginalFilename() + ex + ); } } From 7f2f5d9048ea55df3b649358e792d9f79132f8b6 Mon Sep 17 00:00:00 2001 From: CCStepan Date: Wed, 10 Jun 2026 22:56:47 +0300 Subject: [PATCH 5/8] add Dockerfile --- Dockerfile | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..41a7123f1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM maven:3.8-openjdk-17 AS build +WORKDIR /app + +COPY pom.xml . +RUN mvn dependency:go-offline -B + +COPY src src +RUN mvn clean package -DskipTests -B + +FROM openjdk:17-jre-slim +WORKDIR /app + +COPY --from=build /app/target/jira-1.0.jar app.jar + +RUN mkdir -p /app/attachments + +EXPOSE 8080 + +ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"] \ No newline at end of file From 84c69a210827a05a5a0beb36738e8f27bef45941 Mon Sep 17 00:00:00 2001 From: CCStepan Date: Thu, 11 Jun 2026 13:55:08 +0300 Subject: [PATCH 6/8] add tests for ProfileRestController --- .../web/ProfileRestControllerTest.java | 126 ++++++++++++++++++ 1 file changed, 126 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..88451ad55 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,134 @@ package com.javarush.jira.profile.internal.web; import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.profile.ProfileTo; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import static com.javarush.jira.common.util.JsonUtil.writeValue; +import static com.javarush.jira.login.internal.web.UserTestData.*; +import static com.javarush.jira.profile.internal.web.ProfileTestData.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; class ProfileRestControllerTest extends AbstractControllerTest { + private static final String REST_URL = ProfileRestController.REST_URL; + @Test + @WithUserDetails(value = USER_MAIL) + void get() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)); + } + + @Test + void getUnAuth() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails(value = GUEST_MAIL) + void getGuest() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void update() throws Exception { + ProfileTo updatedTo = getUpdatedTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isNoContent()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateEmptyBody() throws Exception { + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content("{}")) + .andDo(print()) + .andExpect(status().isNoContent()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateWithInvalidData() throws Exception { + ProfileTo invalidTo = getInvalidTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(invalidTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateWithUnknownNotification() throws Exception { + ProfileTo unknownNotificationTo = getWithUnknownNotificationTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(unknownNotificationTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateWithUnknownContactType() throws Exception { + ProfileTo unknownContactTo = getWithUnknownContactTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(unknownContactTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateWithHtmlUnsafeContent() throws Exception { + ProfileTo htmlUnsafeTo = getWithContactHtmlUnsafeTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(htmlUnsafeTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void updateUnAuth() throws Exception { + ProfileTo updatedTo = getUpdatedTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(updatedTo))) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void updateWithWrongContentType() throws Exception { + ProfileTo updatedTo = getUpdatedTo(); + + perform(MockMvcRequestBuilders.put(REST_URL) + .contentType(MediaType.TEXT_PLAIN) + .content(writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isUnsupportedMediaType()); + } } \ No newline at end of file From aaae3786a47b0a2f288e7ac74eaf31a995c1ce3f Mon Sep 17 00:00:00 2001 From: CCStepan Date: Thu, 11 Jun 2026 14:48:24 +0300 Subject: [PATCH 7/8] Add functionality for adding tags to a task --- .../javarush/jira/bugtracking/task/Task.java | 11 ++- .../jira/bugtracking/task/TaskController.java | 64 +++++++++++++++-- .../jira/bugtracking/task/TaskRepository.java | 32 +++++++++ .../jira/bugtracking/task/TaskService.java | 72 +++++++++++++++++++ .../jira/bugtracking/task/to/TagRequest.java | 30 ++++++++ .../com/javarush/jira/common/BaseMapper.java | 8 +++ 6 files changed, 208 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/javarush/jira/bugtracking/task/to/TagRequest.java diff --git a/src/main/java/com/javarush/jira/bugtracking/task/Task.java b/src/main/java/com/javarush/jira/bugtracking/task/Task.java index 6c9f4d96e..9ba46a255 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/Task.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/Task.java @@ -16,7 +16,7 @@ import java.util.List; import java.util.Set; - +import java.util.HashSet; import static com.javarush.jira.bugtracking.task.TaskUtil.checkStatusChangePossible; @Entity @@ -74,13 +74,20 @@ public class Task extends TitleEntity implements HasCode { @OneToMany(mappedBy = "taskId", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private List activities; - public Task(Long id, String title, String typeCode, String statusCode, Long parentId, long projectId, Long sprintId) { + public Task(Long id, String title, String typeCode, String statusCode, Long parentId, long projectId, Long sprintId, Set tags) { super(id, title); this.typeCode = typeCode; this.statusCode = statusCode; this.parentId = parentId; this.projectId = projectId; this.sprintId = sprintId; + this.tags = tags != null ? new HashSet<>(tags) : new HashSet<>(); + } + + public Task(Long id, String title, String typeCode, String statusCode, + Long parentId, Long projectId, Long sprintId) { + this(id, title, typeCode, statusCode, parentId, + projectId != null ? projectId : 0L, sprintId, new HashSet<>()); } public void checkAndSetStatusCode(String statusCode) { 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..1b5c034b2 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java @@ -3,10 +3,7 @@ import com.javarush.jira.bugtracking.Handlers; import com.javarush.jira.bugtracking.UserBelong; import com.javarush.jira.bugtracking.UserBelongRepository; -import com.javarush.jira.bugtracking.task.to.ActivityTo; -import com.javarush.jira.bugtracking.task.to.TaskTo; -import com.javarush.jira.bugtracking.task.to.TaskToExt; -import com.javarush.jira.bugtracking.task.to.TaskToFull; +import com.javarush.jira.bugtracking.task.to.*; import com.javarush.jira.bugtracking.tree.ITreeNode; import com.javarush.jira.common.util.Util; import com.javarush.jira.login.AuthUser; @@ -20,9 +17,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; +import java.util.*; import static com.javarush.jira.common.BaseHandler.createdResponse; @@ -151,6 +146,61 @@ public void delete(@PathVariable long id) { activityService.delete(id); } + + + @GetMapping("/{id}/tags") + @ResponseStatus(HttpStatus.OK) + public Set getTags(@PathVariable long id) { + log.info("Getting tags for task {}", id); + Set tags = taskService.getTags(id); + return tags; + } + + /** + * POST /api/tasks/{id}/tags - добавить тег к задаче + * Body: {"tag": "urgent"} + */ + @PostMapping("/{id}/tags") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void addTag(@PathVariable long id, @Valid @RequestBody TagRequest tagRequest) { + log.info("Adding tag '{}' to task {}", tagRequest.getTag(), id); + taskService.addTag(id, tagRequest.getTag()); + } + + /** + * DELETE /api/tasks/{id}/tags/{tag} - удалить тег у задачи + */ + @DeleteMapping("/{id}/tags/{tag}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void removeTag(@PathVariable long id, @PathVariable String tag) { + log.info("Removing tag '{}' from task {}", tag, id); + taskService.removeTag(id, tag); + } + + /** + * PUT /api/tasks/{id}/tags - заменить все теги задачи + * Body: ["feature", "backend", "api"] + */ + @PutMapping("/{id}/tags") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void replaceTags(@PathVariable long id, @RequestBody Set tags) { + log.info("Replacing tags for task {} with {}", id, tags); + taskService.replaceTags(id, tags); + } + + /** + * GET /api/tasks/by-tag?tag=urgent - найти все задачи по тегу + */ + @GetMapping("/by-tag") + public Set findTasksByTag(@RequestParam String tag) { + log.info("Finding tasks by tag: {}", tag); + Set tasks = taskService.findTasksByTag(tag); + List taskToList = handler.getMapper().toList(new ArrayList<>(tasks)); + return new HashSet<>(taskToList); + } + + + private record TaskTreeNode(TaskTo taskTo, List subNodes) implements ITreeNode { public TaskTreeNode(TaskTo taskTo) { this(taskTo, new LinkedList<>()); diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java index 982c4be8b..d265628a1 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java @@ -3,10 +3,12 @@ import com.javarush.jira.common.BaseRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; +import java.util.Set; @Transactional(readOnly = true) public interface TaskRepository extends BaseRepository { @@ -37,4 +39,34 @@ WITH RECURSIVE task_with_subtasks AS ( WHERE id IN (SELECT child FROM task_with_subtasks) """, nativeQuery = true) void setTaskAndSubTasksSprint(long taskId, Long sprintId); + + + @Query("SELECT t.tags FROM Task t WHERE t.id = :taskId") + Set findTagsByTaskId(@Param("taskId") Long taskId); + + + @Modifying + @Transactional + @Query(value = "INSERT INTO task_tag (task_id, tag) VALUES (:taskId, :tag)", + nativeQuery = true) + void addTag(@Param("taskId") Long taskId, @Param("tag") String tag); + + @Modifying + @Transactional + @Query(value = "DELETE FROM task_tag WHERE task_id = :taskId AND tag = :tag", + nativeQuery = true) + void removeTag(@Param("taskId") Long taskId, @Param("tag") String tag); + + @Query("SELECT CASE WHEN COUNT(t) > 0 THEN true ELSE false END " + + "FROM Task t JOIN t.tags tag WHERE t.id = :taskId AND tag = :tag") + boolean existsByTaskIdAndTag(@Param("taskId") Long taskId, @Param("tag") String tag); + + @Modifying + @Transactional + @Query(value = "DELETE FROM task_tag WHERE task_id = :taskId", + nativeQuery = true) + void deleteAllTags(@Param("taskId") Long taskId); + + @Query("SELECT t FROM Task t JOIN t.tags tag WHERE tag = :tag") + Set findTasksByTag(@Param("tag") String 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..b4e66a661 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java @@ -21,6 +21,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Set; import static com.javarush.jira.bugtracking.ObjectType.TASK; import static com.javarush.jira.bugtracking.task.TaskUtil.fillExtraFields; @@ -39,6 +40,7 @@ public class TaskService { private final SprintRepository sprintRepository; private final TaskExtMapper extMapper; private final UserBelongRepository userBelongRepository; + private final TaskRepository taskRepository; @Transactional public void changeStatus(long taskId, String statusCode) { @@ -140,4 +142,74 @@ private void checkAssignmentActionPossible(long id, String userType, boolean ass throw new DataConflictException(String.format(assign ? CANNOT_ASSIGN : CANNOT_UN_ASSIGN, userType, task.getStatusCode())); } } + + + + @Transactional(readOnly = true) + public Set getTags(Long taskId) { + if (!taskRepository.existsById(taskId)) { + throw new NotFoundException("Task with id " + taskId + " not found"); + } + return taskRepository.findTagsByTaskId(taskId); + } + + @Transactional + public void addTag(Long taskId, String tag) { + String normalizedTag = tag.trim().toLowerCase(); + + if (normalizedTag.isEmpty()) { + throw new IllegalArgumentException("Tag cannot be empty"); + } + + if (normalizedTag.length() < 2 || normalizedTag.length() > 32) { + throw new IllegalArgumentException("Tag must be between 2 and 32 characters"); + } + + if (!taskRepository.existsById(taskId)) { + throw new NotFoundException("Task with id " + taskId + " not found"); + } + + if (taskRepository.existsByTaskIdAndTag(taskId, normalizedTag)) { + throw new DataConflictException("Tag '" + normalizedTag + "' already exists for task " + taskId); + } + + taskRepository.addTag(taskId, normalizedTag); + } + + @Transactional + public void removeTag(Long taskId, String tag) { + String normalizedTag = tag.trim().toLowerCase(); + + if (!taskRepository.existsById(taskId)) { + throw new NotFoundException("Task with id " + taskId + " not found"); + } + + taskRepository.removeTag(taskId, normalizedTag); + } + + @Transactional + public void replaceTags(Long taskId, Set newTags) { + if (!taskRepository.existsById(taskId)) { + throw new NotFoundException("Task with id " + taskId + " not found"); + } + + taskRepository.deleteAllTags(taskId); + + if (newTags != null && !newTags.isEmpty()) { + for (String tag : newTags) { + String normalizedTag = tag.trim().toLowerCase(); + if (normalizedTag.length() >= 2 && normalizedTag.length() <= 32) { + taskRepository.addTag(taskId, normalizedTag); + } + } + } + } + + @Transactional(readOnly = true) + public Set findTasksByTag(String tag) { + String normalizedTag = tag.trim().toLowerCase(); + return taskRepository.findTasksByTag(normalizedTag); + } + + } diff --git a/src/main/java/com/javarush/jira/bugtracking/task/to/TagRequest.java b/src/main/java/com/javarush/jira/bugtracking/task/to/TagRequest.java new file mode 100644 index 000000000..59959a7bc --- /dev/null +++ b/src/main/java/com/javarush/jira/bugtracking/task/to/TagRequest.java @@ -0,0 +1,30 @@ +package com.javarush.jira.bugtracking.task.to; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TagRequest { + + @NotBlank(message = "Tag cannot be empty") + @Size(min = 2, max = 32, message = "Tag must be between 2 and 32 characters") + @Pattern(regexp = "^[a-zA-Z0-9_-]+$", + message = "Tag can only contain letters, numbers, underscores and hyphens") + private String tag; + + // Метод getTag() автоматически создается аннотацией @Data + // Но если Lombok не работает, добавьте вручную: + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } +} \ No newline at end of file diff --git a/src/main/java/com/javarush/jira/common/BaseMapper.java b/src/main/java/com/javarush/jira/common/BaseMapper.java index b3ae2c6e7..9af4875ba 100644 --- a/src/main/java/com/javarush/jira/common/BaseMapper.java +++ b/src/main/java/com/javarush/jira/common/BaseMapper.java @@ -1,10 +1,14 @@ package com.javarush.jira.common; +import com.javarush.jira.bugtracking.task.Task; +import com.javarush.jira.bugtracking.task.to.TaskTo; import com.javarush.jira.common.to.BaseTo; import org.mapstruct.MappingTarget; +import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Set; public interface BaseMapper { @@ -17,4 +21,8 @@ public interface BaseMapper { T toTo(E entity); List toToList(Collection entities); + + List toList(ArrayList tasks); + + Set toSet(Set tasks); } From e44dfb786cf6a75270ab2b460a7808adacced559 Mon Sep 17 00:00:00 2001 From: CCStepan Date: Thu, 11 Jun 2026 20:39:34 +0300 Subject: [PATCH 8/8] Update README.md --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 719b268f5..a7e2e8f39 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,16 @@ - https://habr.com/ru/articles/259055/ Список выполненных задач: -... \ No newline at end of file +1. Разобраться со структурой проекта (onboarding). +2. Удалить социальные сети: vk, yandex. +3. Вынести чувствительную информацию в отдельный проперти файл: +-логин +-пароль БД +-идентификаторы для OAuth регистрации/авторизации +-настройки почты +4. Переделать тесты так, чтоб во время тестов использовалась in memory БД (H2), а не PostgreSQL. +5. Написать тесты для всех публичных методов контроллера ProfileRestController. +6. Сделать рефакторинг метода com.javarush.jira.bugtracking.attachment.FileUtil#upload чтоб он использовал современный подход для работы с файловой системмой. +7. Добавить новый функционал: добавления тегов к задаче (REST API + реализация на сервисе). +8. Написать Dockerfile для основного сервера +9. Написать docker-compose файл для запуска контейнера сервера вместе с БД и nginx. Для nginx используй конфиг-файл config/nginx.conf. При необходимости файл конфига можно редактировать.