From 7709437f436b7eaedb04ea0d94c12e80c4a63114 Mon Sep 17 00:00:00 2001
From: Vilem Gottwald <xvigo.dev@gmail.com>
Date: Tue, 23 Apr 2024 20:36:32 +0200
Subject: [PATCH] add integration tests

---
 .../src/main/resources/application.yml        |   3 +-
 .../src/main/resources/application.yml        |   1 +
 docker-compose.yml                            |   6 +-
 readme.md                                     |  18 +-
 .../src/main/resources/application.yml        |   4 +-
 user-service/pom.xml                          |  10 +
 .../fi/obs/controller/UserController.java     |  13 +-
 .../obs/controller/UserControllerAdvice.java  |   6 +
 .../obs/data/repository/UserRepository.java   |   2 +-
 .../exceptions/ClientConnectionException.java |   8 +
 .../fi/obs/http/TransactionServiceClient.java |   9 +-
 .../fi/obs/service/UserAccountService.java    |  27 +-
 .../src/main/resources/application.yml        |   4 +-
 .../db/migration/V0__initialize_database.sql  |   2 +
 .../ControllerIntegrationTest.java            |  32 ++
 .../rest/UserControllerIntegrationTest.java   | 345 ++++++++++++++++++
 .../fi/obs/repository/UserRepositoryTest.java | 150 ++++++++
 .../muni/fi/obs/service/UserServiceTest.java  |  59 ++-
 .../src/test/resources/application-test.yml   |  10 +
 user-service/src/test/resources/drop_all.sql  |   3 +
 .../src/test/resources/initialize_db.sql      |  10 +
 21 files changed, 676 insertions(+), 46 deletions(-)
 create mode 100644 user-service/src/main/java/cz/muni/fi/obs/exceptions/ClientConnectionException.java
 create mode 100644 user-service/src/test/java/cz/muni/fi/obs/integration/ControllerIntegrationTest.java
 create mode 100644 user-service/src/test/java/cz/muni/fi/obs/integration/rest/UserControllerIntegrationTest.java
 create mode 100644 user-service/src/test/java/cz/muni/fi/obs/repository/UserRepositoryTest.java
 create mode 100644 user-service/src/test/resources/application-test.yml
 create mode 100644 user-service/src/test/resources/drop_all.sql
 create mode 100644 user-service/src/test/resources/initialize_db.sql

diff --git a/analytics-service/src/main/resources/application.yml b/analytics-service/src/main/resources/application.yml
index 676b724..4b7dd3f 100644
--- a/analytics-service/src/main/resources/application.yml
+++ b/analytics-service/src/main/resources/application.yml
@@ -1,3 +1,4 @@
 server:
   servlet:
-    context-path: '/api/analytics-service'
\ No newline at end of file
+    context-path: '/api/analytics-service'
+  port: 8080
diff --git a/currency-service/src/main/resources/application.yml b/currency-service/src/main/resources/application.yml
index 6a8974c..3c9f20c 100644
--- a/currency-service/src/main/resources/application.yml
+++ b/currency-service/src/main/resources/application.yml
@@ -1,6 +1,7 @@
 server:
   servlet:
     context-path: '/api/currency-service'
+  port: 8081
 
 currency:
   auto-update:
diff --git a/docker-compose.yml b/docker-compose.yml
index adedaba..3734575 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -31,7 +31,7 @@ services:
     environment:
       - SPRING_DATASOURCE_URL=jdbc:postgresql://currency-db:5432/currency_db
     ports:
-      - 8081:8080
+      - 8081:8081
   currency-db:
     container_name: currency-db
     image: postgres:latest
@@ -53,7 +53,7 @@ services:
     environment:
       - SPRING_DATASOURCE_URL=jdbc:postgresql://transaction-db:5432/transaction_db
     ports:
-      - 8082:8080
+      - 8082:8082
   transaction-db:
     container_name: transaction-db
     image: postgres:latest
@@ -75,7 +75,7 @@ services:
     environment:
       - SPRING_DATASOURCE_URL=jdbc:postgresql://user-db:5432/user_db
     ports:
-      - 8083:8080
+      - 8083:8083
   user-db:
     container_name: user-db
     image: postgres:latest
diff --git a/readme.md b/readme.md
index b9e5e39..ff413cc 100644
--- a/readme.md
+++ b/readme.md
@@ -36,4 +36,20 @@ In M2 we need to implement creation and execution of scheduled payments.
 ## Currency-service
 
 Service handles all currency related operation, it manages currencies so exchange rates are always up-to-date and
