commit 15f5a4072e51cbf8ad2e292f0768945d8f988927
Author: Jakub Smid <jaksmid@seznam.cz>
Date:   Tue Mar 29 20:54:35 2022 +0200

    Added first unit tests
     - for catalog and user repository
     - for type and title page service
    re #9363

diff --git a/backend/pom.xml b/backend/pom.xml
index f6f7c49..1cd1d4c 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -33,6 +33,11 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <scope>test</scope>
+        </dependency>
 
         <dependency>
             <groupId>org.postgresql</groupId>
@@ -82,7 +87,7 @@
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-configuration-processor</artifactId>
-            <version>2.4.2</version>
+            <version>2.6.5</version>
             <optional>true</optional>
         </dependency>
 
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/alternativename/AlternativeName.java b/backend/src/main/java/cz/zcu/kiv/backendapi/alternativename/AlternativeName.java
index ab9664e..bdbd596 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/alternativename/AlternativeName.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/alternativename/AlternativeName.java
@@ -3,6 +3,7 @@ package cz.zcu.kiv.backendapi.alternativename;
 import cz.zcu.kiv.backendapi.catalog.CatalogEntry;
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 
 import javax.persistence.*;
@@ -12,6 +13,7 @@ import java.io.Serializable;
  * Alternative name entity representing alternative name of geographic entry
  */
 @Data
+@EqualsAndHashCode(exclude = "catalog")
 @NoArgsConstructor
 @AllArgsConstructor
 @Entity
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/bibliography/Bibliography.java b/backend/src/main/java/cz/zcu/kiv/backendapi/bibliography/Bibliography.java
index f261707..cfceada 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/bibliography/Bibliography.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/bibliography/Bibliography.java
@@ -3,6 +3,7 @@ package cz.zcu.kiv.backendapi.bibliography;
 import cz.zcu.kiv.backendapi.catalog.CatalogEntry;
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 
 import javax.persistence.*;
@@ -12,6 +13,7 @@ import java.io.Serializable;
  * Bibliography entity representing bibliography
  */
 @Data
+@EqualsAndHashCode(exclude = "catalog")
 @NoArgsConstructor
 @AllArgsConstructor
 @Entity
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/catalog/CatalogEntry.java b/backend/src/main/java/cz/zcu/kiv/backendapi/catalog/CatalogEntry.java
index 799ff09..47f5ef2 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/catalog/CatalogEntry.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/catalog/CatalogEntry.java
@@ -5,9 +5,7 @@ import cz.zcu.kiv.backendapi.bibliography.Bibliography;
 import cz.zcu.kiv.backendapi.country.Country;
 import cz.zcu.kiv.backendapi.type.Type;
 import cz.zcu.kiv.backendapi.writtenform.WrittenForm;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.Setter;
+import lombok.*;
 import org.hibernate.annotations.LazyCollection;
 import org.hibernate.annotations.LazyCollectionOption;
 
@@ -20,9 +18,7 @@ import java.util.stream.Collectors;
 /**
  * Catalog entity representing catalog entry
  */
-//@Data
-@Getter
-@Setter
+@Data
 @NoArgsConstructor
 @Entity
 @Table(name = "catalog")
