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