diff --git a/.gitignore b/.gitignore index cd38e2e7b..8ab303562 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ target logs attachments *.patch - +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..32de4fce4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM maven:3.9-eclipse-temurin-17 AS build + +WORKDIR /build +COPY pom.xml . + +RUN mvn dependency:go-offline +COPY src ./src + +RUN mvn clean package -DskipTests + +FROM eclipse-temurin:17-jre-alpine +WORKDIR /app + +COPY --from=build /build/target/jira-1.0.jar app.jar +COPY resources ./resources + +EXPOSE 8081 + +ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"] \ No newline at end of file diff --git a/README.md b/README.md index 719b268f5..765abb981 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,11 @@ - https://habr.com/ru/articles/259055/ Список выполненных задач: -... \ No newline at end of file +- Task 2. Remove VK and Yandex social login providers +- Task 3. Move sensitive config (DB, OAuth, mail) to env variables +- Task 4. Switch tests to H2 in-memory DB with Spring profile selection +- Task 5. Add tests for ProfileRestController (success and failure paths) +- Task 6. Refactor FileUtil#upload to use modern NIO API +- Task 8. Add task time tracking (in-progress and testing duration) +- Task 9. Add Dockerfile for the application server +- Task 10. Add Dockerfile for the application server, DB and nginx \ No newline at end of file diff --git a/config/_application-prod.yaml b/config/_application-prod.yaml index 67fd8b7c2..ba4f0141e 100644 --- a/config/_application-prod.yaml +++ b/config/_application-prod.yaml @@ -3,7 +3,7 @@ app: host-url: http://localhost spring: datasource: - url: jdbc:postgresql://localhost:5432/jira + url: jdbc:postgresql://jira-db:5432/jira username: jira password: JiraRush diff --git a/config/nginx.conf b/config/nginx.conf index 82b9e234d..6b11da4da 100644 --- a/config/nginx.conf +++ b/config/nginx.conf @@ -20,6 +20,7 @@ server { location /static/ { expires 30d; access_log off; + proxy_pass http://jira-app:8081; } location /robots.txt { access_log off; @@ -27,11 +28,11 @@ server { location ~ (/$|/view/|/ui/|/oauth2/) { expires 0m; - proxy_pass http://localhost:8080; + proxy_pass http://jira-app:8081; proxy_connect_timeout 30s; } location ~ (/api/|/doc|/swagger-ui/|/v3/api-docs/) { - proxy_pass http://localhost:8080; + proxy_pass http://jira-app:8081; proxy_connect_timeout 150s; } location / { diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..7a2950992 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,56 @@ +version: '3.9' + +services: + jira-db: + image: postgres:15-alpine + container_name: postgres-db + restart: always + environment: + POSTGRES_USER: jira + POSTGRES_PASSWORD: JiraRush + POSTGRES_DB: jira + ports: + - "5434:5432" + volumes: + - jira-db-data:/var/lib/postgresql/data + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U jira -d jira" ] + interval: 10s + timeout: 5s + retries: 5 + + app: + build: . + container_name: jira-app + restart: always + env_file: + - .env + environment: + DB_URL: jdbc:postgresql://jira-db:5432/jira +# ports: +# - "8080:8081" + volumes: + - app-logs:/app/logs + - app-attachments:/app/attachments +# - app-resources:/app/resources + depends_on: + jira-db: + condition: service_healthy + + nginx: + image: nginx:alpine + container_name: jira-nginx + restart: always + ports: + - "80:80" + volumes: + - ./config/nginx.conf:/etc/nginx/conf.d/default.conf:ro +# - app-resources:/opt/jirarush/resources:ro + depends_on: + - app + +volumes: + jira-db-data: + app-logs: + app-attachments: +# app-resources: \ No newline at end of file diff --git a/pom.xml b/pom.xml index f6c152c68..8021cf6e1 100644 --- a/pom.xml +++ b/pom.xml @@ -142,6 +142,11 @@ junit-platform-launcher test + + com.h2database + h2 + runtime + 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..b84012d59 100644 --- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java +++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java @@ -7,14 +7,12 @@ import org.springframework.core.io.UrlResource; import org.springframework.web.multipart.MultipartFile; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStream; import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; @UtilityClass public class FileUtil { @@ -25,14 +23,13 @@ public static void upload(MultipartFile multipartFile, String directoryPath, Str throw new IllegalRequestDataException("Select a file to upload."); } - File dir = new File(directoryPath); - if (dir.exists() || dir.mkdirs()) { - File file = new File(directoryPath + fileName); - try (OutputStream outStream = new FileOutputStream(file)) { - outStream.write(multipartFile.getBytes()); - } catch (IOException ex) { - throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename()); - } + try { + Path dirPath = Path.of(directoryPath); + Files.createDirectories(dirPath); + Path filePath = dirPath.resolve(fileName); + Files.copy(multipartFile.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException ex) { + throw new IllegalRequestDataException("Failed to upload file: " + multipartFile.getOriginalFilename()); } } 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..3a0819d36 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java @@ -19,8 +19,10 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; +import java.time.Duration; import java.time.LocalDateTime; import java.util.List; +import java.util.Objects; import static com.javarush.jira.bugtracking.ObjectType.TASK; import static com.javarush.jira.bugtracking.task.TaskUtil.fillExtraFields; @@ -140,4 +142,33 @@ private void checkAssignmentActionPossible(long id, String userType, boolean ass throw new DataConflictException(String.format(assign ? CANNOT_ASSIGN : CANNOT_UN_ASSIGN, userType, task.getStatusCode())); } } + + public Duration getInProgressDuration(long taskId) { + List activities = activityHandler.getRepository().findAllByTaskIdOrderByUpdatedDesc(taskId); + LocalDateTime inProgress = getStatusTime(activities, "in_progress"); + LocalDateTime readyForReview = getStatusTime(activities, "ready_for_review"); + if (inProgress == null || readyForReview == null) { + return null; + } + return Duration.between(inProgress, readyForReview); + } + + public Duration getTestingDuration(long taskId) { + List activities = activityHandler.getRepository().findAllByTaskIdOrderByUpdatedDesc(taskId); + LocalDateTime readyForReview = getStatusTime(activities, "ready_for_review"); + LocalDateTime done = getStatusTime(activities, "done"); + if (readyForReview == null || done == null) { + return null; + } + return Duration.between(readyForReview, done); + } + + private LocalDateTime getStatusTime(List activities, String statusCode) { + return activities.stream() + .filter(a -> statusCode.equals(a.getStatusCode())) + .map(Activity::getUpdated) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } } 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..496548718 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,16 @@ 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 { 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..75a27dadc 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 @@ -4,8 +4,6 @@ import com.javarush.jira.common.util.validation.View; import com.javarush.jira.login.UserTo; import com.javarush.jira.login.internal.verification.ConfirmData; -import com.javarush.jira.login.internal.verification.RegistrationConfirmEvent; -import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -35,15 +33,13 @@ public String register(Model model) { } @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/ref/ReferenceService.java b/src/main/java/com/javarush/jira/ref/ReferenceService.java index 257ab24e9..c73e8faf5 100644 --- a/src/main/java/com/javarush/jira/ref/ReferenceService.java +++ b/src/main/java/com/javarush/jira/ref/ReferenceService.java @@ -52,7 +52,7 @@ public static Map filterEnabled(Map unfilteredRefs } @PostConstruct - void initialize() { + public void initialize() { log.info("init loading"); List references = mapper.toToList(repository.findAllByOrderByIdAsc()); refSelect = references.stream() diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7fcba1570..89c40906b 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,13 +1,15 @@ # https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html app: - host-url: http://localhost:8080 - test-mail: jira4jr@gmail.com + host-url: http://localhost:8081 + test-mail: ${APP_TEST_MAIL} templates-update-cache: 5s mail-sending-props: core-pool-size: 8 max-pool-size: 100 spring: + config: + import: optional:file:.env[.properties] init: mode: never jpa: @@ -26,9 +28,9 @@ spring: # https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc jdbc.batch_size: 20 datasource: - url: jdbc:postgresql://localhost:5432/jira - username: jira - password: JiraRush + url: jdbc:postgresql://jira-db:5432/${DB_NAME} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} liquibase: changeLog: "classpath:db/changelog.sql" @@ -51,48 +53,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 @@ -110,10 +88,10 @@ spring: starttls: enable: true auth: true - host: smtp.gmail.com - username: jira4jr@gmail.com - password: zdfzsrqvgimldzyj - port: 587 + host: ${MAIL_HOST} + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} + port: ${MAIL_PORT} thymeleaf.check-template-location: false mvc.throw-exception-if-no-handler-found: true @@ -134,4 +112,5 @@ server: charset: UTF-8 # Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly enabled: true # Enable http encoding support force: true + port: 8081 springdoc.swagger-ui.path: /doc diff --git a/src/main/resources/data4dev/data.sql b/src/main/resources/data4dev/data.sql index a7d43cbad..791d81b32 100644 --- a/src/main/resources/data4dev/data.sql +++ b/src/main/resources/data4dev/data.sql @@ -54,8 +54,7 @@ values (1, 'skype', 'userSkype'), (1, 'mobile', '+01234567890'), (1, 'website', 'user.com'), (2, 'github', 'adminGitHub'), - (2, 'tg', 'adminTg'), - (2, 'vk', 'adminVk'); + (2, 'tg', 'adminTg'); delete from ATTACHMENT; @@ -325,3 +324,7 @@ values (6, 1, '2023-05-15 09:05:10', null, 'Data', null, 3, 'epic', 'in_progress (5, 118, '2023-05-16 11:10:10', null, 'UI tab of tasks', null, null, null, null, 'high'), (11, 118, '2023-05-16 12:30:10', null, 'UI tab of tasks', null, 2, null, null, null); +insert into ACTIVITY (AUTHOR_ID, TASK_ID, UPDATED, STATUS_CODE) +values (1, 1, '2023-01-01 10:00:00', 'in_progress'), + (1, 1, '2023-01-03 15:00:00', 'ready_for_review'), + (1, 1, '2023-01-05 12:00:00', 'done'); \ No newline at end of file diff --git a/src/test/java/com/javarush/jira/AbstractControllerTest.java b/src/test/java/com/javarush/jira/AbstractControllerTest.java index 5981bae53..b58f48116 100644 --- a/src/test/java/com/javarush/jira/AbstractControllerTest.java +++ b/src/test/java/com/javarush/jira/AbstractControllerTest.java @@ -1,5 +1,7 @@ package com.javarush.jira; +import com.javarush.jira.ref.ReferenceService; +import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.test.context.jdbc.Sql; @@ -9,7 +11,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-h2.sql", "classpath:data-h2.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 { @@ -17,6 +19,14 @@ public abstract class AbstractControllerTest extends BaseTests { @Autowired private MockMvc mockMvc; + @Autowired + private ReferenceService referenceService; + + @BeforeEach + void reloadReferences() { + referenceService.initialize(); + } + protected ResultActions perform(MockHttpServletRequestBuilder builder) throws Exception { return mockMvc.perform(builder); } diff --git a/src/test/java/com/javarush/jira/JiraRushApplicationTests.java b/src/test/java/com/javarush/jira/JiraRushApplicationTests.java index 287178598..f821163f8 100644 --- a/src/test/java/com/javarush/jira/JiraRushApplicationTests.java +++ b/src/test/java/com/javarush/jira/JiraRushApplicationTests.java @@ -1,7 +1,9 @@ package com.javarush.jira; import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; +@ActiveProfiles("test") class JiraRushApplicationTests extends BaseTests { @Test void contextLoads() { 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..f003b947d 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 @@ -35,26 +35,15 @@ void showRegisterPageWhenAuthorized() throws Exception { @Test 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) - .param("email", "newemail@gmail.com") - .param("password", "newPassword") - .param("firstName", "newName") - .param("lastName", "newLastName") - .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); + perform(MockMvcRequestBuilders.post(REGISTER_URL) + .param("email", "newemail@gmail.com") + .param("password", "newPassword") + .param("firstName", "newName") + .param("lastName", "newLastName") + .param("displayName", "newDisplayName") + .with(csrf())) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/view/login")); } @Test 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..3be529e49 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 @@ -5,12 +5,15 @@ import com.javarush.jira.login.Role; import com.javarush.jira.login.User; import com.javarush.jira.login.UserTo; +import com.javarush.jira.profile.ProfileTo; public class UserTestData { public static final MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator( User.class, "startpoint", "endpoint", "password"); public static final MatcherFactory.Matcher TO_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(UserTo.class); + public static MatcherFactory.Matcher PROFILE_TO_MATCHER = MatcherFactory.usingIgnoringFieldsComparator( + ProfileTo.class, "id"); public static final long USER_ID = 1; public static final long ADMIN_ID = 2; 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..117379af2 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,99 @@ package com.javarush.jira.profile.internal.web; +import com.fasterxml.jackson.databind.ObjectMapper; import com.javarush.jira.AbstractControllerTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import static com.javarush.jira.login.internal.web.UserTestData.*; +import static com.javarush.jira.profile.internal.web.ProfileTestData.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; class ProfileRestControllerTest extends AbstractControllerTest { + private static final String REST_URL = ProfileRestController.REST_URL; + + @Autowired + private ObjectMapper objectMapper; + + @Test + void get_shouldReturnUserProfileTo_whenAuthenticatedAsUser() throws Exception { + perform(get(REST_URL) + .with(httpBasic(USER_MAIL, "password"))) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(PROFILE_TO_MATCHER.contentJson(USER_PROFILE_TO)); + } + + @Test + void get_shouldReturnEmptyProfileTo_whenAuthenticatedAsGuest() throws Exception { + perform(get(REST_URL) + .with(httpBasic(GUEST_MAIL, "guest"))) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(PROFILE_TO_MATCHER.contentJson(GUEST_PROFILE_EMPTY_TO)); + } + + @Test + void get_shouldReturn401_whenNotAuthenticated() throws Exception { + perform(get(REST_URL)) + .andExpect(status().isUnauthorized()); + } + + @Test + void update_shouldReturn204_whenValidData() throws Exception { + perform(put(REST_URL) + .with(httpBasic(USER_MAIL, "password")) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(getUpdatedTo()))) + .andExpect(status().isNoContent()); + } + + @Test + void update_shouldReturn401_whenNotAuthenticated() throws Exception { + perform(put(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(getUpdatedTo()))) + .andExpect(status().isUnauthorized()); + } + + @Test + void update_shouldReturn422_whenInvalidData() throws Exception { + perform(put(REST_URL) + .with(httpBasic(USER_MAIL, "password")) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(getInvalidTo()))) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void update_shouldReturn422_whenUnknownNotification() throws Exception { + perform(put(REST_URL) + .with(httpBasic(USER_MAIL, "password")) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(getWithUnknownNotificationTo()))) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void update_shouldReturn422_whenUnknownContact() throws Exception { + perform(put(REST_URL) + .with(httpBasic(USER_MAIL, "password")) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(getWithUnknownContactTo()))) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void update_shouldReturn422_whenContactValueContainsHtmlUnsafeContent() throws Exception { + perform(put(REST_URL) + .with(httpBasic(USER_MAIL, "password")) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(getWithContactHtmlUnsafeTo()))) + .andExpect(status().isUnprocessableEntity()); + } } \ No newline at end of file diff --git a/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java b/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java index fb4407268..cc0513971 100644 --- a/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java +++ b/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java @@ -44,7 +44,6 @@ public static ProfileTo getUpdatedTo() { new ContactTo("website", "new.com"), new ContactTo("github", "newGitHub"), new ContactTo("tg", "newTg"), - new ContactTo("vk", "newVk"), new ContactTo("linkedin", "newLinkedin"))); } @@ -57,7 +56,6 @@ public static Profile getUpdated(long id) { new Contact(id, "website", "new.com"), new Contact(id, "github", "newGitHub"), new Contact(id, "tg", "newTg"), - new Contact(id, "vk", "newVk"), new Contact(id, "linkedin", "newLinkedin"))); return profile; } diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 51137fd06..ced359271 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -3,6 +3,19 @@ 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:jira-test;MODE=PostgreSQL;NON_KEYWORDS=VALUE,COMMENT + driver-class-name: org.h2.Driver + username: sa + password: + + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create-drop + + sql: + init: + mode: never + + liquibase: + enabled: false \ No newline at end of file diff --git a/src/test/resources/data.sql b/src/test/resources/data-h2.sql similarity index 82% rename from src/test/resources/data.sql rename to src/test/resources/data-h2.sql index 5087dbddc..5facf6e3b 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data-h2.sql @@ -1,32 +1,19 @@ --------- users ---------------------- -delete -from USER_ROLE; -delete -from CONTACT; -delete -from PROFILE; +delete from USER_ROLE; +delete from CONTACT; +delete from PROFILE; +delete from ACTIVITY; +delete from TASK; +delete from SPRINT; +delete from PROJECT; +delete from USERS; -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; +-- Reset identity counters +ALTER TABLE ACTIVITY ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE TASK ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE SPRINT ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE PROJECT ALTER COLUMN ID RESTART WITH 1; +ALTER TABLE USERS 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'), @@ -53,8 +40,7 @@ values (1, 'skype', 'userSkype'), (1, 'mobile', '+01234567890'), (1, 'website', 'user.com'), (2, 'github', 'adminGitHub'), - (2, 'tg', 'adminTg'), - (2, 'vk', 'adminVk'); + (2, 'tg', 'adminTg'); insert into PROJECT (code, title, description, type_code, parent_id) @@ -88,9 +74,7 @@ values (1, 1, '2023-05-15 09:05:10', null, 'Data', null, 3, 'epic', 'in_progress (1, 2, '2023-05-15 12:05:10', null, 'Trees', 'Trees desc', 4, 'epic', 'in_progress', 'normal'); insert into USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE, STARTPOINT, ENDPOINT) -values (1, 2, 2, 'task_developer', '2023-06-14 08:35:10', '2023-06-14 08:55:00'), - (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'), +values (1, 2, 2, 'task_reviewer', '2023-06-14 09:35:10', 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), diff --git a/src/test/resources/db/changelog-h2.sql b/src/test/resources/db/changelog-h2.sql new file mode 100644 index 000000000..fc00a8de8 --- /dev/null +++ b/src/test/resources/db/changelog-h2.sql @@ -0,0 +1,325 @@ +--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 bigint generated by default as identity primary key, + CODE varchar(32) not null + constraint UK_PROJECT_CODE unique, + TITLE varchar(1024) not null, + DESCRIPTION varchar(4096) not null, + TYPE_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + PARENT_ID bigint, + constraint FK_PROJECT_PARENT foreign key (PARENT_ID) references PROJECT (ID) on delete cascade +); + +create table MAIL_CASE +( + ID bigint generated by default as identity primary key, + EMAIL varchar(255) not null, + NAME varchar(255) not null, + DATE_TIME timestamp not null, + RESULT varchar(255) not null, + TEMPLATE varchar(255) not null +); + +create table SPRINT +( + ID bigint generated by default as identity primary key, + STATUS_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + 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 bigint generated by default as identity primary key, + CODE varchar(32) not null, + REF_TYPE smallint not null, + ENDPOINT timestamp, + STARTPOINT timestamp, + TITLE varchar(1024) not null, + AUX varchar, + constraint UK_REFERENCE_REF_TYPE_CODE unique (REF_TYPE, CODE) +); + +create table USERS +( + ID bigint generated by default as identity primary key, + DISPLAY_NAME varchar(32) not null + constraint UK_USERS_DISPLAY_NAME unique, + EMAIL varchar(128) not null + constraint UK_USERS_EMAIL unique, + FIRST_NAME varchar(32) not null, + LAST_NAME varchar(32), + PASSWORD varchar(128) not null, + ENDPOINT timestamp, + STARTPOINT timestamp +); + +create table PROFILE +( + ID bigint generated by default as identity 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 bigint generated by default as identity 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 bigint generated by default as identity primary key, + AUTHOR_ID bigint not null, + TASK_ID bigint not null, + UPDATED timestamp, + COMMENT varchar(4096), +-- history of task field change + TITLE varchar(1024), + DESCRIPTION varchar(4096), + ESTIMATE integer, + TYPE_CODE varchar(32), + STATUS_CODE varchar(32), + PRIORITY_CODE varchar(32), + constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID), + constraint FK_ACTIVITY_TASK foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table TASK_TAG +( + TASK_ID bigint not null, + TAG varchar(32) not null, + constraint UK_TASK_TAG unique (TASK_ID, TAG), + constraint FK_TASK_TAG foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table USER_BELONG +( + ID bigint generated by default as identity primary key, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + USER_TYPE_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) +); +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 bigint generated by default as identity primary key, + NAME varchar(128) not null, + FILE_LINK varchar(2048) not null, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + DATE_TIME timestamp, + constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) +); + +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), + ('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 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); \ No newline at end of file