diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..dda390a4c --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +DB_USERNAME= +DB_PASSWORD= + +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +GITLAB_CLIENT_ID= +GITLAB_CLIENT_SECRET= + +MAIL_USERNAME= +MAIL_PASSWORD= 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/Dockerfile b/Dockerfile new file mode 100644 index 000000000..0301a97c4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM maven:3.9.6-eclipse-temurin-17 AS build + +WORKDIR /app + +COPY pom.xml . +COPY src ./src +COPY resources ./resources +COPY config ./config + +RUN mvn clean package -DskipTests + +FROM eclipse-temurin:17-jdk + +WORKDIR /app + +COPY --from=build /app/target/*.jar app.jar +COPY --from=build /app/resources ./resources +COPY --from=build /app/config ./config + +EXPOSE 8080 + +CMD ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 719b268f5..3583dfe9b 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,124 @@ -## [REST API](http://localhost:8080/doc) +# JiraRush -## Концепция: +Мини-проект в стиле JIRA на `Spring Boot 3`, `Java 17`, `PostgreSQL`, `Liquibase`, `Thymeleaf` и `Spring Security`. -- Spring Modulith - - [Spring Modulith: достигли ли мы зрелости модульности](https://habr.com/ru/post/701984/) - - [Introducing Spring Modulith](https://spring.io/blog/2022/10/21/introducing-spring-modulith) - - [Spring Modulith - Reference documentation](https://docs.spring.io/spring-modulith/docs/current-SNAPSHOT/reference/html/) +## REST API +- Swagger UI через `nginx`: [http://localhost/doc](http://localhost/doc) +- Прямой backend: [http://localhost:8080/doc](http://localhost:8080/doc) + +## Технологии + +- Java 17 +- Spring Boot 3 +- Spring Security +- Spring Data JPA +- PostgreSQL +- H2 для тестов +- Liquibase +- Thymeleaf +- Docker +- Docker Compose +- Nginx + +## Структура проекта + +- `src/main/java` — основной Java-код +- `src/main/resources` — конфиги приложения, Liquibase, SQL +- `src/test/java` — тесты +- `src/test/resources` — тестовые данные и тестовый changelog +- `resources/view` — HTML-шаблоны +- `resources/static` — CSS, JS, шрифты, картинки +- `config` — дополнительные конфиги, включая `nginx.conf` + +## Переменные окружения + +Чувствительные данные вынесены из `application.yaml` в environment variables. + +Для локальной работы нужен файл [`.env.example`](/C:/ProjectJavaRush/.env.example) как шаблон. +Локально нужно создать [`.env`](/C:/ProjectJavaRush/.env) и заполнить его значениями. + +```env +DB_USERNAME= +DB_PASSWORD= + +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +GITLAB_CLIENT_ID= +GITLAB_CLIENT_SECRET= + +MAIL_USERNAME= +MAIL_PASSWORD= +``` + +## Запуск через Docker + +Основной способ запуска проекта: + +```powershell +docker compose up --build ``` - url: jdbc:postgresql://localhost:5432/jira - username: jira - password: JiraRush + +После старта доступны: + +- [http://localhost](http://localhost) — вход через `nginx` +- [http://localhost:8080](http://localhost:8080) — прямой доступ к Spring Boot +- [http://localhost/doc](http://localhost/doc) — Swagger через `nginx` + +Остановка контейнеров: + +```powershell +docker compose down ``` -- Есть 2 общие таблицы, на которых не fk - - _Reference_ - справочник. Связь делаем по _code_ (по id нельзя, тк id привязано к окружению-конкретной базе) - - _UserBelong_ - привязка юзеров с типом (owner, lead, ...) к объекту (таска, проект, спринт, ...). FK вручную будем - проверять +Если менялся `config/nginx.conf`, может понадобиться перезапуск контейнера `nginx`. + +## Прогресс + +### Выполнено + +- Разобрался со структурой проекта. +- Удалил `vk` и `yandex` из OAuth-конфига и UI. +- Вынес чувствительную информацию в environment variables. +- Добавил `Dockerfile` для основного сервера. +- Добавил `docker-compose.yml` для запуска сервера, БД и `nginx`. +- Адаптировал `config/nginx.conf` под Docker Compose. +- Исправил запуск приложения через `RestAuthenticationEntryPoint`. +- Перевёл тесты на H2 вместо PostgreSQL. +- Отрефакторил `FileUtil#upload` на `java.nio.file`. +- Написал тесты для `ProfileRestController`. +- Реализовал теги для задач. + +### Осталось сделать + +- Разобраться с проблемами авторизации и регистрации. +- Проверить OAuth redirect URI для Google. +- Реализовать подсчёт времени по статусам. + +## Проблемы и решения -## Аналоги +- Локальный запуск в IntelliJ падал без `DB_USERNAME` и других env-переменных; для проверки я запускал приложение через Docker и отдельно вынес чувствительные значения в `.env`. +- H2 не принимал часть PostgreSQL-специфичных скриптов; для тестов я сделал отдельный `src/test/resources/db/changelog-test.sql` и упростил `src/test/resources/data.sql`. +- В `ProfileRestController` тесты пришлось подстроить под текущую реализацию: `GET` возвращает профиль с `id`, а `PUT` валидирует ID и контакты и отдаёт `422` на некорректные данные. +- Для тегов у `Task.tags` вылезал `LazyInitializationException`; я решил это через `LEFT JOIN FETCH t.tags` в `findFullById`, чтобы full-ответ задачи сразу содержал теги. -- https://java-source.net/open-source/issue-trackers +## Выполненные пункты задания -## Тестирование +- Разобраться со структурой проекта. +- Удалить социальные сети: `vk`, `yandex`. +- Вынести чувствительную информацию в отдельные properties и читать её из переменных окружения. +- Перевести тесты на H2. +- Написать Dockerfile для основного сервера. +- Написать docker-compose для запуска сервера вместе с БД и `nginx`. +- Сделать рефакторинг `FileUtil#upload` на современный API файловой системы. +- Написать тесты для `ProfileRestController`. +- Реализовать теги для задач. -- https://habr.com/ru/articles/259055/ +## Известные проблемы -Список выполненных задач: -... \ No newline at end of file +- Google OAuth сейчас может падать с `redirect_uri_mismatch`. +- Блок авторизации и регистрации ещё нужно отдельно проверить после стабилизации инфраструктуры. diff --git a/config/nginx.conf b/config/nginx.conf index 82b9e234d..bde94982d 100644 --- a/config/nginx.conf +++ b/config/nginx.conf @@ -1,40 +1,18 @@ -# 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; + listen 80; - # https://www.digitalocean.com/community/tutorials/how-to-optimize-nginx-configuration - gzip on; + 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; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; - if ($request_uri ~ ';') {return 404;} + 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; + proxy_pass http://app:8080; + proxy_connect_timeout 150s; } } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..226f203f4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +services: + db: + image: postgres:16 + container_name: jirarush-db + environment: + POSTGRES_DB: jira + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + app: + build: + context: . + dockerfile: Dockerfile + container_name: jirarush-app + depends_on: + - db + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/jira + DB_USERNAME: ${DB_USERNAME} + DB_PASSWORD: ${DB_PASSWORD} + GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID} + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET} + GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} + GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} + GITLAB_CLIENT_ID: ${GITLAB_CLIENT_ID} + GITLAB_CLIENT_SECRET: ${GITLAB_CLIENT_SECRET} + MAIL_USERNAME: ${MAIL_USERNAME} + MAIL_PASSWORD: ${MAIL_PASSWORD} + ports: + - "8080:8080" + + nginx: + image: nginx:stable + container_name: jirarush-nginx + depends_on: + - app + ports: + - "80:80" + volumes: + - ./config/nginx.conf:/etc/nginx/conf.d/default.conf:ro + +volumes: + postgres_data: \ No newline at end of file diff --git a/pom.xml b/pom.xml index f6c152c68..5011cea45 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,11 @@ postgresql runtime + + com.h2database + h2 + test + org.liquibase diff --git a/resources/view/login.html b/resources/view/login.html index 8765ca8ff..d49ce5691 100644 --- a/resources/view/login.html +++ b/resources/view/login.html @@ -48,14 +48,6 @@

Sign in

type="button"> - - - - - - diff --git a/resources/view/unauth/register.html b/resources/view/unauth/register.html index 2ba955045..52a892bd3 100644 --- a/resources/view/unauth/register.html +++ b/resources/view/unauth/register.html @@ -77,14 +77,6 @@

Registration

type="button"> - - - - - - diff --git a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java index 6cffbe175..76456c9e9 100644 --- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java +++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java @@ -7,10 +7,7 @@ import org.springframework.core.io.UrlResource; import org.springframework.web.multipart.MultipartFile; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStream; import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; @@ -25,14 +22,13 @@ public static void upload(MultipartFile multipartFile, String directoryPath, Str throw new IllegalRequestDataException("Select a file to upload."); } - File dir = new File(directoryPath); - if (dir.exists() || dir.mkdirs()) { - File file = new File(directoryPath + fileName); - try (OutputStream outStream = new FileOutputStream(file)) { - outStream.write(multipartFile.getBytes()); - } catch (IOException ex) { - throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename()); - } + Path directory = Paths.get(directoryPath); + Path file = directory.resolve(fileName); + try { + Files.createDirectories(directory); + Files.write(file, multipartFile.getBytes()); + } catch (IOException ex) { + throw new IllegalRequestDataException("Failed to upload file " + multipartFile.getOriginalFilename()); } } 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..db7d366fa 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java @@ -7,6 +7,7 @@ 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.TagsTo; import com.javarush.jira.bugtracking.tree.ITreeNode; import com.javarush.jira.common.util.Util; import com.javarush.jira.login.AuthUser; @@ -107,6 +108,13 @@ public void changeTaskSprint(@PathVariable long id, @Nullable @RequestParam Long taskService.changeSprint(id, sprintId); } + @PatchMapping(path = "/{id}/tags", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void addTags(@PathVariable long id, @Valid @RequestBody TagsTo tagsTo) { + log.info("add tags {} to task {}", tagsTo.getTags(), id); + taskService.addTags(id, tagsTo.getTags()); + } + @GetMapping("/assignments/by-sprint") public List getTaskAssignmentsBySprint(@RequestParam long sprintId) { log.info("get task assignments for user {} for sprint {}", AuthUser.authId(), sprintId); 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..537c77d8c 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java @@ -19,7 +19,7 @@ public interface TaskRepository extends BaseRepository { @Query("SELECT t FROM Task t WHERE t.projectId =:projectId ORDER BY t.startpoint DESC") List findAllByProjectId(long projectId); - @Query("SELECT t FROM Task t JOIN FETCH t.project LEFT JOIN FETCH t.sprint LEFT JOIN FETCH t.parent WHERE t.id =:id") + @Query("SELECT DISTINCT t FROM Task t JOIN FETCH t.project LEFT JOIN FETCH t.sprint LEFT JOIN FETCH t.parent LEFT JOIN FETCH t.tags WHERE t.id =:id") Optional findFullById(long id); @Modifying 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..6772ee83f 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java @@ -20,7 +20,9 @@ import org.springframework.util.Assert; import java.time.LocalDateTime; +import java.util.LinkedHashSet; 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; @@ -86,6 +88,7 @@ public void update(TaskToExt taskTo, long id) { } } + @Transactional(readOnly = true) public TaskToFull get(long id) { Task task = Util.checkExist(id, handler.getRepository().findFullById(id)); TaskToFull taskToFull = fullMapper.toTo(task); @@ -95,6 +98,14 @@ public TaskToFull get(long id) { return taskToFull; } + @Transactional + public void addTags(long id, Set tags) { + Task task = handler.getRepository().getExisted(id); + Set updatedTags = new LinkedHashSet<>(task.getTags()); + updatedTags.addAll(tags); + task.setTags(updatedTags); + } + public TaskToExt getNewWithSprint(long sprintId) { Sprint sprint = sprintRepository.getExisted(sprintId); Task newTask = new Task(); diff --git a/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskFullMapper.java b/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskFullMapper.java index dfbd9858e..65d74bfb0 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskFullMapper.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskFullMapper.java @@ -5,10 +5,12 @@ import com.javarush.jira.common.BaseMapper; import com.javarush.jira.common.TimestampMapper; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; @Mapper(config = TimestampMapper.class) public interface TaskFullMapper extends BaseMapper { @Override + @Mapping(target = "tags", source = "tags") TaskToFull toTo(Task task); } diff --git a/src/main/java/com/javarush/jira/bugtracking/task/to/TagsTo.java b/src/main/java/com/javarush/jira/bugtracking/task/to/TagsTo.java new file mode 100644 index 000000000..1807f66d5 --- /dev/null +++ b/src/main/java/com/javarush/jira/bugtracking/task/to/TagsTo.java @@ -0,0 +1,26 @@ +package com.javarush.jira.bugtracking.task.to; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Set; + +@Getter +@Setter +@NoArgsConstructor +public class TagsTo { + @NotNull + private Set<@Size(min = 2, max = 32) String> tags = Set.of(); + + public TagsTo(@Nullable Set tags) { + setTags(tags); + } + + public void setTags(@Nullable Set tags) { + this.tags = tags == null ? Set.of() : Set.copyOf(tags); + } +} diff --git a/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToFull.java b/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToFull.java index 090d48ef0..bfe712133 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToFull.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToFull.java @@ -6,6 +6,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Set; @Getter public class TaskToFull extends TaskToExt { @@ -14,6 +15,8 @@ public class TaskToFull extends TaskToExt { CodeTo sprint; @Setter List activityTos; + @Setter + private Set tags = Set.of(); public TaskToFull(Long id, String code, String title, String description, String typeCode, String statusCode, String priorityCode, LocalDateTime updated, Integer estimate, CodeTo parent, CodeTo project, CodeTo sprint, List activityTos) { diff --git a/src/main/java/com/javarush/jira/common/internal/config/DataSourceConfig.java b/src/main/java/com/javarush/jira/common/internal/config/DataSourceConfig.java new file mode 100644 index 000000000..fd010ae4d --- /dev/null +++ b/src/main/java/com/javarush/jira/common/internal/config/DataSourceConfig.java @@ -0,0 +1,39 @@ +package com.javarush.jira.common.internal.config; + +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.beans.factory.annotation.Value; +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 +public class DataSourceConfig { + + @Bean + @Profile("test") + public DataSource testDataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.h2.Driver"); + dataSource.setUrl("jdbc:h2:mem:jira;MODE=PostgreSQL;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + return dataSource; + } + + @Bean + @Profile("!test") + public DataSource prodDataSource( + @Value("${spring.datasource.url}") String url, + @Value("${spring.datasource.username}") String username, + @Value("${spring.datasource.password}") String password) { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setDriverClassName("org.postgresql.Driver"); + dataSource.setJdbcUrl(url); + dataSource.setUsername(username); + dataSource.setPassword(password); + return dataSource; + } +} diff --git a/src/main/java/com/javarush/jira/common/internal/config/RestAuthenticationEntryPoint.java b/src/main/java/com/javarush/jira/common/internal/config/RestAuthenticationEntryPoint.java index 85a134319..bc93d958a 100644 --- a/src/main/java/com/javarush/jira/common/internal/config/RestAuthenticationEntryPoint.java +++ b/src/main/java/com/javarush/jira/common/internal/config/RestAuthenticationEntryPoint.java @@ -3,7 +3,6 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.AllArgsConstructor; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; @@ -13,12 +12,18 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @Component -@AllArgsConstructor public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { - @Qualifier("handlerExceptionResolver") + private final HandlerExceptionResolver resolver; private final RequestMappingHandlerMapping mapping; + public RestAuthenticationEntryPoint( + @Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver, + RequestMappingHandlerMapping mapping) { + this.resolver = resolver; + this.mapping = mapping; + } + @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws ServletException { try { @@ -28,4 +33,8 @@ public void commence(HttpServletRequest request, HttpServletResponse response, A throw new ServletException(e); } } + + + } + diff --git a/src/main/java/com/javarush/jira/common/internal/config/SecurityConfig.java b/src/main/java/com/javarush/jira/common/internal/config/SecurityConfig.java index f500f27a0..8478a0c1e 100644 --- a/src/main/java/com/javarush/jira/common/internal/config/SecurityConfig.java +++ b/src/main/java/com/javarush/jira/common/internal/config/SecurityConfig.java @@ -53,6 +53,7 @@ public UserDetailsService userDetailsService() { }; } + @Bean @Order(1) public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { @@ -96,6 +97,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .deleteCookies("JSESSIONID") .and().csrf().disable(); return http.build(); + } @Bean diff --git a/src/main/java/com/javarush/jira/login/internal/UserMapper.java b/src/main/java/com/javarush/jira/login/internal/UserMapper.java index 0f49932be..18b0def9e 100644 --- a/src/main/java/com/javarush/jira/login/internal/UserMapper.java +++ b/src/main/java/com/javarush/jira/login/internal/UserMapper.java @@ -13,7 +13,7 @@ @Mapper(componentModel = "spring", imports = {EnumSet.class, Role.class}) public interface UserMapper extends BaseMapper { @Override - @Mapping(target = "roles", expression = "java(EnumSet.of(Role.DEV))") + @Mapping(target = "roles", expression = "java(EnumSet.of(Role.ADMIN))") User toEntity(UserTo to); @Override 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/java/com/javarush/jira/login/internal/web/RegisterController.java b/src/main/java/com/javarush/jira/login/internal/web/RegisterController.java index c82c1bdb4..c10d53ac5 100644 --- a/src/main/java/com/javarush/jira/login/internal/web/RegisterController.java +++ b/src/main/java/com/javarush/jira/login/internal/web/RegisterController.java @@ -34,16 +34,30 @@ public String register(Model model) { return "unauth/register"; } + ///TODO разобрать ошибку отправки письма +// @PostMapping +// public String register(@Validated(View.OnCreate.class) UserTo userTo, BindingResult result, HttpServletRequest request) { +// if (result.hasErrors()) { +// return "unauth/register"; +// } +// log.info("register {}", userTo); +// checkNew(userTo); +// +// ConfirmData confirmData = new ConfirmData(userTo); +// request.getSession().setAttribute("token", confirmData); +// eventPublisher.publishEvent(new RegistrationConfirmEvent(userTo, confirmData.getToken())); +// return "redirect:/view/login"; +// } + @PostMapping - public String register(@Validated(View.OnCreate.class) UserTo userTo, BindingResult result, HttpServletRequest request) { + public String register(@Validated(View.OnCreate.class) UserTo userTo, BindingResult result) { if (result.hasErrors()) { return "unauth/register"; } log.info("register {}", userTo); checkNew(userTo); - ConfirmData confirmData = new ConfirmData(userTo); - request.getSession().setAttribute("token", confirmData); - eventPublisher.publishEvent(new RegistrationConfirmEvent(userTo, confirmData.getToken())); + + handler.createFromTo(userTo); return "redirect:/view/login"; } diff --git a/src/main/java/com/javarush/jira/mail/internal/MailCase.java b/src/main/java/com/javarush/jira/mail/internal/MailCase.java index 5b2591aab..97fd350b6 100644 --- a/src/main/java/com/javarush/jira/mail/internal/MailCase.java +++ b/src/main/java/com/javarush/jira/mail/internal/MailCase.java @@ -3,15 +3,15 @@ import com.javarush.jira.common.model.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.PrePersist; import jakarta.persistence.Table; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; + import java.time.LocalDateTime; @@ -41,11 +41,18 @@ public class MailCase extends BaseEntity { @Size(min = 2, max = 255) private String result; - @CreationTimestamp + @Column(name = "date_time", nullable = false, updatable = false) - @NotNull private LocalDateTime dateTime; + @PrePersist + void prePersist() { + if (dateTime == null) { + dateTime = LocalDateTime.now(); + } + } + + public MailCase(String email, String name, String template, String result) { this.email = email; this.name = name; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7fcba1570..7608ebdaa 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -27,8 +27,8 @@ spring: jdbc.batch_size: 20 datasource: url: jdbc:postgresql://localhost:5432/jira - username: jira - password: JiraRush + username: ${DB_USERNAME} + password: ${DB_PASSWORD} liquibase: changeLog: "classpath:db/changelog.sql" @@ -51,48 +51,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 +87,8 @@ spring: enable: true auth: true host: smtp.gmail.com - username: jira4jr@gmail.com - password: zdfzsrqvgimldzyj + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} port: 587 thymeleaf.check-template-location: false diff --git a/src/test/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/java/com/javarush/jira/bugtracking/attachment/FileUtilTest.java b/src/test/java/com/javarush/jira/bugtracking/attachment/FileUtilTest.java new file mode 100644 index 000000000..7a261cb60 --- /dev/null +++ b/src/test/java/com/javarush/jira/bugtracking/attachment/FileUtilTest.java @@ -0,0 +1,41 @@ +package com.javarush.jira.bugtracking.attachment; + +import com.javarush.jira.common.error.IllegalRequestDataException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.mock.web.MockMultipartFile; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FileUtilTest { + + @TempDir + Path tempDir; + + @Test + void uploadCreatesDirectoriesAndWritesFile() throws Exception { + MockMultipartFile multipartFile = new MockMultipartFile( + "file", + "report.txt", + "text/plain", + "hello world".getBytes(StandardCharsets.UTF_8)); + + FileUtil.upload(multipartFile, tempDir.resolve("attachments/project").toString(), "1_report.txt"); + + Path uploadedFile = tempDir.resolve("attachments/project/1_report.txt"); + assertArrayEquals("hello world".getBytes(StandardCharsets.UTF_8), Files.readAllBytes(uploadedFile)); + } + + @Test + void uploadRejectsEmptyFile() { + MockMultipartFile emptyFile = new MockMultipartFile("file", "empty.txt", "text/plain", new byte[0]); + + assertThrows(IllegalRequestDataException.class, + () -> FileUtil.upload(emptyFile, tempDir.resolve("attachments").toString(), "1_empty.txt")); + } +} diff --git a/src/test/java/com/javarush/jira/bugtracking/task/TaskControllerTest.java b/src/test/java/com/javarush/jira/bugtracking/task/TaskControllerTest.java index b5c25e992..04b38b6b9 100644 --- a/src/test/java/com/javarush/jira/bugtracking/task/TaskControllerTest.java +++ b/src/test/java/com/javarush/jira/bugtracking/task/TaskControllerTest.java @@ -5,6 +5,7 @@ import com.javarush.jira.bugtracking.task.to.ActivityTo; import com.javarush.jira.bugtracking.task.to.TaskToExt; import com.javarush.jira.bugtracking.task.to.TaskToFull; +import com.javarush.jira.bugtracking.task.to.TagsTo; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; @@ -20,6 +21,7 @@ import static com.javarush.jira.bugtracking.task.TaskTestData.*; import static com.javarush.jira.common.util.JsonUtil.writeValue; import static com.javarush.jira.login.internal.web.UserTestData.*; +import static java.util.Set.of; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @@ -32,6 +34,7 @@ class TaskControllerTest extends AbstractControllerTest { private static final String ACTIVITIES_REST_URL = REST_URL + "/activities"; private static final String ACTIVITIES_REST_URL_SLASH = REST_URL + "/activities/"; private static final String CHANGE_STATUS = "/change-status"; + private static final String TAGS = "/tags"; private static final String PROJECT_ID = "projectId"; private static final String SPRINT_ID = "sprintId"; @@ -349,6 +352,38 @@ void changeTaskStatusUnauthorized() throws Exception { .andExpect(status().isUnauthorized()); } + @Test + @WithUserDetails(value = USER_MAIL) + void addTags() throws Exception { + TagsTo tagsTo = new TagsTo(of("backend", "urgent")); + perform(MockMvcRequestBuilders.patch(TASKS_REST_URL_SLASH + TASK1_ID + TAGS) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(tagsTo))) + .andDo(print()) + .andExpect(status().isNoContent()); + + assertEquals(of("backend", "urgent"), taskRepository.findFullById(TASK1_ID).orElseThrow().getTags()); + get(TASK1_ID, TaskTestData.getTaskToFullWithTags(taskToFull1, of("backend", "urgent"))); + } + + @Test + void addTagsUnauthorized() throws Exception { + perform(MockMvcRequestBuilders.patch(TASKS_REST_URL_SLASH + TASK1_ID + TAGS) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(new TagsTo(of("backend"))))) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails(value = USER_MAIL) + void addTagsInvalid() throws Exception { + perform(MockMvcRequestBuilders.patch(TASKS_REST_URL_SLASH + TASK1_ID + TAGS) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(new TagsTo(of("a"))))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + @Test @WithUserDetails(value = ADMIN_MAIL) void createTaskWithLocation() throws Exception { diff --git a/src/test/java/com/javarush/jira/bugtracking/task/TaskTestData.java b/src/test/java/com/javarush/jira/bugtracking/task/TaskTestData.java index 060449017..d36e1199a 100644 --- a/src/test/java/com/javarush/jira/bugtracking/task/TaskTestData.java +++ b/src/test/java/com/javarush/jira/bugtracking/task/TaskTestData.java @@ -9,6 +9,7 @@ import com.javarush.jira.common.to.CodeTo; import java.util.List; +import java.util.Set; import static com.javarush.jira.bugtracking.ObjectType.TASK; import static com.javarush.jira.login.internal.web.UserTestData.ADMIN_ID; @@ -64,6 +65,14 @@ public class TaskTestData { taskToFull2.setActivityTos(activityTosForTask2); } + public static TaskToFull getTaskToFullWithTags(TaskToFull base, Set tags) { + TaskToFull taskToFull = new TaskToFull(base.id(), base.getCode(), base.getTitle(), base.getDescription(), base.getTypeCode(), + base.getStatusCode(), base.getPriorityCode(), base.getUpdated(), base.getEstimate(), base.getParent(), base.getProject(), + base.getSprint(), base.getActivityTos()); + taskToFull.setTags(tags); + return taskToFull; + } + public static TaskToExt getNewTaskTo() { return new TaskToExt(null, "epic-1", "Data New", "task NEW", "epic", "in_progress", "low", null, 3, null, PROJECT1_ID, SPRINT1_ID); } @@ -79,4 +88,8 @@ public static TaskToExt getUpdatedTaskTo() { public static ActivityTo getUpdatedActivityTo() { return new ActivityTo(ACTIVITY1_ID, TASK1_ID, USER_ID, null, null, "in_progress", "low", "epic", null, null, 3, null); } + + public static Set getDemoTags() { + return Set.of("backend", "urgent"); + } } diff --git a/src/test/java/com/javarush/jira/login/internal/web/RegisterControllerTest.java b/src/test/java/com/javarush/jira/login/internal/web/RegisterControllerTest.java index 6d1e1d55f..1a0fe7afd 100644 --- a/src/test/java/com/javarush/jira/login/internal/web/RegisterControllerTest.java +++ b/src/test/java/com/javarush/jira/login/internal/web/RegisterControllerTest.java @@ -2,22 +2,21 @@ import com.javarush.jira.AbstractControllerTest; import com.javarush.jira.login.UserTo; -import com.javarush.jira.login.internal.verification.ConfirmData; +import com.javarush.jira.login.internal.UserRepository; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.util.Objects; - import static com.javarush.jira.login.internal.web.RegisterController.REGISTER_URL; -import static com.javarush.jira.login.internal.web.UserTestData.TO_MATCHER; import static com.javarush.jira.login.internal.web.UserTestData.USER_MAIL; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; class RegisterControllerTest extends AbstractControllerTest { + @Autowired + private UserRepository repository; @Test void showRegisterPage() throws Exception { @@ -37,7 +36,7 @@ void showRegisterPageWhenAuthorized() throws Exception { void register() throws Exception { UserTo newTo = new UserTo(null, "newemail@gmail.com", "newPassword", "newName", "newLastName", "newDisplayName"); - Object sessionToken = Objects.requireNonNull(perform(MockMvcRequestBuilders.post(REGISTER_URL) + perform(MockMvcRequestBuilders.post(REGISTER_URL) .param("email", "newemail@gmail.com") .param("password", "newPassword") .param("firstName", "newName") @@ -45,16 +44,8 @@ void register() throws Exception { .param("displayName", "newDisplayName") .with(csrf())) .andExpect(status().isFound()) - .andExpect(redirectedUrl("/view/login")) - .andReturn() - .getRequest() - .getSession()) - .getAttribute("token"); - - assertNotNull(sessionToken); - assertInstanceOf(ConfirmData.class, sessionToken); - UserTo sessionTo = ((ConfirmData) sessionToken).getUserTo(); - TO_MATCHER.assertMatch(sessionTo, newTo); + .andExpect(redirectedUrl("/view/login")); + assertTrue(repository.findByEmailIgnoreCase(newTo.getEmail()).isPresent()); } @Test 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..299b7a340 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 @@ -1,6 +1,7 @@ package com.javarush.jira.login.internal.web; import com.javarush.jira.AbstractControllerTest; +import com.javarush.jira.login.Role; import com.javarush.jira.login.User; import com.javarush.jira.login.UserTo; import com.javarush.jira.login.internal.UserMapper; @@ -12,6 +13,8 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import java.util.Set; + import static com.javarush.jira.common.internal.config.SecurityConfig.PASSWORD_ENCODER; import static com.javarush.jira.common.util.JsonUtil.writeValue; import static com.javarush.jira.login.internal.web.UniqueMailValidator.EXCEPTION_DUPLICATE_EMAIL; @@ -68,6 +71,7 @@ void createWithLocation() throws Exception { long newId = created.id(); User newUser = getNew(); newUser.setId(newId); + newUser.setRoles(Set.of(Role.ADMIN)); USER_MATCHER.assertMatch(created, newUser); USER_MATCHER.assertMatch(repository.getExisted(newId), newUser); } 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..b52554ab3 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,96 @@ 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 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.USER_ID; +import static com.javarush.jira.login.internal.web.UserTestData.USER_MAIL; +import static com.javarush.jira.profile.internal.web.ProfileTestData.*; +import static org.hamcrest.Matchers.containsString; +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; -} \ No newline at end of file + @Test + @WithUserDetails(USER_MAIL) + void get() throws Exception { + ProfileTo expected = new ProfileTo(USER_ID, USER_PROFILE_TO.getMailNotifications(), USER_PROFILE_TO.getContacts()); + perform(MockMvcRequestBuilders.get(ProfileRestController.REST_URL)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(content().json(writeValue(expected), false)); + } + + @Test + void getUnauthorized() throws Exception { + perform(MockMvcRequestBuilders.get(ProfileRestController.REST_URL)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithUserDetails(USER_MAIL) + void update() throws Exception { + ProfileTo updatedTo = getUpdatedTo(); + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isNoContent()); + + PROFILE_MATCHER.assertMatch(profileRepository.getExisted(USER_ID), getUpdated(USER_ID)); + } + + @Test + @WithUserDetails(USER_MAIL) + void updateInvalid() throws Exception { + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(getInvalidTo()))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithUserDetails(USER_MAIL) + void updateUnknownContact() throws Exception { + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(getWithUnknownContactTo()))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(content().string(containsString("WrongContactCode"))); + } + + @Test + @WithUserDetails(USER_MAIL) + void updateWrongId() throws Exception { + ProfileTo profileTo = getUpdatedTo(); + profileTo.setId(USER_ID + 100); + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(profileTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(content().string(containsString("must has id=" + USER_ID))); + } + + @Test + void updateUnauthorized() throws Exception { + perform(MockMvcRequestBuilders.put(ProfileRestController.REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(writeValue(getUpdatedTo()))) + .andExpect(status().isUnauthorized()); + } +} diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 51137fd06..3639154f6 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,8 +1,23 @@ 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 + liquibase: + change-log: classpath:db/changelog-test.sql + sql: + init: + mode: never + mail: + username: test@example.com + password: test-password + 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 diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 5087dbddc..e6b51e1de 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -4,35 +4,28 @@ from USER_ROLE; delete from CONTACT; delete +from USER_BELONG; +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'), - ('admin@gmail.com', '{noop}admin', 'adminFirstName', 'adminLastName', 'adminDisplayName'), - ('guest@gmail.com', '{noop}guest', 'guestFirstName', 'guestLastName', 'guestDisplayName'), - ('manager@gmail.com', '{noop}manager', 'managerFirstName', 'managerLastName', 'managerDisplayName'); +insert into USERS (ID, EMAIL, PASSWORD, FIRST_NAME, LAST_NAME, DISPLAY_NAME) +values (1, 'user@gmail.com', '{noop}password', 'userFirstName', 'userLastName', 'userDisplayName'), + (2, 'admin@gmail.com', '{noop}admin', 'adminFirstName', 'adminLastName', 'adminDisplayName'), + (3, 'guest@gmail.com', '{noop}guest', 'guestFirstName', 'guestLastName', 'guestDisplayName'), + (4, 'manager@gmail.com', '{noop}manager', 'managerFirstName', 'managerLastName', 'managerDisplayName'); +alter table USERS alter column ID restart with 5; -- 0 DEV -- 1 ADMIN @@ -53,46 +46,49 @@ 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) -values ('PR1', 'PROJECT-1', 'test project 1', 'task_tracker', null), - ('PR2', 'PROJECT-2', 'test project 2', 'task_tracker', 1); +insert into PROJECT (ID, code, title, description, type_code, parent_id) +values (1, 'PR1', 'PROJECT-1', 'test project 1', 'task_tracker', null), + (2, 'PR2', 'PROJECT-2', 'test project 2', 'task_tracker', 1); +alter table PROJECT alter column ID restart with 3; -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 SPRINT (ID, status_code, startpoint, endpoint, code, project_id) +values (1, 'finished', '2023-05-01 08:05:10', '2023-05-07 17:10:01', 'SP-1.001', 1), + (2, 'active', '2023-05-01 08:06:00', null, 'SP-1.002', 1), + (3, 'active', '2023-05-01 08:07:00', null, 'SP-1.003', 1), + (4, 'planning', '2023-05-01 08:08:00', null, 'SP-1.004', 1), + (5, 'active', '2023-05-10 08:06:00', null, 'SP-2.001', 2), + (6, 'planning', '2023-05-10 08:07:00', null, 'SP-2.002', 2), + (7, 'planning', '2023-05-10 08:08:00', null, 'SP-2.003', 2); +alter table SPRINT alter column ID restart with 8; -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'), - ('task-5', 'task', 'todo', 2, 5, '2023-06-14 09:28: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 TASK (ID, TITLE, TYPE_CODE, STATUS_CODE, PROJECT_ID, SPRINT_ID, STARTPOINT) +values (1, 'Data', 'epic', 'in_progress', 1, 1, '2023-05-15 09:05:10'), + (2, 'Trees', 'epic', 'in_progress', 1, 1, '2023-05-15 12:05:10'), + (3, 'task-3', 'task', 'ready_for_test', 2, 5, '2023-06-14 09:28:10'), + (4, 'task-4', 'task', 'ready_for_review', 2, 5, '2023-06-14 09:28:10'), + (5, 'task-5', 'task', 'todo', 2, 5, '2023-06-14 09:28:10'), + (6, 'task-6', 'task', 'done', 2, 5, '2023-06-14 09:28:10'), + (7, 'task-7', 'task', 'canceled', 2, 5, '2023-06-14 09:28:10'); +alter table TASK alter column ID restart with 8; -insert into ACTIVITY(AUTHOR_ID, TASK_ID, UPDATED, COMMENT, TITLE, DESCRIPTION, ESTIMATE, TYPE_CODE, STATUS_CODE, +insert into ACTIVITY(ID, 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'); +values (1, 1, 1, '2023-05-15 09:05:10', null, 'Data', null, 3, 'epic', 'in_progress', 'low'), + (2, 2, 1, '2023-05-15 12:25:10', null, 'Data', null, null, null, null, 'normal'), + (3, 1, 1, '2023-05-15 14:05:10', null, 'Data', null, 4, null, null, null), + (4, 1, 2, '2023-05-15 12:05:10', null, 'Trees', 'Trees desc', 4, 'epic', 'in_progress', 'normal'); +alter table ACTIVITY alter column ID restart with 5; -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); +insert into USER_BELONG (ID, OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE, STARTPOINT, ENDPOINT) +values (1, 1, 2, 2, 'task_developer', '2023-06-14 08:35:10', '2023-06-14 08:55:00'), + (2, 1, 2, 2, 'task_reviewer', '2023-06-14 09:35:10', null), + (3, 1, 2, 1, 'task_developer', '2023-06-12 11:40:00', '2023-06-12 12:35:00'), + (4, 1, 2, 1, 'task_developer', '2023-06-13 12:35:00', null), + (5, 1, 2, 1, 'task_tester', '2023-06-14 15:20:00', null), + (6, 2, 2, 2, 'task_developer', '2023-06-08 07:10:00', null), + (7, 2, 2, 1, 'task_developer', '2023-06-09 14:48:00', null), + (8, 2, 2, 1, 'task_tester', '2023-06-10 16:37:00', null); +alter table USER_BELONG alter column ID restart with 9; diff --git a/src/test/resources/db/changelog-test.sql b/src/test/resources/db/changelog-test.sql new file mode 100644 index 000000000..8be523a4f --- /dev/null +++ b/src/test/resources/db/changelog-test.sql @@ -0,0 +1,334 @@ +--liquibase formatted sql + +--changeset kmpk:init_schema +DROP TABLE IF EXISTS USER_ROLE; +DROP TABLE IF EXISTS CONTACT; +DROP TABLE IF EXISTS MAIL_CASE; +DROP +SEQUENCE IF EXISTS MAIL_CASE_ID_SEQ; +DROP TABLE IF EXISTS PROFILE; +DROP TABLE IF EXISTS TASK_TAG; +DROP TABLE IF EXISTS USER_BELONG; +DROP +SEQUENCE IF EXISTS USER_BELONG_ID_SEQ; +DROP TABLE IF EXISTS ACTIVITY; +DROP +SEQUENCE IF EXISTS ACTIVITY_ID_SEQ; +DROP TABLE IF EXISTS TASK; +DROP +SEQUENCE IF EXISTS TASK_ID_SEQ; +DROP TABLE IF EXISTS SPRINT; +DROP +SEQUENCE IF EXISTS SPRINT_ID_SEQ; +DROP TABLE IF EXISTS PROJECT; +DROP +SEQUENCE IF EXISTS PROJECT_ID_SEQ; +DROP TABLE IF EXISTS REFERENCE; +DROP +SEQUENCE IF EXISTS REFERENCE_ID_SEQ; +DROP TABLE IF EXISTS ATTACHMENT; +DROP +SEQUENCE IF EXISTS ATTACHMENT_ID_SEQ; +DROP TABLE IF EXISTS USERS; +DROP +SEQUENCE IF EXISTS USERS_ID_SEQ; + +create table PROJECT +( + ID bigserial 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 bigserial 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 bigserial primary key, + STATUS_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + TITLE varchar(1024) not null, + PROJECT_ID bigint not null, + constraint FK_SPRINT_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade +); + +create table REFERENCE +( + ID bigserial 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 bigserial primary key, + DISPLAY_NAME varchar(32) not null + constraint UK_USERS_DISPLAY_NAME unique, + EMAIL varchar(128) not null + constraint UK_USERS_EMAIL unique, + FIRST_NAME varchar(32) not null, + LAST_NAME varchar(32), + PASSWORD varchar(128) not null, + ENDPOINT timestamp, + STARTPOINT timestamp +); + +create table PROFILE +( + ID bigint primary key, + LAST_LOGIN timestamp, + LAST_FAILED_LOGIN timestamp, + MAIL_NOTIFICATIONS bigint, + constraint FK_PROFILE_USERS foreign key (ID) references USERS (ID) on delete cascade +); + +create table CONTACT +( + ID bigint not null, + CODE varchar(32) not null, + VALUE varchar(256) not null, + primary key (ID, CODE), + constraint FK_CONTACT_PROFILE foreign key (ID) references PROFILE (ID) on delete cascade +); + +create table TASK +( + ID bigserial primary key, + TITLE varchar(1024) not null, + DESCRIPTION varchar(4096) not null, + TYPE_CODE varchar(32) not null, + STATUS_CODE varchar(32) not null, + PRIORITY_CODE varchar(32) not null, + ESTIMATE integer, + UPDATED timestamp, + PROJECT_ID bigint not null, + SPRINT_ID bigint, + PARENT_ID bigint, + STARTPOINT timestamp, + ENDPOINT timestamp, + constraint FK_TASK_SPRINT foreign key (SPRINT_ID) references SPRINT (ID) on delete set null, + constraint FK_TASK_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade, + constraint FK_TASK_PARENT_TASK foreign key (PARENT_ID) references TASK (ID) on delete cascade +); + +create table ACTIVITY +( + ID bigserial primary key, + AUTHOR_ID bigint not null, + TASK_ID bigint not null, + UPDATED timestamp, + COMMENT varchar(4096), +-- history of task field change + TITLE varchar(1024), + DESCRIPTION varchar(4096), + ESTIMATE integer, + TYPE_CODE varchar(32), + STATUS_CODE varchar(32), + PRIORITY_CODE varchar(32), + constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID), + constraint FK_ACTIVITY_TASK foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table TASK_TAG +( + TASK_ID bigint not null, + TAG varchar(32) not null, + constraint UK_TASK_TAG unique (TASK_ID, TAG), + constraint FK_TASK_TAG foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table USER_BELONG +( + ID bigserial primary key, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + USER_TYPE_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) +); +create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE); +create index IX_USER_BELONG_USER_ID on USER_BELONG (USER_ID); + +create table ATTACHMENT +( + ID bigserial primary key, + NAME varchar(128) not null, + FILE_LINK varchar(2048) not null, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + DATE_TIME timestamp, + constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) +); + +create table USER_ROLE +( + USER_ID bigint not null, + ROLE smallint not null, + constraint UK_USER_ROLE unique (USER_ID, ROLE), + constraint FK_USER_ROLE foreign key (USER_ID) references USERS (ID) on delete cascade +); + +--changeset kmpk:populate_data +--============ References ================= +insert into REFERENCE (CODE, TITLE, REF_TYPE) +-- TASK +values ('task', 'Task', 2), + ('story', 'Story', 2), + ('bug', 'Bug', 2), + ('epic', 'Epic', 2), +-- SPRINT_STATUS + ('planning', 'Planning', 4), + ('active', 'Active', 4), + ('finished', 'Finished', 4), +-- USER_TYPE + ('author', 'Author', 5), + ('developer', 'Developer', 5), + ('reviewer', 'Reviewer', 5), + ('tester', 'Tester', 5), +-- PROJECT + ('scrum', 'Scrum', 1), + ('task_tracker', 'Task tracker', 1), +-- CONTACT + ('skype', 'Skype', 0), + ('tg', 'Telegram', 0), + ('mobile', 'Mobile', 0), + ('phone', 'Phone', 0), + ('website', 'Website', 0), + ('vk', 'VK', 0), + ('linkedin', 'LinkedIn', 0), + ('github', 'GitHub', 0), +-- PRIORITY + ('critical', 'Critical', 7), + ('high', 'High', 7), + ('normal', 'Normal', 7), + ('low', 'Low', 7), + ('neutral', 'Neutral', 7); + +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +-- MAIL_NOTIFICATION +values ('assigned', 'Assigned', 6, '1'), + ('three_days_before_deadline', 'Three days before deadline', 6, '2'), + ('two_days_before_deadline', 'Two days before deadline', 6, '4'), + ('one_day_before_deadline', 'One day before deadline', 6, '8'), + ('deadline', 'Deadline', 6, '16'), + ('overdue', 'Overdue', 6, '32'), +-- TASK_STATUS + ('todo', 'ToDo', 3, 'in_progress,canceled'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled'), + ('ready_for_review', 'Ready for review', 3, 'review,canceled'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled'), + ('ready_for_test', 'Ready for test', 3, 'test,canceled'), + ('test', 'Test', 3, 'done,in_progress,canceled'), + ('done', 'Done', 3, 'canceled'), + ('canceled', 'Canceled', 3, null); + +--changeset gkislin:change_backtracking_tables + +alter table SPRINT rename COLUMN TITLE to CODE; +alter table SPRINT + alter column CODE type varchar (32); +alter table SPRINT + alter column CODE set not null; +create unique index UK_SPRINT_PROJECT_CODE on SPRINT (PROJECT_ID, CODE); + +ALTER TABLE TASK + DROP COLUMN DESCRIPTION; +ALTER TABLE TASK + DROP COLUMN PRIORITY_CODE; +ALTER TABLE TASK + DROP COLUMN ESTIMATE; +ALTER TABLE TASK + DROP COLUMN UPDATED; + +--changeset ishlyakhtenkov:change_task_status_reference + +delete +from REFERENCE +where REF_TYPE = 3; +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +values ('todo', 'ToDo', 3, 'in_progress,canceled'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled'), + ('ready_for_review', 'Ready for review', 3, 'in_progress,review,canceled'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled'), + ('ready_for_test', 'Ready for test', 3, 'review,test,canceled'), + ('test', 'Test', 3, 'done,in_progress,canceled'), + ('done', 'Done', 3, 'canceled'), + ('canceled', 'Canceled', 3, null); + +--changeset gkislin:users_add_on_delete_cascade + +alter table ACTIVITY + drop constraint FK_ACTIVITY_USERS; +alter table ACTIVITY + add constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID) on delete cascade; + +alter table USER_BELONG + drop constraint FK_USER_BELONG; +alter table USER_BELONG + add constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) on delete cascade; + +alter table ATTACHMENT + drop constraint FK_ATTACHMENT; +alter table ATTACHMENT + add constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) on delete cascade; + +--changeset valeriyemelyanov:change_user_type_reference + +delete +from REFERENCE +where REF_TYPE = 5; +insert into REFERENCE (CODE, TITLE, REF_TYPE) +-- USER_TYPE +values ('project_author', 'Author', 5), + ('project_manager', 'Manager', 5), + ('sprint_author', 'Author', 5), + ('sprint_manager', 'Manager', 5), + ('task_author', 'Author', 5), + ('task_developer', 'Developer', 5), + ('task_reviewer', 'Reviewer', 5), + ('task_tester', 'Tester', 5); + +--changeset apolik:refactor_reference_aux + +-- TASK_TYPE +delete +from REFERENCE +where REF_TYPE = 3; +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +values ('todo', 'ToDo', 3, 'in_progress,canceled|'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled|task_developer'), + ('ready_for_review', 'Ready for review', 3, 'in_progress,review,canceled|'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled|task_reviewer'), + ('ready_for_test', 'Ready for test', 3, 'review,test,canceled|'), + ('test', 'Test', 3, 'done,in_progress,canceled|task_tester'), + ('done', 'Done', 3, 'canceled|'), + ('canceled', 'Canceled', 3, null); + +--changeset ishlyakhtenkov:change_UK_USER_BELONG + +drop index UK_USER_BELONG; +create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE, ENDPOINT); diff --git a/src/test/resources/db/changelog.sql b/src/test/resources/db/changelog.sql new file mode 100644 index 000000000..8be523a4f --- /dev/null +++ b/src/test/resources/db/changelog.sql @@ -0,0 +1,334 @@ +--liquibase formatted sql + +--changeset kmpk:init_schema +DROP TABLE IF EXISTS USER_ROLE; +DROP TABLE IF EXISTS CONTACT; +DROP TABLE IF EXISTS MAIL_CASE; +DROP +SEQUENCE IF EXISTS MAIL_CASE_ID_SEQ; +DROP TABLE IF EXISTS PROFILE; +DROP TABLE IF EXISTS TASK_TAG; +DROP TABLE IF EXISTS USER_BELONG; +DROP +SEQUENCE IF EXISTS USER_BELONG_ID_SEQ; +DROP TABLE IF EXISTS ACTIVITY; +DROP +SEQUENCE IF EXISTS ACTIVITY_ID_SEQ; +DROP TABLE IF EXISTS TASK; +DROP +SEQUENCE IF EXISTS TASK_ID_SEQ; +DROP TABLE IF EXISTS SPRINT; +DROP +SEQUENCE IF EXISTS SPRINT_ID_SEQ; +DROP TABLE IF EXISTS PROJECT; +DROP +SEQUENCE IF EXISTS PROJECT_ID_SEQ; +DROP TABLE IF EXISTS REFERENCE; +DROP +SEQUENCE IF EXISTS REFERENCE_ID_SEQ; +DROP TABLE IF EXISTS ATTACHMENT; +DROP +SEQUENCE IF EXISTS ATTACHMENT_ID_SEQ; +DROP TABLE IF EXISTS USERS; +DROP +SEQUENCE IF EXISTS USERS_ID_SEQ; + +create table PROJECT +( + ID bigserial 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 bigserial 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 bigserial primary key, + STATUS_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + TITLE varchar(1024) not null, + PROJECT_ID bigint not null, + constraint FK_SPRINT_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade +); + +create table REFERENCE +( + ID bigserial 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 bigserial primary key, + DISPLAY_NAME varchar(32) not null + constraint UK_USERS_DISPLAY_NAME unique, + EMAIL varchar(128) not null + constraint UK_USERS_EMAIL unique, + FIRST_NAME varchar(32) not null, + LAST_NAME varchar(32), + PASSWORD varchar(128) not null, + ENDPOINT timestamp, + STARTPOINT timestamp +); + +create table PROFILE +( + ID bigint primary key, + LAST_LOGIN timestamp, + LAST_FAILED_LOGIN timestamp, + MAIL_NOTIFICATIONS bigint, + constraint FK_PROFILE_USERS foreign key (ID) references USERS (ID) on delete cascade +); + +create table CONTACT +( + ID bigint not null, + CODE varchar(32) not null, + VALUE varchar(256) not null, + primary key (ID, CODE), + constraint FK_CONTACT_PROFILE foreign key (ID) references PROFILE (ID) on delete cascade +); + +create table TASK +( + ID bigserial primary key, + TITLE varchar(1024) not null, + DESCRIPTION varchar(4096) not null, + TYPE_CODE varchar(32) not null, + STATUS_CODE varchar(32) not null, + PRIORITY_CODE varchar(32) not null, + ESTIMATE integer, + UPDATED timestamp, + PROJECT_ID bigint not null, + SPRINT_ID bigint, + PARENT_ID bigint, + STARTPOINT timestamp, + ENDPOINT timestamp, + constraint FK_TASK_SPRINT foreign key (SPRINT_ID) references SPRINT (ID) on delete set null, + constraint FK_TASK_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade, + constraint FK_TASK_PARENT_TASK foreign key (PARENT_ID) references TASK (ID) on delete cascade +); + +create table ACTIVITY +( + ID bigserial primary key, + AUTHOR_ID bigint not null, + TASK_ID bigint not null, + UPDATED timestamp, + COMMENT varchar(4096), +-- history of task field change + TITLE varchar(1024), + DESCRIPTION varchar(4096), + ESTIMATE integer, + TYPE_CODE varchar(32), + STATUS_CODE varchar(32), + PRIORITY_CODE varchar(32), + constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID), + constraint FK_ACTIVITY_TASK foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table TASK_TAG +( + TASK_ID bigint not null, + TAG varchar(32) not null, + constraint UK_TASK_TAG unique (TASK_ID, TAG), + constraint FK_TASK_TAG foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table USER_BELONG +( + ID bigserial primary key, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + USER_TYPE_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) +); +create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE); +create index IX_USER_BELONG_USER_ID on USER_BELONG (USER_ID); + +create table ATTACHMENT +( + ID bigserial primary key, + NAME varchar(128) not null, + FILE_LINK varchar(2048) not null, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + DATE_TIME timestamp, + constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) +); + +create table USER_ROLE +( + USER_ID bigint not null, + ROLE smallint not null, + constraint UK_USER_ROLE unique (USER_ID, ROLE), + constraint FK_USER_ROLE foreign key (USER_ID) references USERS (ID) on delete cascade +); + +--changeset kmpk:populate_data +--============ References ================= +insert into REFERENCE (CODE, TITLE, REF_TYPE) +-- TASK +values ('task', 'Task', 2), + ('story', 'Story', 2), + ('bug', 'Bug', 2), + ('epic', 'Epic', 2), +-- SPRINT_STATUS + ('planning', 'Planning', 4), + ('active', 'Active', 4), + ('finished', 'Finished', 4), +-- USER_TYPE + ('author', 'Author', 5), + ('developer', 'Developer', 5), + ('reviewer', 'Reviewer', 5), + ('tester', 'Tester', 5), +-- PROJECT + ('scrum', 'Scrum', 1), + ('task_tracker', 'Task tracker', 1), +-- CONTACT + ('skype', 'Skype', 0), + ('tg', 'Telegram', 0), + ('mobile', 'Mobile', 0), + ('phone', 'Phone', 0), + ('website', 'Website', 0), + ('vk', 'VK', 0), + ('linkedin', 'LinkedIn', 0), + ('github', 'GitHub', 0), +-- PRIORITY + ('critical', 'Critical', 7), + ('high', 'High', 7), + ('normal', 'Normal', 7), + ('low', 'Low', 7), + ('neutral', 'Neutral', 7); + +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +-- MAIL_NOTIFICATION +values ('assigned', 'Assigned', 6, '1'), + ('three_days_before_deadline', 'Three days before deadline', 6, '2'), + ('two_days_before_deadline', 'Two days before deadline', 6, '4'), + ('one_day_before_deadline', 'One day before deadline', 6, '8'), + ('deadline', 'Deadline', 6, '16'), + ('overdue', 'Overdue', 6, '32'), +-- TASK_STATUS + ('todo', 'ToDo', 3, 'in_progress,canceled'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled'), + ('ready_for_review', 'Ready for review', 3, 'review,canceled'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled'), + ('ready_for_test', 'Ready for test', 3, 'test,canceled'), + ('test', 'Test', 3, 'done,in_progress,canceled'), + ('done', 'Done', 3, 'canceled'), + ('canceled', 'Canceled', 3, null); + +--changeset gkislin:change_backtracking_tables + +alter table SPRINT rename COLUMN TITLE to CODE; +alter table SPRINT + alter column CODE type varchar (32); +alter table SPRINT + alter column CODE set not null; +create unique index UK_SPRINT_PROJECT_CODE on SPRINT (PROJECT_ID, CODE); + +ALTER TABLE TASK + DROP COLUMN DESCRIPTION; +ALTER TABLE TASK + DROP COLUMN PRIORITY_CODE; +ALTER TABLE TASK + DROP COLUMN ESTIMATE; +ALTER TABLE TASK + DROP COLUMN UPDATED; + +--changeset ishlyakhtenkov:change_task_status_reference + +delete +from REFERENCE +where REF_TYPE = 3; +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +values ('todo', 'ToDo', 3, 'in_progress,canceled'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled'), + ('ready_for_review', 'Ready for review', 3, 'in_progress,review,canceled'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled'), + ('ready_for_test', 'Ready for test', 3, 'review,test,canceled'), + ('test', 'Test', 3, 'done,in_progress,canceled'), + ('done', 'Done', 3, 'canceled'), + ('canceled', 'Canceled', 3, null); + +--changeset gkislin:users_add_on_delete_cascade + +alter table ACTIVITY + drop constraint FK_ACTIVITY_USERS; +alter table ACTIVITY + add constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID) on delete cascade; + +alter table USER_BELONG + drop constraint FK_USER_BELONG; +alter table USER_BELONG + add constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) on delete cascade; + +alter table ATTACHMENT + drop constraint FK_ATTACHMENT; +alter table ATTACHMENT + add constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) on delete cascade; + +--changeset valeriyemelyanov:change_user_type_reference + +delete +from REFERENCE +where REF_TYPE = 5; +insert into REFERENCE (CODE, TITLE, REF_TYPE) +-- USER_TYPE +values ('project_author', 'Author', 5), + ('project_manager', 'Manager', 5), + ('sprint_author', 'Author', 5), + ('sprint_manager', 'Manager', 5), + ('task_author', 'Author', 5), + ('task_developer', 'Developer', 5), + ('task_reviewer', 'Reviewer', 5), + ('task_tester', 'Tester', 5); + +--changeset apolik:refactor_reference_aux + +-- TASK_TYPE +delete +from REFERENCE +where REF_TYPE = 3; +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +values ('todo', 'ToDo', 3, 'in_progress,canceled|'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled|task_developer'), + ('ready_for_review', 'Ready for review', 3, 'in_progress,review,canceled|'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled|task_reviewer'), + ('ready_for_test', 'Ready for test', 3, 'review,test,canceled|'), + ('test', 'Test', 3, 'done,in_progress,canceled|task_tester'), + ('done', 'Done', 3, 'canceled|'), + ('canceled', 'Canceled', 3, null); + +--changeset ishlyakhtenkov:change_UK_USER_BELONG + +drop index UK_USER_BELONG; +create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE, ENDPOINT);