-provides needed services to the rest of the system.
\ No newline at end of file
+provides needed services to the rest of the system.
+
+### Swagger Links
+
+- [Analytics-service](http://localhost:8080/api/analytics-service/swagger-ui/index.html)
+- [User-service](http://localhost:8083/api/user-service/swagger-ui/index.html)
+- [Transaction-service](http://localhost:8082/api/transaction-service/swagger-ui/index.html)
+- [Currency-service](http://localhost:8081/api/currency-service/swagger-ui/index.html)
+
+### Adminer
+
+Password: `changemelater`
+
+- [Analytics-service](http://localhost:8084/?pgsql=analytics-db&username=analytics_service&db=analytics_db&)
+- [User-service](http://localhost:8084/?pgsql=user-db&username=user_service&db=user_db&)
+- [Transaction-service](http://localhost:8084/?pgsql=transaction-db&username=transaction_service&db=transaction_db&)
+- [Currency-service](http://localhost:8084/?pgsql=currency-db&username=currency_service&db=currency_db&)
\ No newline at end of file
diff --git a/transaction-service/src/main/resources/application.yml b/transaction-service/src/main/resources/application.yml
index 18bb727..a77cb25 100644
--- a/transaction-service/src/main/resources/application.yml
+++ b/transaction-service/src/main/resources/application.yml
@@ -1,6 +1,7 @@
 server:
   servlet:
     context-path: '/api/transaction-service'
+  port: 8082
 
 spring:
   application:
@@ -35,4 +36,5 @@ resilience4j:
 
 clients:
   currency-service:
-    url: 'http://localhost:8080/api/currency-service'
+    #    url: 'http://localhost:8081/api/currency-service'
+    url: 'http://currency-service:8081/api/currency-service'
diff --git a/user-service/pom.xml b/user-service/pom.xml
index 76f7c3f..6748a3c 100644
--- a/user-service/pom.xml
+++ b/user-service/pom.xml
@@ -87,6 +87,16 @@
             <artifactId>spring-boot-starter-data-jpa</artifactId>
             <version>${spring.boot.version}</version>
         </dependency>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.rest-assured</groupId>
+            <artifactId>rest-assured</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/user-service/src/main/java/cz/muni/fi/obs/controller/UserController.java b/user-service/src/main/java/cz/muni/fi/obs/controller/UserController.java
index 1f08a30..472e3f4 100644
--- a/user-service/src/main/java/cz/muni/fi/obs/controller/UserController.java
+++ b/user-service/src/main/java/cz/muni/fi/obs/controller/UserController.java
@@ -12,8 +12,6 @@ import io.swagger.v3.oas.annotations.info.License;
 import io.swagger.v3.oas.annotations.media.Content;
 import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.servers.Server;
-import io.swagger.v3.oas.annotations.servers.ServerVariable;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
 import lombok.extern.slf4j.Slf4j;
@@ -45,16 +43,7 @@ import java.util.UUID;
                 - getting user accounts by user ID
                 """,
                      contact = @Contact(name = "Vilem Gottwald", email = "553627@mail.muni.cz"),
-                     license = @License(name = "Apache 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0.html")),
-        servers = @Server(
-                description = "local server",
-                url = "{scheme}://{server}:{port}/api/user-service",
-                variables = {
-                        @ServerVariable(name = "scheme", defaultValue = "http"),
-                        @ServerVariable(name = "server", defaultValue = "localhost"),
-                        @ServerVariable(name = "port", defaultValue = "8080"),
-                }
-        )
+                     license = @License(name = "Apache 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0.html"))
 )
 @Tag(name = "User management", description = "Microservice for managing users and their bank accounts")
 @RequestMapping(path = "/v1/users", produces = MediaType.APPLICATION_JSON_VALUE)
diff --git a/user-service/src/main/java/cz/muni/fi/obs/controller/UserControllerAdvice.java b/user-service/src/main/java/cz/muni/fi/obs/controller/UserControllerAdvice.java
index 10fca41..7545e85 100644
--- a/user-service/src/main/java/cz/muni/fi/obs/controller/UserControllerAdvice.java
+++ b/user-service/src/main/java/cz/muni/fi/obs/controller/UserControllerAdvice.java
@@ -3,6 +3,7 @@ package cz.muni.fi.obs.controller;
 import cz.muni.fi.obs.api.NotFoundResponse;
 import cz.muni.fi.obs.api.ValidationErrors;
 import cz.muni.fi.obs.api.ValidationFailedResponse;
+import cz.muni.fi.obs.exceptions.ClientConnectionException;
 import cz.muni.fi.obs.exceptions.UserNotFoundException;
 import org.postgresql.util.PSQLException;
 import org.springframework.http.HttpStatus;
@@ -102,6 +103,11 @@ public class UserControllerAdvice {
         }
     }
 
+    @ExceptionHandler(ClientConnectionException.class)
+    public ResponseEntity<String> handleClientConnectionExceptions(ClientConnectionException ex) {
+        return new ResponseEntity<>(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
+    }
+
     private String extractFieldName(String message) {
         String fieldName = null;
         if (message.contains("Key (")) {
diff --git a/user-service/src/main/java/cz/muni/fi/obs/data/repository/UserRepository.java b/user-service/src/main/java/cz/muni/fi/obs/data/repository/UserRepository.java
index 308fbb7..c91c06d 100644
--- a/user-service/src/main/java/cz/muni/fi/obs/data/repository/UserRepository.java
+++ b/user-service/src/main/java/cz/muni/fi/obs/data/repository/UserRepository.java
@@ -24,7 +24,7 @@ public interface UserRepository extends JpaRepository<User, UUID> {
             "(:lastName IS NULL OR u.lastName = :lastName) AND " +
             "(:phoneNumber IS NULL OR u.phoneNumber = :phoneNumber) AND " +
             "(:email IS NULL OR u.email = :email) AND " +
-            "(:birthDate IS NULL OR u.birthDate = :birthDate) AND " +
+            "(cast(:birthDate as date) IS NULL OR u.birthDate = cast(:birthDate as date)) AND " +
             "(:birthNumber IS NULL OR u.birthNumber = :birthNumber) AND " +
             "(:active IS NULL OR u.active = :active)")
     Page<User> findBySearchParams(
diff --git a/user-service/src/main/java/cz/muni/fi/obs/exceptions/ClientConnectionException.java b/user-service/src/main/java/cz/muni/fi/obs/exceptions/ClientConnectionException.java
new file mode 100644
index 0000000..6d65e8a
--- /dev/null
+++ b/user-service/src/main/java/cz/muni/fi/obs/exceptions/ClientConnectionException.java
@@ -0,0 +1,8 @@
+package cz.muni.fi.obs.exceptions;
+
+public class ClientConnectionException extends RuntimeException {
+
+    public ClientConnectionException(String message) {
+        super(message);
+    }
+}
diff --git a/user-service/src/main/java/cz/muni/fi/obs/http/TransactionServiceClient.java b/user-service/src/main/java/cz/muni/fi/obs/http/TransactionServiceClient.java
index 5081f19..d5d7181 100644
--- a/user-service/src/main/java/cz/muni/fi/obs/http/TransactionServiceClient.java
+++ b/user-service/src/main/java/cz/muni/fi/obs/http/TransactionServiceClient.java
@@ -1,6 +1,7 @@
 package cz.muni.fi.obs.http;
 
 import cz.muni.fi.obs.config.FeignClientConfiguration;
+import cz.muni.fi.obs.exceptions.ClientConnectionException;
 import cz.muni.fi.obs.http.api.TSAccount;
 import cz.muni.fi.obs.http.api.TSAccountCreate;
 import lombok.extern.slf4j.Slf4j;
@@ -31,14 +32,14 @@ public interface TransactionServiceClient {
 
         @Override
         public TSAccount createAccount(TSAccountCreate currencyExchangeRequest) {
-            log.error("Could not create account, returning null");
-            return null;
+            log.error("Could not create account for customer id {}", currencyExchangeRequest.customerId());
+            throw new ClientConnectionException("Could not create account");
         }
 
         @Override
         public List<TSAccount> getAccountsByCustomerId(String customerId) {
-            log.error("Could not get accounts by customer id, returning null");
-            return null;
+            log.error("Could not get accounts by customer id {}", customerId);
+            throw new ClientConnectionException("Could not get accounts by customer id");
         }
     }
 }
diff --git a/user-service/src/main/java/cz/muni/fi/obs/service/UserAccountService.java b/user-service/src/main/java/cz/muni/fi/obs/service/UserAccountService.java
index 964f4bc..bdcc66b 100644
--- a/user-service/src/main/java/cz/muni/fi/obs/service/UserAccountService.java
+++ b/user-service/src/main/java/cz/muni/fi/obs/service/UserAccountService.java
@@ -5,9 +5,11 @@ import cz.muni.fi.obs.api.AccountDto;
 import cz.muni.fi.obs.http.TransactionServiceClient;
 import cz.muni.fi.obs.http.api.TSAccount;
 import cz.muni.fi.obs.http.api.TSAccountCreate;
+import feign.FeignException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
 import java.util.stream.Collectors;
@@ -30,9 +32,6 @@ public class UserAccountService {
                 accountCreateDto.accountNumber()
         );
         TSAccount tsAccount = transactionServiceClient.createAccount(tsAccountCreate);
-        if (tsAccount == null) {
-            return null;
-        }
         return new AccountDto(
                 UUID.fromString(tsAccount.id()),
                 tsAccount.accountNumber(),
@@ -41,17 +40,17 @@ public class UserAccountService {
     }
 
     public List<AccountDto> getUserAccounts(UUID userId) {
-        List<TSAccount> tsAccounts = transactionServiceClient.getAccountsByCustomerId(userId.toString());
-        if (tsAccounts == null) {
-            return null;
+        try {
+            List<TSAccount> tsAccounts = transactionServiceClient.getAccountsByCustomerId(userId.toString());
+            return tsAccounts.stream()
+                             .map(tsAccount -> new AccountDto(
+                                     UUID.fromString(tsAccount.id()),
+                                     tsAccount.accountNumber(),
+                                     tsAccount.currencyCode()
+                             ))
+                             .collect(Collectors.toList());
+        } catch (FeignException.NotFound e) {
+            return Collections.emptyList();
         }
-
-        return tsAccounts.stream()
-                         .map(tsAccount -> new AccountDto(
-                                 UUID.fromString(tsAccount.id()),
-                                 tsAccount.accountNumber(),
-                                 tsAccount.currencyCode()
-                         ))
-                         .collect(Collectors.toList());
     }
 }
diff --git a/user-service/src/main/resources/application.yml b/user-service/src/main/resources/application.yml
index 71d955f..f4df708 100644
--- a/user-service/src/main/resources/application.yml
+++ b/user-service/src/main/resources/application.yml
@@ -1,6 +1,7 @@
 server:
   servlet:
     context-path: '/api/user-service'
+  port: 8083
 
 spring:
   datasource:
@@ -16,4 +17,5 @@ spring:
 
 clients:
   transaction-service:
-    url: 'http://localhost:8080/api/transaction-service'
\ No newline at end of file
+    #    url: 'http://localhost:8082/api/transaction-service'
+    url: 'http://transaction-service:8082/api/transaction-service'
\ No newline at end of file
diff --git a/user-service/src/main/resources/db/migration/V0__initialize_database.sql b/user-service/src/main/resources/db/migration/V0__initialize_database.sql
index aae645d..4b801e4 100644
--- a/user-service/src/main/resources/db/migration/V0__initialize_database.sql
+++ b/user-service/src/main/resources/db/migration/V0__initialize_database.sql
@@ -10,3 +10,5 @@ CREATE TABLE us_user
     birth_number varchar(20)  not null unique,
     active       boolean      not null default true
 );
+
+CREATE INDEX us_user_id_index ON us_user (id);
diff --git a/user-service/src/test/java/cz/muni/fi/obs/integration/ControllerIntegrationTest.java b/user-service/src/test/java/cz/muni/fi/obs/integration/ControllerIntegrationTest.java
new file mode 100644
index 0000000..82ee287
--- /dev/null
+++ b/user-service/src/test/java/cz/muni/fi/obs/integration/ControllerIntegrationTest.java
@@ -0,0 +1,32 @@
+package cz.muni.fi.obs.integration;
+
+import io.restassured.RestAssured;
+import io.restassured.specification.RequestSpecification;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.web.util.UriComponents;
+
+import static io.restassured.RestAssured.given;
+import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS;
+
+@ActiveProfiles("test")
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@Sql(value = {"/initialize_db.sql"}, executionPhase = BEFORE_TEST_CLASS)
+public abstract class ControllerIntegrationTest {
+
+    @LocalServerPort
+    private int port;
+
+    public static RequestSpecification requestSpecification(UriComponents uri) {
+        return given().basePath(uri.getPath())
+                      .queryParams(uri.getQueryParams());
+    }
+
+    @BeforeEach
+    void setup() {
+        RestAssured.port = port;
+    }
+}
diff --git a/user-service/src/test/java/cz/muni/fi/obs/integration/rest/UserControllerIntegrationTest.java b/user-service/src/test/java/cz/muni/fi/obs/integration/rest/UserControllerIntegrationTest.java
new file mode 100644
index 0000000..84cce53
--- /dev/null
+++ b/user-service/src/test/java/cz/muni/fi/obs/integration/rest/UserControllerIntegrationTest.java
@@ -0,0 +1,345 @@
+package cz.muni.fi.obs.integration.rest;
+
+import cz.muni.fi.obs.api.*;
+import cz.muni.fi.obs.data.dbo.User;
+import cz.muni.fi.obs.data.enums.Nationality;
+import cz.muni.fi.obs.data.repository.UserRepository;
+import cz.muni.fi.obs.integration.ControllerIntegrationTest;
+import io.restassured.http.ContentType;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.time.LocalDate;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@TestMethodOrder(OrderAnnotation.class)
+class UserControllerIntegrationTest extends ControllerIntegrationTest {
+
+    private static final String USER_CONTROLLER_PATH = "/api/user-service/v1/users";
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @Test
+    public void createUser_newUser_createsUser() {
+        UriComponents components = UriComponentsBuilder.
+                fromPath(USER_CONTROLLER_PATH + "/create")
+                .build();
+
+        UserCreateDto userCreateDto = new UserCreateDto("Joe",
+                                                        "Doe",
+                                                        "123456789",
+                                                        "test@gmail.com",
+                                                        LocalDate.of(2001, 4, 13),
+                                                        Nationality.SK,
+                                                        "010413/2215"
+        );
+
+        UserDto userDto = requestSpecification(components)
+                .contentType(ContentType.JSON)
+                .body(userCreateDto)
+                .post()
+                .then()
+                .statusCode(HttpStatus.SC_CREATED)
+                .extract()
+                .as(UserDto.class);
+
+
+        Optional<User> createdUser = userRepository.findById(userDto.id());
+        assertThat(createdUser).isPresent();
+
+        User user = createdUser.get();
+        userRepository.delete(user);
+
+        assertThat(user)
+                .returns(userCreateDto.firstName(), User::getFirstName)
+                .returns(userCreateDto.lastName(), User::getLastName)
+                .returns(userCreateDto.phoneNumber(), User::getPhoneNumber)
+                .returns(userCreateDto.email(), User::getEmail)
+                .returns(userCreateDto.birthDate(), User::getBirthDate)
+                .returns(userCreateDto.nationality(), User::getNationality)
+                .returns(userCreateDto.birthNumber(), User::getBirthNumber)
+                .returns(true, User::isActive);
+    }
+
+    @Test
+    public void createUser_invalidRequest_returnsValidationError() {
+
+        UriComponents components = UriComponentsBuilder
+                .fromPath(USER_CONTROLLER_PATH + "/create")
+                .build();
+
+        ValidationFailedResponse response = requestSpecification(components)
+                .contentType(ContentType.JSON)
+                .body(new UserCreateDto("", "", "", "", LocalDate.now(), null, ""))
+                .post()
+                .then()
+                .statusCode(HttpStatus.SC_BAD_REQUEST)
+                .extract()
+                .as(ValidationFailedResponse.class);
+
+        assertThat(response.message()).isEqualTo("Validation failed");
+    }
+
+    @Test
+    public void getUserById_userExists_returnsUser() {
+        UUID userId = UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bec2");
+
+        UriComponents components = UriComponentsBuilder
+                .fromPath(USER_CONTROLLER_PATH + "/" + userId)
+                .build();
+
+        UserDto userDto = requestSpecification(components)
+                .get()
+                .then()
+                .statusCode(HttpStatus.SC_OK)
+                .extract()
+                .as(UserDto.class);
+
+        assertThat(userDto.id()).isEqualTo(userId);
+    }
+
+    @Test
+    public void getUserById_invalidRequest_returnsValidationError() {
+
+        UriComponents components = UriComponentsBuilder
+                .fromPath(USER_CONTROLLER_PATH + "/not-a-uuid")
+                .build();
+
+        ValidationFailedResponse response = requestSpecification(components)
+                .get()
+                .then()
+                .statusCode(HttpStatus.SC_BAD_REQUEST)
+                .extract()
+                .as(ValidationFailedResponse.class);
+
+        assertThat(response.message()).isEqualTo("Validation failed");
+    }
+
+    @Test
+    public void getUserById_userNotExists_returnsNotFoundError() {
+        UUID userId = UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bed4");
+
+        UriComponents components = UriComponentsBuilder
+                .fromPath(USER_CONTROLLER_PATH + "/" + userId)
+                .build();
+
+        NotFoundResponse response = requestSpecification(components)
+                .get()
+                .then()
+                .statusCode(HttpStatus.SC_NOT_FOUND)
+                .extract()
+                .as(NotFoundResponse.class);
+
+        assertThat(response.message()).isEqualTo("User with id " + userId + " not found");
+    }
+
+    @Test
+    public void updateUser_userExists_returnsUser() {
+        UUID userId = UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bec2");
+        User origUser = userRepository.findById(userId).orElseThrow();
+
+        UriComponents components = UriComponentsBuilder
+                .fromPath(USER_CONTROLLER_PATH + "/" + userId)
+                .build();
+
+        UserUpdateDto userUpdateDto = new UserUpdateDto(
+                Optional.of("NewName"),
+                Optional.of("NewSurname"),
+                Optional.of("987654321"),
+                Optional.of("newemail@email.cz")
+        );
+
+
+        requestSpecification(components)
+                .contentType(ContentType.JSON)
+                .body(userUpdateDto)
+                .put()
+                .then()
+                .statusCode(HttpStatus.SC_OK)
+                .extract()
+                .as(UserDto.class);
+
+        Optional<User> user = userRepository.findById(userId);
+        assertThat(user).isPresent();
+
+        User updatedUser = user.get();
+        assertThat(updatedUser)
+                .returns("NewName", User::getFirstName)
+                .returns("NewSurname", User::getLastName)
+                .returns("987654321", User::getPhoneNumber)
+                .returns("newemail@email.cz", User::getEmail);
+
+        userRepository.save(origUser);
+    }
+
+    @Test
+    public void updateUser_userNotExists_returnsNotFoundError() {
+        UUID userId = UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bed4");
+        assertThat(userRepository.findById(userId).isPresent()).isFalse();
+
+        UriComponents components = UriComponentsBuilder
+                .fromPath(USER_CONTROLLER_PATH + "/" + userId)
+                .build();
+
+        UserUpdateDto userUpdateDto = new UserUpdateDto(
+                Optional.of("NewName"),
+                Optional.of("NewSurname"),
+                Optional.of("987654321"),
+                Optional.of("newemail@email.cz")
+        );
+
+        requestSpecification(components)
+                .contentType(ContentType.JSON)
+                .body(userUpdateDto)
+                .put()
+                .then()
+                .statusCode(HttpStatus.SC_NOT_FOUND)
+                .extract()
+                .as(NotFoundResponse.class);
+
+        assertThat(userRepository.findById(userId).isPresent()).isFalse();
+    }
+
+    @Test
+    public void deactivateUser_userNotExists_returnsNotFoundError() {
+        UUID userId = UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bed4");
+        assertThat(userRepository.findById(userId).isPresent()).isFalse();
+
+        UriComponents components = UriComponentsBuilder
+                .fromPath(USER_CONTROLLER_PATH + "/" + userId + "/deactivate")
+                .build();
+
+
+        requestSpecification(components)
+                .post()
+                .then()
+                .statusCode(HttpStatus.SC_NOT_FOUND)
+                .extract()
+                .as(NotFoundResponse.class);
+
+        assertThat(userRepository.findById(userId).isPresent()).isFalse();
+    }
+
+    @Test
+    public void deactivateUser_deactivatedUser_doesNothing() {
+        UUID userId = UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bec5");
+        User origUser = userRepository.findById(userId).orElseThrow();
+        assertThat(origUser.isActive()).isFalse();
+
+        UriComponents components = UriComponentsBuilder
+                .fromPath(USER_CONTROLLER_PATH + "/" + userId + "/deactivate")
+                .build();
+
+
+        UserDto userDto = requestSpecification(components)
+                .post()
+                .then()
+                .statusCode(HttpStatus.SC_OK)
+                .extract()
+                .as(UserDto.class);
+
+        User updatedUser = userRepository.findById(userId).orElseThrow();
+        assertThat(updatedUser).isEqualTo(origUser);
+        assertThat(userDto.active()).isFalse();
+    }
+
+    @Test
+    public void deactivateUser_activateUser_deactivatesUser() {
+        UUID userId = UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bec2");
+        User origUser = userRepository.findById(userId).orElseThrow();
+        assertThat(origUser.isActive()).isTrue();
+
+        UriComponents components = UriComponentsBuilder
+                .fromPath(USER_CONTROLLER_PATH + "/" + userId + "/deactivate")
+                .build();
+
+
+        UserDto userDto = requestSpecification(components)
+                .post()
+                .then()
+                .statusCode(HttpStatus.SC_OK)
+                .extract()
+                .as(UserDto.class);
+
+        User updatedUser = userRepository.findById(userId).orElseThrow();
+        assertThat(updatedUser.isActive()).isFalse();
+        assertThat(userDto.active()).isFalse();
+
+        userRepository.save(origUser);
+    }
+
+    @Test
+    public void activateUser_userNotExists_returnsNotFoundError() {
+        UUID userId = UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bed4");
+        assertThat(userRepository.findById(userId).isPresent()).isFalse();
+
+        UriComponents components = UriComponentsBuilder
+                .fromPath(USER_CONTROLLER_PATH + "/" + userId + "/activate")
+                .build();
+
+
+        requestSpecification(components)
+                .post()
+                .then()
+                .statusCode(HttpStatus.SC_NOT_FOUND)
+                .extract()
+                .as(NotFoundResponse.class);
+
+        assertThat(userRepository.findById(userId).isPresent()).isFalse();
+    }
+
+    @Test
+    public void activateUser_activatedUser_doesNothing() {
+        UUID userId = UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bec2");
+        User origUser = userRepository.findById(userId).orElseThrow();
+        assertThat(origUser.isActive()).isTrue();
+
+        UriComponents components = UriComponentsBuilder
+                .fromPath(USER_CONTROLLER_PATH + "/" + userId + "/activate")
+                .build();
+
+        UserDto userDto = requestSpecification(components)
+                .post()
+                .then()
+                .statusCode(HttpStatus.SC_OK)
+                .extract()
+                .as(UserDto.class);
+
+        User updatedUser = userRepository.findById(userId).orElseThrow();
+        assertThat(updatedUser).isEqualTo(origUser);
+        assertThat(userDto.active()).isTrue();
+    }
+
+    @Test
+    public void activateUser_deactivatedUser_activatesUser() {
+        UUID userId = UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bec5");
+        User origUser = userRepository.findById(userId).orElseThrow();
+        assertThat(origUser.isActive()).isFalse();
+
+        UriComponents components = UriComponentsBuilder
+                .fromPath(USER_CONTROLLER_PATH + "/" + userId + "/activate")
+                .build();
+
+        UserDto userDto = requestSpecification(components)
+                .post()
+                .then()
+                .statusCode(HttpStatus.SC_OK)
+                .extract()
+                .as(UserDto.class);
+
+        User updatedUser = userRepository.findById(userId).orElseThrow();
+        assertThat(updatedUser.isActive()).isTrue();
+        assertThat(userDto.active()).isTrue();
+
+        userRepository.save(origUser);
+    }
+}
diff --git a/user-service/src/test/java/cz/muni/fi/obs/repository/UserRepositoryTest.java b/user-service/src/test/java/cz/muni/fi/obs/repository/UserRepositoryTest.java
new file mode 100644
index 0000000..51168bb
--- /dev/null
+++ b/user-service/src/test/java/cz/muni/fi/obs/repository/UserRepositoryTest.java
@@ -0,0 +1,150 @@
+package cz.muni.fi.obs.repository;
+
+import cz.muni.fi.obs.data.dbo.User;
+import cz.muni.fi.obs.data.enums.Nationality;
+import cz.muni.fi.obs.data.repository.UserRepository;
+import cz.muni.fi.obs.exceptions.UserNotFoundException;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+
+import java.time.LocalDate;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_CLASS;
+import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS;
+
+@Sql(value = {"/initialize_db.sql"}, executionPhase = BEFORE_TEST_CLASS)
+@Sql(value = {"/drop_all.sql"}, executionPhase = AFTER_TEST_CLASS)
+@DataJpaTest
+@ActiveProfiles("test")
+public class UserRepositoryTest {
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @Test
+    public void findByIdOrThrow_UserFound_ReturnsUser() {
+        User user = userRepository.findByIdOrThrow(UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bec2"));
+
+        assertThat(user)
+                .returns(UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bec2"), User::getId)
+                .returns("John", User::getFirstName)
+                .returns("Doe", User::getLastName)
+                .returns("9707178239", User::getPhoneNumber)
+                .returns("example1@domain.com", User::getEmail)
+                .returns("1990-01-01", u -> u.getBirthDate().toString())
+                .returns(Nationality.CZ, User::getNationality)
+                .returns("900101/1234", User::getBirthNumber)
+                .returns(true, User::isActive);
+    }
+
+    @Test
+    public void findByIdOrThrow_UserNotFound_ThrowsException() {
+        assertThatThrownBy(() -> userRepository.findByIdOrThrow(UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bec8"
+        )))
+                .isInstanceOf(UserNotFoundException.class)
+                .hasMessage("User with id 5e4b3326-38b5-4484-8034-33d81f34bec8 not found");
+    }
+
+
+    @Test
+    public void findBySearchParams_UsersNotFound_returnEmptyList() {
+        Page<User> users = userRepository.findBySearchParams(
+                Optional.of("non-existing"),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Pageable.unpaged()
+        );
+        assertThat(users).isEmpty();
+    }
+
+    @Test
+    public void findBySearchParams_NoParams_returnAll() {
+        Page<User> users = userRepository.findBySearchParams(
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Pageable.unpaged()
+        );
+
+        assertThat(users).hasSize(4);
+    }
+
+    @Test
+    public void findBySearchParams_WithPagination_ReturnsPaginated() {
+        int pageSize = 2; // Assuming a page size of 2 for testing purposes
+
+        // Fetch the first page
+        Page<User> firstPage = userRepository.findBySearchParams(
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                PageRequest.of(0, pageSize)
+        );
+
+        assertThat(firstPage).hasSize(2);
+        assertThat(firstPage.getTotalElements()).isEqualTo(4); // Assuming there are at least 4 users
+
+        // Fetch the second page
+        Page<User> secondPage = userRepository.findBySearchParams(
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                Optional.empty(),
+                PageRequest.of(1, pageSize)
+        );
+
+        assertThat(secondPage).hasSize(2);
+        assertThat(secondPage.getTotalElements()).isEqualTo(4); // Assuming there are at least 4 users
+    }
+
+    @Test
+    public void findBySearchParams_AllParams_returnSingle() {
+        Page<User> users = userRepository.findBySearchParams(
+                Optional.of("John"),
+                Optional.of("Doe"),
+                Optional.of("9707178239"),
+                Optional.of("example1@domain.com"),
+                Optional.of(LocalDate.of(1990, 1, 1)),
+                Optional.of("900101/1234"),
+                Optional.of(true),
+                Pageable.unpaged()
+        );
+
+        assertThat(users).hasSize(1);
+        assertThat(users.getContent().getFirst())
+                .returns(UUID.fromString("5e4b3326-38b5-4484-8034-33d81f34bec2"), User::getId)
+                .returns("John", User::getFirstName)
+                .returns("Doe", User::getLastName)
+                .returns("9707178239", User::getPhoneNumber)
+                .returns("example1@domain.com", User::getEmail)
+                .returns("1990-01-01", u -> u.getBirthDate().toString())
+                .returns(Nationality.CZ, User::getNationality)
+                .returns("900101/1234", User::getBirthNumber)
+                .returns(true, User::isActive);
+    }
+}
diff --git a/user-service/src/test/java/cz/muni/fi/obs/service/UserServiceTest.java b/user-service/src/test/java/cz/muni/fi/obs/service/UserServiceTest.java
index 2c1979b..21ce721 100644
--- a/user-service/src/test/java/cz/muni/fi/obs/service/UserServiceTest.java
+++ b/user-service/src/test/java/cz/muni/fi/obs/service/UserServiceTest.java
@@ -15,8 +15,7 @@ import java.time.LocalDate;
 import java.util.Optional;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
 
 @ExtendWith(MockitoExtension.class)
 class UserServiceTest {
@@ -126,7 +125,7 @@ class UserServiceTest {
     }
 
     @Test
-    void deactivateUser_userDeactivated_returnsUser() {
+    void activateUser_userActivated_returnsUser() {
         User user = new User("Joe",
                              "Doe",
                              "123456789",
@@ -136,17 +135,39 @@ class UserServiceTest {
                              "900101" + "/123",
                              true
         );
+
         when(userRepository.findByIdOrThrow(user.getId())).thenReturn(user);
-        when(userRepository.save(user)).thenReturn(user);
+        when(userRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
+
+        User response = userService.activateUser(user.getId());
+
+        verify(userRepository).findByIdOrThrow(user.getId());
+        assertThat(response.isActive()).isEqualTo(true);
+    }
+
+    @Test
+    void activateUser_userDeactivated_returnsUser() {
+        User user = new User("Joe",
+                             "Doe",
+                             "123456789",
+                             "test@gmail.com",
+                             LocalDate.now(),
+                             Nationality.CZ,
+                             "900101" + "/123",
+                             true
+        );
+
+        when(userRepository.findByIdOrThrow(user.getId())).thenReturn(user);
+        when(userRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
 
         User response = userService.deactivateUser(user.getId());
 
         verify(userRepository).findByIdOrThrow(user.getId());
-        assertThat(response).isEqualTo(user);
+        assertThat(response.isActive()).isEqualTo(false);
     }
 
     @Test
-    void activateUser_userActivated_returnsUser() {
+    void deactivateUser_userActivated_returnsUser() {
         User user = new User("Joe",
                              "Doe",
                              "123456789",
@@ -156,12 +177,34 @@ class UserServiceTest {
                              "900101" + "/123",
                              true
         );
+
         when(userRepository.findByIdOrThrow(user.getId())).thenReturn(user);
-        when(userRepository.save(user)).thenReturn(user);
+        when(userRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
 
         User response = userService.activateUser(user.getId());
 
         verify(userRepository).findByIdOrThrow(user.getId());
-        assertThat(response).isEqualTo(user);
+        assertThat(response.isActive()).isEqualTo(true);
+    }
+
+    @Test
+    void deactivateUser_userDeactivated_returnsUser() {
+        User user = new User("Joe",
+                             "Doe",
+                             "123456789",
+                             "test@gmail.com",
+                             LocalDate.now(),
+                             Nationality.CZ,
+                             "900101" + "/123",
+                             true
+        );
+
+        when(userRepository.findByIdOrThrow(user.getId())).thenReturn(user);
+        when(userRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
+
+        User response = userService.deactivateUser(user.getId());
+
+        verify(userRepository).findByIdOrThrow(user.getId());
+        assertThat(response.isActive()).isEqualTo(false);
     }
 }
diff --git a/user-service/src/test/resources/application-test.yml b/user-service/src/test/resources/application-test.yml
new file mode 100644
index 0000000..2139754
--- /dev/null
+++ b/user-service/src/test/resources/application-test.yml
@@ -0,0 +1,10 @@
+spring:
+  datasource:
+    driver-class-name: org.h2.Driver
+    url: jdbc:h2:mem:testdb
+    username: sa
+    password: password
+  jpa:
+    hibernate:
+      ddl-auto: create-drop
+    database-platform: org.hibernate.dialect.H2Dialect
diff --git a/user-service/src/test/resources/drop_all.sql b/user-service/src/test/resources/drop_all.sql
new file mode 100644
index 0000000..66e6df9
--- /dev/null
+++ b/user-service/src/test/resources/drop_all.sql
@@ -0,0 +1,3 @@
+DELETE
+FROM us_user u
+where u.id = u.id;;
diff --git a/user-service/src/test/resources/initialize_db.sql b/user-service/src/test/resources/initialize_db.sql
new file mode 100644
index 0000000..6178244
--- /dev/null
+++ b/user-service/src/test/resources/initialize_db.sql
@@ -0,0 +1,10 @@
+INSERT INTO us_user(id, first_name, last_name, phone_number, email, birth_date, nationality, birth_number, active)
+VALUES ('5e4b3326-38b5-4484-8034-33d81f34bec2', 'John', 'Doe', '9707178239', 'example1@domain.com', '1990-01-01', 'CZ',
+        '900101/1234', true),
+       ('5e4b3326-38b5-4484-8034-33d81f34bec3', 'Jane', 'Doe', '9707178238', 'example2@domain.com', '1991-02-02', 'SK',
+        '931028/4632', true),
+       ('5e4b3326-38b5-4484-8034-33d81f34bec4', 'Pete', 'Hel', '9707178237', 'example3@domain.com', '1992-03-03', 'CZ',
+        '761110/2983', false),
+       ('5e4b3326-38b5-4484-8034-33d81f34bec5', 'John', 'Doe', '9707178236', 'example4@domain.com', '1993-04-04', 'SK',
+        '580516/1472', false);
+
-- 
GitLab