@@ -104,23 +100,23 @@ public class CatalogEntry {
 
         this.name = csvFields.get(1);
         List<String> stringList = processListField(csvFields.get(2));
-        this.alternativeNames = stringList.stream().map(s->new AlternativeName(s, this)).collect(Collectors.toSet());
+        this.alternativeNames = stringList.stream().map(s -> new AlternativeName(s, this)).collect(Collectors.toSet());
 
         this.certainty = processIntField(csvFields.get(3));
         this.latitude = processDoubleField(csvFields.get(4));
         this.longitude = processDoubleField(csvFields.get(5));
 
         stringList = processListField(csvFields.get(6));
-        this.writtenForms = stringList.stream().map(s->new WrittenForm(s, this)).collect(Collectors.toSet());
+        this.writtenForms = stringList.stream().map(s -> new WrittenForm(s, this)).collect(Collectors.toSet());
 
         stringList = processListField(csvFields.get(7));
         this.types = stringList.stream().map(Type::new).collect(Collectors.toSet());
 
         stringList = processListField(csvFields.get(8));
-        this.countries = stringList.stream().map(s->new Country(s, this)).collect(Collectors.toSet());
+        this.countries = stringList.stream().map(s -> new Country(s, this)).collect(Collectors.toSet());
 
         stringList = processListField(csvFields.get(9));
-        this.bibliography = stringList.stream().map(s->new Bibliography(s, this)).collect(Collectors.toSet());
+        this.bibliography = stringList.stream().map(s -> new Bibliography(s, this)).collect(Collectors.toSet());
     }
 
     private int processIntField(String field) {
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/catalog/CatalogEntryRepository.java b/backend/src/main/java/cz/zcu/kiv/backendapi/catalog/CatalogEntryRepository.java
index 20e5ec5..6990d15 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/catalog/CatalogEntryRepository.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/catalog/CatalogEntryRepository.java
@@ -24,8 +24,8 @@ public interface CatalogEntryRepository extends JpaRepository<CatalogEntry, UUID
     @Query("SELECT DISTINCT e FROM CatalogEntry e LEFT JOIN AlternativeName a ON e = a.catalog " +
             "LEFT JOIN Country c ON e = c.catalog " +
             "INNER JOIN e.types t " +
-            "WHERE (?1 = '' OR e.name LIKE ?1 OR a.name LIKE ?1) " +
-            "AND (?2 = '' OR c.name LIKE ?2) " +
-            "AND (?3 = '' OR t.type LIKE ?3)")
+            "WHERE (?1 = '' OR UPPER(e.name) LIKE UPPER(?1) OR UPPER(a.name) LIKE UPPER(?1)) " +
+            "AND (?2 = '' OR UPPER(c.name) LIKE UPPER(?2)) " +
+            "AND (?3 = '' OR UPPER(t.type) LIKE UPPER(?3))")
     Set<CatalogEntry> filterCatalog(String name, String country, String type);
 }
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/catalog/CatalogEntryServiceImpl.java b/backend/src/main/java/cz/zcu/kiv/backendapi/catalog/CatalogEntryServiceImpl.java
index c8e14b0..b82279b 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/catalog/CatalogEntryServiceImpl.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/catalog/CatalogEntryServiceImpl.java
@@ -26,10 +26,31 @@ import java.util.stream.Collectors;
 @RequiredArgsConstructor
 @Slf4j
 public class CatalogEntryServiceImpl implements ICatalogEntryService {
+    /**
+     * Regex for one arbitrary character in string
+     */
+    private static final String WILDCARD_CHARACTER_REGEX = "\\?";
+
+    /**
+     * Regex for one arbitrary character in string used in SQL language
+     */
+    private static final String WILDCARD_CHARACTER_REGEX_SQL = "_";
+
+    /**
+     * Regex for any number of arbitrary characters in string
+     */
+    private static final String WILDCARD_CHARACTERS_REGEX = "\\*";
+
+    /**
+     * Regex for any number of arbitrary characters in string used in SQL language
+     */
+    private static final String WILDCARD_CHARACTERS_REGEX_SQL = "%";
+
     /**
      * Message for exception when catalog entry is not found by id
      */
     private static final String CATALOG_ENTRY_NOT_FOUND = "Catalog entry not found";
+
     /**
      * Catalog repository
      */
@@ -78,6 +99,12 @@ public class CatalogEntryServiceImpl implements ICatalogEntryService {
     @Override
     public List<CatalogEntryDto> getCatalog(String name, String country, String type) {
         log.info("Retrieving catalog");
+        name = name.replaceAll(WILDCARD_CHARACTER_REGEX, WILDCARD_CHARACTER_REGEX_SQL);
+        name = name.replaceAll(WILDCARD_CHARACTERS_REGEX, WILDCARD_CHARACTERS_REGEX_SQL);
+        country = country.replaceAll(WILDCARD_CHARACTER_REGEX, WILDCARD_CHARACTER_REGEX_SQL);
+        country = country.replaceAll(WILDCARD_CHARACTERS_REGEX, WILDCARD_CHARACTERS_REGEX_SQL);
+        type = type.replaceAll(WILDCARD_CHARACTER_REGEX, WILDCARD_CHARACTER_REGEX_SQL);
+        type = type.replaceAll(WILDCARD_CHARACTERS_REGEX, WILDCARD_CHARACTERS_REGEX_SQL);
         Set<CatalogEntry> entities = catalogEntryRepository.filterCatalog(name, country, type);
         return entities.stream().map(this::convertEntityToDto).collect(Collectors.toList());
     }
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/config/DataInitiator.java b/backend/src/main/java/cz/zcu/kiv/backendapi/config/DataInitiator.java
index 169d864..33cb91c 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/config/DataInitiator.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/config/DataInitiator.java
@@ -43,7 +43,9 @@ public class DataInitiator implements ApplicationListener<ContextRefreshedEvent>
     @Transactional
     public void onApplicationEvent(ContextRefreshedEvent event) {
         List<Type> types = loadTypes();
-        typeService.saveTypes(types);
+        for (Type type : types) {
+            typeService.saveType(type);
+        }
         List<CatalogEntry> catalog = loadCatalog();
         catalogService.saveCatalog(catalog);
 
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/country/Country.java b/backend/src/main/java/cz/zcu/kiv/backendapi/country/Country.java
index 26dec24..bb14878 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/country/Country.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/country/Country.java
@@ -3,6 +3,7 @@ package cz.zcu.kiv.backendapi.country;
 import cz.zcu.kiv.backendapi.catalog.CatalogEntry;
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 
 import javax.persistence.*;
@@ -12,6 +13,7 @@ import java.io.Serializable;
  * Country entity representing country of geographic entry
  */
 @Data
+@EqualsAndHashCode(exclude = "catalog")
 @NoArgsConstructor
 @AllArgsConstructor
 @Entity
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/password/PasswordDto.java b/backend/src/main/java/cz/zcu/kiv/backendapi/password/PasswordDto.java
deleted file mode 100644
index 3cfa4c3..0000000
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/password/PasswordDto.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package cz.zcu.kiv.backendapi.password;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import cz.zcu.kiv.backendapi.validation.PasswordMatches;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
-import javax.validation.constraints.Size;
-
-/**
- * Password data object
- */
-@Data
-@PasswordMatches
-@NoArgsConstructor
-public class PasswordDto {
-    /**
-     * Minimal length for password
-     */
-    @JsonIgnore
-    private  static final int MIN_PASSWORD_LENGTH = 8;
-
-    /**
-     * New password - must be strong enough
-     */
-    @NotNull(message = "Password must not be null")
-    @NotEmpty(message = "Password must not be empty")
-    @Size(min = MIN_PASSWORD_LENGTH, message = "Password must be at least " + MIN_PASSWORD_LENGTH + " characters long")
-    private String password;
-
-    /**
-     * New password confirmation - must match new password
-     */
-    @NotNull(message = "Confirmation password must not be null")
-    @NotEmpty(message = "Confirmation password must not be empty")
-    private String confirmationPassword;
-
-}
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/permission/PermissionDto.java b/backend/src/main/java/cz/zcu/kiv/backendapi/permission/PermissionDto.java
deleted file mode 100644
index 7a783c4..0000000
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/permission/PermissionDto.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package cz.zcu.kiv.backendapi.permission;
-
-import lombok.Data;
-
-/**
- * Permission DTO
- */
-@Data
-public class PermissionDto {
-    /**
-     * Whether user has 'READ' authority
-     */
-    private boolean canRead;
-
-    /**
-     * Whether user has 'WRITE' authority
-     */
-    private boolean canWrite;
-
-    /**
-     * Whether user has 'DELETE' authority
-     */
-    private boolean canDelete;
-}
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/security/jwt/JwtUtils.java b/backend/src/main/java/cz/zcu/kiv/backendapi/security/jwt/JwtUtils.java
index c853ebf..01404b9 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/security/jwt/JwtUtils.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/security/jwt/JwtUtils.java
@@ -68,7 +68,7 @@ public class JwtUtils {
      */
     @Bean
     public Algorithm getAlgorithm() {
-        return Algorithm.HMAC256(secretKey.getBytes());
+        return Algorithm.HMAC512(secretKey.getBytes());
     }
 
     /**
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/type/ITypeService.java b/backend/src/main/java/cz/zcu/kiv/backendapi/type/ITypeService.java
index ad89f2e..c8a921f 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/type/ITypeService.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/type/ITypeService.java
@@ -1,6 +1,5 @@
 package cz.zcu.kiv.backendapi.type;
 
-import java.util.List;
 import java.util.Optional;
 
 /**
@@ -8,9 +7,18 @@ import java.util.Optional;
  */
 public interface ITypeService {
 
+    /**
+     * Saves on type
+     *
+     * @param type type
+     */
     void saveType(Type type);
 
-    void saveTypes(List<Type> typesEntities);
-
+    /**
+     * Returns type by name (optional)
+     *
+     * @param type type name
+     * @return type by name (optional)
+     */
     Optional<Type> getTypeByName(String type);
 }
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/type/TypeServiceImpl.java b/backend/src/main/java/cz/zcu/kiv/backendapi/type/TypeServiceImpl.java
index bd8d4a0..2cc5998 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/type/TypeServiceImpl.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/type/TypeServiceImpl.java
@@ -4,7 +4,6 @@ import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.List;
 import java.util.Optional;
 
 /**
@@ -24,11 +23,6 @@ public class TypeServiceImpl implements ITypeService {
         typeRepository.save(type);
     }
 
-    @Override
-    public void saveTypes(List<Type> typesEntities) {
-        typeRepository.saveAll(typesEntities);
-    }
-
     @Override
     public Optional<Type> getTypeByName(String type) {
         return typeRepository.findById(type);
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/user/IUserService.java b/backend/src/main/java/cz/zcu/kiv/backendapi/user/IUserService.java
index 82adafa..61e0f29 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/user/IUserService.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/user/IUserService.java
@@ -1,7 +1,7 @@
 package cz.zcu.kiv.backendapi.user;
 
-import cz.zcu.kiv.backendapi.password.PasswordDto;
-import cz.zcu.kiv.backendapi.permission.PermissionDto;
+import cz.zcu.kiv.backendapi.user.password.PasswordDto;
+import cz.zcu.kiv.backendapi.user.permission.PermissionDto;
 
 import java.util.List;
 
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserController.java b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserController.java
index b284f98..381a739 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserController.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserController.java
@@ -6,7 +6,7 @@ import com.auth0.jwt.algorithms.Algorithm;
 import com.auth0.jwt.interfaces.DecodedJWT;
 import com.google.common.base.Strings;
 import cz.zcu.kiv.backendapi.exception.ApiRequestException;
-import cz.zcu.kiv.backendapi.password.PasswordDto;
+import cz.zcu.kiv.backendapi.user.password.PasswordDto;
 import cz.zcu.kiv.backendapi.security.jwt.JwtUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -15,7 +15,7 @@ import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.web.bind.annotation.*;
-import cz.zcu.kiv.backendapi.permission.PermissionDto;
+import cz.zcu.kiv.backendapi.user.permission.PermissionDto;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserDto.java b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserDto.java
index ab5e7fe..58fec75 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserDto.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserDto.java
@@ -1,10 +1,10 @@
 package cz.zcu.kiv.backendapi.user;
 
-import cz.zcu.kiv.backendapi.password.PasswordDto;
+import cz.zcu.kiv.backendapi.user.password.PasswordDto;
 import cz.zcu.kiv.backendapi.validation.PasswordMatches;
 import cz.zcu.kiv.backendapi.validation.ValidEmail;
 import lombok.Data;
-import cz.zcu.kiv.backendapi.permission.PermissionDto;
+import cz.zcu.kiv.backendapi.user.permission.PermissionDto;
 
 import javax.validation.Valid;
 import javax.validation.constraints.NotEmpty;
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserEntity.java b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserEntity.java
index 4aecd90..62e222a 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserEntity.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserEntity.java
@@ -1,5 +1,6 @@
 package cz.zcu.kiv.backendapi.user;
 
+import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import org.hibernate.annotations.Type;
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserServiceImpl.java b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserServiceImpl.java
index fe8a31f..ef9df9a 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserServiceImpl.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/user/UserServiceImpl.java
@@ -1,7 +1,7 @@
 package cz.zcu.kiv.backendapi.user;
 
 import cz.zcu.kiv.backendapi.exception.ApiRequestException;
-import cz.zcu.kiv.backendapi.password.PasswordDto;
+import cz.zcu.kiv.backendapi.user.password.PasswordDto;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
@@ -12,7 +12,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
-import cz.zcu.kiv.backendapi.permission.PermissionDto;
+import cz.zcu.kiv.backendapi.user.permission.PermissionDto;
 
 import java.util.List;
 import java.util.stream.Collectors;
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/user/password/PasswordDto.java b/backend/src/main/java/cz/zcu/kiv/backendapi/user/password/PasswordDto.java
new file mode 100644
index 0000000..171d927
--- /dev/null
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/user/password/PasswordDto.java
@@ -0,0 +1,40 @@
+package cz.zcu.kiv.backendapi.user.password;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import cz.zcu.kiv.backendapi.validation.PasswordMatches;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+/**
+ * Password data object
+ */
+@Data
+@PasswordMatches
+@NoArgsConstructor
+public class PasswordDto {
+    /**
+     * Minimal length for password
+     */
+    @JsonIgnore
+    private  static final int MIN_PASSWORD_LENGTH = 8;
+
+    /**
+     * New password - must be strong enough
+     */
+    @NotNull(message = "Password must not be null")
+    @NotEmpty(message = "Password must not be empty")
+    @Size(min = MIN_PASSWORD_LENGTH, message = "Password must be at least " + MIN_PASSWORD_LENGTH + " characters long")
+    private String password;
+
+    /**
+     * New password confirmation - must match new password
+     */
+    @NotNull(message = "Confirmation password must not be null")
+    @NotEmpty(message = "Confirmation password must not be empty")
+    private String confirmationPassword;
+
+}
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/user/permission/PermissionDto.java b/backend/src/main/java/cz/zcu/kiv/backendapi/user/permission/PermissionDto.java
new file mode 100644
index 0000000..eb54d15
--- /dev/null
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/user/permission/PermissionDto.java
@@ -0,0 +1,24 @@
+package cz.zcu.kiv.backendapi.user.permission;
+
+import lombok.Data;
+
+/**
+ * Permission DTO
+ */
+@Data
+public class PermissionDto {
+    /**
+     * Whether user has 'READ' authority
+     */
+    private boolean canRead;
+
+    /**
+     * Whether user has 'WRITE' authority
+     */
+    private boolean canWrite;
+
+    /**
+     * Whether user has 'DELETE' authority
+     */
+    private boolean canDelete;
+}
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/validation/PasswordMatchesValidator.java b/backend/src/main/java/cz/zcu/kiv/backendapi/validation/PasswordMatchesValidator.java
index 76c08e9..a243457 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/validation/PasswordMatchesValidator.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/validation/PasswordMatchesValidator.java
@@ -1,6 +1,6 @@
 package cz.zcu.kiv.backendapi.validation;
 
-import cz.zcu.kiv.backendapi.password.PasswordDto;
+import cz.zcu.kiv.backendapi.user.password.PasswordDto;
 
 import javax.validation.ConstraintValidator;
 import javax.validation.ConstraintValidatorContext;
diff --git a/backend/src/main/java/cz/zcu/kiv/backendapi/writtenform/WrittenForm.java b/backend/src/main/java/cz/zcu/kiv/backendapi/writtenform/WrittenForm.java
index 8d98d74..32cd62d 100644
--- a/backend/src/main/java/cz/zcu/kiv/backendapi/writtenform/WrittenForm.java
+++ b/backend/src/main/java/cz/zcu/kiv/backendapi/writtenform/WrittenForm.java
@@ -3,6 +3,7 @@ package cz.zcu.kiv.backendapi.writtenform;
 import cz.zcu.kiv.backendapi.catalog.CatalogEntry;
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 
 import javax.persistence.*;
@@ -12,6 +13,7 @@ import java.io.Serializable;
  * Written form entity representing written form of geographic entry
  */
 @Data
+@EqualsAndHashCode(exclude = "catalog")
 @NoArgsConstructor
 @AllArgsConstructor
 @Entity
diff --git a/backend/src/test/java/cz/zcu/kiv/backendapi/catalog/CatalogEntryRepositoryTest.java b/backend/src/test/java/cz/zcu/kiv/backendapi/catalog/CatalogEntryRepositoryTest.java
new file mode 100644
index 0000000..0f9269d
--- /dev/null
+++ b/backend/src/test/java/cz/zcu/kiv/backendapi/catalog/CatalogEntryRepositoryTest.java
@@ -0,0 +1,329 @@
+package cz.zcu.kiv.backendapi.catalog;
+
+import cz.zcu.kiv.backendapi.alternativename.AlternativeName;
+import cz.zcu.kiv.backendapi.country.Country;
+import cz.zcu.kiv.backendapi.type.Type;
+import cz.zcu.kiv.backendapi.type.TypeRepository;
+import cz.zcu.kiv.backendapi.user.UserEntity;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+
+@DataJpaTest
+class CatalogEntryRepositoryTest {
+
+    @Autowired
+    private CatalogEntryRepository underTest;
+
+    @Autowired
+    private TypeRepository typeRepository;
+
+    private CatalogEntry catalogEntry1;
+    private CatalogEntry catalogEntry2;
+    private CatalogEntry catalogEntry3;
+    private CatalogEntry catalogEntry4;
+    private CatalogEntry catalogEntry5;
+
+    @BeforeEach
+    void setUp() {
+        Type typeCountry = new Type("country");
+        Type typeCity = new Type("city");
+        Type typeCapitalCity = new Type("capital city new");
+        Type typeCityUpper = new Type("City");
+        Type typeCountri = new Type("countri");
+        typeRepository.saveAll(List.of(typeCountry, typeCity, typeCapitalCity, typeCityUpper, typeCountri));
+
+        String nameFirst = "first";
+        String nameFirstUpper = "First";
+        String nameSecond = "second";
+        String nameSedond = "sedond";
+        String nameThird = "third";
+        String nameTwelve = "twelve";
+        String nameTwentyUpper = "TWENTY";
+
+        String countryAaa = "aaa";
+        String countryAaaUpper = "AAA";
+        String countryBbb = "bbb";
+        String countryBccb = "bccb";
+        String countryCcc = "ccc";
+        String countryDdd = "ddd";
+        String countryDcd = "dcd";
+
+        catalogEntry1 = new CatalogEntry();
+        catalogEntry2 = new CatalogEntry();
+        catalogEntry3 = new CatalogEntry();
+        catalogEntry4 = new CatalogEntry();
+        catalogEntry5 = new CatalogEntry();
+
+        catalogEntry1.setName(nameFirst);
+        catalogEntry1.setTypes(Set.of(typeCountry, typeCity));
+        catalogEntry1.setCountries(Set.of(new Country(countryAaa, catalogEntry1), new Country(countryDcd, catalogEntry1)));
+
+        catalogEntry2.setName(nameSecond);
+        catalogEntry2.setAlternativeNames(Set.of(new AlternativeName(nameTwelve, catalogEntry2), new AlternativeName(nameThird, catalogEntry2)));
+        catalogEntry2.setTypes(Set.of(typeCountry, typeCityUpper));
+        catalogEntry2.setCountries(Set.of(new Country(countryAaa, catalogEntry2), new Country(countryBbb, catalogEntry2)));
+
+        catalogEntry3.setName(nameThird);
+        catalogEntry3.setAlternativeNames(Set.of(new AlternativeName(nameTwentyUpper, catalogEntry3), new AlternativeName(nameSedond, catalogEntry3)));
+        catalogEntry3.setTypes(Set.of(typeCountri, typeCapitalCity));
+        catalogEntry3.setCountries(Set.of(new Country(countryAaaUpper, catalogEntry3), new Country(countryCcc, catalogEntry3)));
+
+
+        catalogEntry4.setAlternativeNames(Set.of(new AlternativeName(nameTwelve, catalogEntry4), new AlternativeName(nameFirstUpper, catalogEntry4)));
+        catalogEntry4.setTypes(Set.of(typeCountri, typeCountry));
+        catalogEntry4.setCountries(Set.of(new Country(countryBccb, catalogEntry4), new Country(countryDdd, catalogEntry4)));
+
+
+        catalogEntry5.setName(nameSedond);
+        catalogEntry5.setAlternativeNames(Set.of(new AlternativeName(nameThird, catalogEntry5)));
+        catalogEntry5.setTypes(Set.of(typeCountri));
+
+        underTest.saveAll(List.of(catalogEntry1, catalogEntry2, catalogEntry3, catalogEntry4, catalogEntry5));
+
+
+    }
+
+    @AfterEach
+    void tearDown() {
+        underTest.deleteAll();
+    }
+
+    @Test
+    void itShouldReturnAll() {
+        // given
+        String name = "";
+        String country = "";
+        String type = "";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(5);
+    }
+
+    @Test
+    void testName() {
+        // given
+        String name = "first";
+        String country = "";
+        String type = "";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(2);
+        assertTrue(filterResult.contains(catalogEntry1));
+        assertTrue(filterResult.contains(catalogEntry4));
+    }
+
+    @Test
+    void testWildcardCharacterName() {
+        // given
+        String name = "se_ond";
+        String country = "";
+        String type = "";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(3);
+        assertTrue(filterResult.contains(catalogEntry2));
+        assertTrue(filterResult.contains(catalogEntry3));
+        assertTrue(filterResult.contains(catalogEntry5));
+    }
+
+    @Test
+    void testWildcardCharactersName() {
+        // given
+        String name = "twe%";
+        String country = "";
+        String type = "";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(3);
+        assertTrue(filterResult.contains(catalogEntry2));
+        assertTrue(filterResult.contains(catalogEntry3));
+        assertTrue(filterResult.contains(catalogEntry4));
+    }
+
+    @Test
+    void testCountry() {
+        // given
+        String name = "";
+        String country = "aaa";
+        String type = "";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(3);
+        assertTrue(filterResult.contains(catalogEntry1));
+        assertTrue(filterResult.contains(catalogEntry2));
+        assertTrue(filterResult.contains(catalogEntry3));
+    }
+
+    @Test
+    void testWildcardCharacterCountry() {
+        // given
+        String name = "";
+        String country = "d_d";
+        String type = "";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(2);
+        assertTrue(filterResult.contains(catalogEntry1));
+        assertTrue(filterResult.contains(catalogEntry4));
+    }
+
+    @Test
+    void testWildcardCharactersCountry() {
+        // given
+        String name = "";
+        String country = "b%b";
+        String type = "";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(2);
+        assertTrue(filterResult.contains(catalogEntry2));
+        assertTrue(filterResult.contains(catalogEntry4));
+    }
+
+
+    @Test
+    void testType() {
+        // given
+        String name = "";
+        String country = "";
+        String type = "city";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(2);
+        assertTrue(filterResult.contains(catalogEntry1));
+        assertTrue(filterResult.contains(catalogEntry2));
+    }
+
+    @Test
+    void testWildcardCharacterType() {
+        // given
+        String name = "";
+        String country = "";
+        String type = "countr_";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(5);
+        assertTrue(filterResult.contains(catalogEntry1));
+        assertTrue(filterResult.contains(catalogEntry2));
+        assertTrue(filterResult.contains(catalogEntry3));
+        assertTrue(filterResult.contains(catalogEntry4));
+        assertTrue(filterResult.contains(catalogEntry5));
+    }
+
+    @Test
+    void testWildcardCharactersType() {
+        // given
+        String name = "";
+        String country = "";
+        String type = "%city%";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(3);
+        assertTrue(filterResult.contains(catalogEntry1));
+        assertTrue(filterResult.contains(catalogEntry2));
+        assertTrue(filterResult.contains(catalogEntry3));
+    }
+
+    @Test
+    void testNameAndCountry() {
+        // given
+        String name = "third";
+        String country = "aaa";
+        String type = "";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(2);
+        assertTrue(filterResult.contains(catalogEntry2));
+        assertTrue(filterResult.contains(catalogEntry3));
+    }
+
+    @Test
+    void testNameAndType() {
+        // given
+        String name = "third";
+        String country = "";
+        String type = "countri";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(2);
+        assertTrue(filterResult.contains(catalogEntry3));
+        assertTrue(filterResult.contains(catalogEntry5));
+    }
+
+    @Test
+    void testCountryAndType() {
+        // given
+        String name = "";
+        String country = "ddd";
+        String type = "country";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(1);
+        assertTrue(filterResult.contains(catalogEntry4));
+    }
+
+    @Test
+    void testAll() {
+        // given
+        String name = "third";
+        String country = "AAA";
+        String type = "countri";
+
+        // when
+        Set<CatalogEntry> filterResult = underTest.filterCatalog(name, country, type);
+
+        // then
+        assertThat(filterResult.size()).isEqualTo(1);
+        assertTrue(filterResult.contains(catalogEntry3));
+    }
+}
\ No newline at end of file
diff --git a/backend/src/test/java/cz/zcu/kiv/backendapi/titlepage/TitlePageServiceImplementationTest.java b/backend/src/test/java/cz/zcu/kiv/backendapi/titlepage/TitlePageServiceImplementationTest.java
new file mode 100644
index 0000000..ef9d791
--- /dev/null
+++ b/backend/src/test/java/cz/zcu/kiv/backendapi/titlepage/TitlePageServiceImplementationTest.java
@@ -0,0 +1,52 @@
+package cz.zcu.kiv.backendapi.titlepage;
+
+import cz.zcu.kiv.backendapi.type.Type;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+class TitlePageServiceImplementationTest {
+
+    @Mock
+    private TitlePageRepository titlePageRepository;
+
+    private TitlePageServiceImplementation underTest;
+
+    @BeforeEach
+    void setUp() {
+        underTest = new TitlePageServiceImplementation(titlePageRepository);
+    }
+
+    @Test
+    void testAddTitlePage() {
+        // given
+        long id = 1;
+        TitlePage titlePage = new TitlePage();
+        titlePage.setTitle("title");
+        titlePage.setContent("content");
+
+        // when
+
+        underTest.addOrUpdateTitlePage(id, titlePage);
+
+        // then
+        ArgumentCaptor<TitlePage> argumentCaptor = ArgumentCaptor.forClass(TitlePage.class);
+
+        verify(titlePageRepository).save(argumentCaptor.capture());
+
+        TitlePage capturedTitlePage = argumentCaptor.getValue();
+
+        titlePage.setId(id);
+
+        assertThat(capturedTitlePage).isEqualTo(titlePage);
+
+    }
+}
\ No newline at end of file
diff --git a/backend/src/test/java/cz/zcu/kiv/backendapi/type/TypeServiceImplTest.java b/backend/src/test/java/cz/zcu/kiv/backendapi/type/TypeServiceImplTest.java
new file mode 100644
index 0000000..51fcda5
--- /dev/null
+++ b/backend/src/test/java/cz/zcu/kiv/backendapi/type/TypeServiceImplTest.java
@@ -0,0 +1,52 @@
+package cz.zcu.kiv.backendapi.type;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+class TypeServiceImplTest {
+
+    @Mock
+    private TypeRepository typeRepository;
+
+    private TypeServiceImpl underTest;
+
+    @BeforeEach
+    void setUp() {
+        underTest = new TypeServiceImpl(typeRepository);
+    }
+
+    @Test
+    void saveType() {
+        // when
+        Type type = new Type("type");
+        underTest.saveType(type);
+
+        // then
+        ArgumentCaptor<Type> argumentCaptor = ArgumentCaptor.forClass(Type.class);
+
+        verify(typeRepository).save(argumentCaptor.capture());
+
+        Type capturedType = argumentCaptor.getValue();
+
+        assertThat(capturedType).isEqualTo(type);
+    }
+
+    @Test
+    void getTypeByName() {
+        // when
+        String typeName = "type";
+        underTest.getTypeByName(typeName);
+
+        // then
+        verify(typeRepository).findById(typeName);
+    }
+}
\ No newline at end of file
diff --git a/backend/src/test/java/cz/zcu/kiv/backendapi/user/UserRepositoryTest.java b/backend/src/test/java/cz/zcu/kiv/backendapi/user/UserRepositoryTest.java
new file mode 100644
index 0000000..291a616
--- /dev/null
+++ b/backend/src/test/java/cz/zcu/kiv/backendapi/user/UserRepositoryTest.java
@@ -0,0 +1,48 @@
+package cz.zcu.kiv.backendapi.user;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DataJpaTest
+class UserRepositoryTest {
+
+    @Autowired
+    private UserRepository underTest;
+
+    @AfterEach
+    void tearDown() {
+        underTest.deleteAll();
+    }
+
+    @Test
+    void itShouldFindByEmail() {
+        // given
+        String email = "user@user.com";
+        UserEntity userEntity = new UserEntity("User", email, "password", (byte) 0, false);
+        underTest.save(userEntity);
+
+        // when
+        Optional<UserEntity> userFoundByEmail = underTest.findByEmail(email);
+
+        // then
+        assertTrue(userFoundByEmail.isPresent());
+    }
+
+    @Test
+    void itShouldNotFindByEmail() {
+        // given
+        String email = "user@user.com";
+
+        // when
+        Optional<UserEntity> userFoundByEmail = underTest.findByEmail(email);
+
+        // then
+        assertTrue(userFoundByEmail.isEmpty());
+    }
+}
\ No newline at end of file
diff --git a/backend/src/test/resources/application.properties b/backend/src/test/resources/application.properties
new file mode 100644
index 0000000..40e4da4
--- /dev/null
+++ b/backend/src/test/resources/application.properties
@@ -0,0 +1,12 @@
+spring.datasource.url=jdbc:h2://mem:db;DB_CLOSE_DELAY=-1
+spring.datasource.username=sa
+spring.datasource.password=sa
+spring.datasource.driver-class-name=org.h2.Driver
+
+spring.jpa.hibernate.ddl-auto=create-drop
+spring.jpa.generate-ddl=true
+spring.jpa.open-in-view=false
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL10Dialect
+spring.jpa.properties.hibernate.format_sql=true
+spring.jpa.show-sql=true
+
