diff --git a/application/model/src/main/java/org/fuseri/model/dto/course/LanguageTypeDto.java b/application/model/src/main/java/org/fuseri/model/dto/course/LanguageTypeDto.java
index 84214fd415d40511029aebd52f60bbc526ff8e7f..2b7f9fde7906edc96a7ccee7d2f926e5c83b3d0b 100644
--- a/application/model/src/main/java/org/fuseri/model/dto/course/LanguageTypeDto.java
+++ b/application/model/src/main/java/org/fuseri/model/dto/course/LanguageTypeDto.java
@@ -1,5 +1,8 @@
 package org.fuseri.model.dto.course;
 
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
 public enum LanguageTypeDto {
     ENGLISH,
     GERMAN,
diff --git a/application/model/src/main/java/org/fuseri/model/dto/user/AddressDto.java b/application/model/src/main/java/org/fuseri/model/dto/user/AddressDto.java
index ff0af99960e0daa36f948a11bee5f9bdaf1721f0..bcbbbe4a3a2280476f47e72ab406052843a67b50 100644
--- a/application/model/src/main/java/org/fuseri/model/dto/user/AddressDto.java
+++ b/application/model/src/main/java/org/fuseri/model/dto/user/AddressDto.java
@@ -1,11 +1,15 @@
 package org.fuseri.model.dto.user;
 
 import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
 import lombok.Getter;
+import lombok.NoArgsConstructor;
 import lombok.Setter;
 
 @Getter
 @Setter
+@AllArgsConstructor
+@NoArgsConstructor
 public class AddressDto {
     @NotBlank
     private String country;
diff --git a/application/model/src/main/java/org/fuseri/model/dto/user/UserAddLanguageDto.java b/application/model/src/main/java/org/fuseri/model/dto/user/UserAddLanguageDto.java
index ebe92f4f3068a6b04c2beec0c17b268b8a2b4c17..271ce6435594e1a25946db945556ad8c048943cc 100644
--- a/application/model/src/main/java/org/fuseri/model/dto/user/UserAddLanguageDto.java
+++ b/application/model/src/main/java/org/fuseri/model/dto/user/UserAddLanguageDto.java
@@ -1,14 +1,23 @@
 package org.fuseri.model.dto.user;
 
+import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotNull;
-import org.fuseri.model.dto.common.DomainObjectDto;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
 import org.fuseri.model.dto.course.LanguageTypeDto;
 import org.fuseri.model.dto.course.ProficiencyLevelDto;
 
-public class UserAddLanguageDto extends DomainObjectDto {
+@Getter
+@Setter
+@AllArgsConstructor
+public class UserAddLanguageDto {
 
     @NotNull
+    @Valid
     LanguageTypeDto language;
+
     @NotNull
+    @Valid
     ProficiencyLevelDto proficiency;
 }
diff --git a/application/model/src/main/java/org/fuseri/model/dto/user/UserCreateDto.java b/application/model/src/main/java/org/fuseri/model/dto/user/UserCreateDto.java
index 403960952d468ad7afe797b014f0a627f65ceb49..9be887e9319946ecf9e73f1188e7c94201a1d874 100644
--- a/application/model/src/main/java/org/fuseri/model/dto/user/UserCreateDto.java
+++ b/application/model/src/main/java/org/fuseri/model/dto/user/UserCreateDto.java
@@ -1,14 +1,15 @@
 package org.fuseri.model.dto.user;
 
+import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
-import lombok.NonNull;
-import org.fuseri.model.dto.common.DomainObjectDto;
+import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.Setter;
 
 @Getter
 @Setter
+@AllArgsConstructor
 public class UserCreateDto {
 
     @NotBlank
@@ -22,5 +23,6 @@ public class UserCreateDto {
     @NotBlank
     private String lastName;
     @NotNull
+    @Valid
     private AddressDto address;
 }
diff --git a/application/model/src/main/java/org/fuseri/model/dto/user/UserLoginDto.java b/application/model/src/main/java/org/fuseri/model/dto/user/UserLoginDto.java
index 0aba85a7f95a3b40719101c8e332a10d472ef0b5..23972ce45017b07c3507535167952d16dc8ecd15 100644
--- a/application/model/src/main/java/org/fuseri/model/dto/user/UserLoginDto.java
+++ b/application/model/src/main/java/org/fuseri/model/dto/user/UserLoginDto.java
@@ -1,12 +1,13 @@
 package org.fuseri.model.dto.user;
 
 import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.Setter;
-import org.fuseri.model.dto.common.DomainObjectDto;
 
 @Getter
 @Setter
+@AllArgsConstructor
 public class UserLoginDto {
     @NotBlank
     private String username;
diff --git a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserController.java b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserController.java
index 31cfd86e0d4f9b7ab652143468c7a61666609e00..d0c6429b245ad650d6e5fe747d9db2fb48e88681 100644
--- a/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserController.java
+++ b/application/module-language-school/src/main/java/org/fuseri/modulelanguageschool/user/UserController.java
@@ -62,7 +62,7 @@ public class UserController {
     }
 
     @PostMapping("/login")
-    public String login(@RequestBody UserLoginDto dto) {
+    public String login(@Valid @RequestBody UserLoginDto dto) {
         return String.format("User %s has spawned", dto.getUsername());
     }
 
diff --git a/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserControllerTest.java b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..85aa5f33c134528abba0f729717a020278e440bb
--- /dev/null
+++ b/application/module-language-school/src/test/java/org/fuseri/modulelanguageschool/user/UserControllerTest.java
@@ -0,0 +1,227 @@
+package org.fuseri.modulelanguageschool.user;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.fuseri.model.dto.course.LanguageTypeDto;
+import org.fuseri.model.dto.course.ProficiencyLevelDto;
+import org.fuseri.model.dto.user.*;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+
+@SpringBootTest
+@AutoConfigureMockMvc
+class UserControllerTest {
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    private static final AddressDto ADDRESS_TO_CREATE = new AddressDto(
+            "Czechia", "Brno", "Masarykova", "45", "90033");
+
+    private static final List<AddressDto> INVALID_ADDRESSES = List.of(
+            new AddressDto("", "Brno", "Masarykova", "45", "90033"),
+            new AddressDto("Czechia", "", "Masarykova", "45", "90033"),
+            new AddressDto("Czechia", "Brno", "", "45", "90033"),
+            new AddressDto("Czechia", "Brno", "Masarykova", "", "90033"),
+            new AddressDto("Czechia", "Brno", "Masarykova", "45", ""),
+            new AddressDto(null, "Brno", "Masarykova", "45", "90033"),
+            new AddressDto("Czechia", null, "Masarykova", "45", "90033"),
+            new AddressDto("Czechia", "Brno", null, "45", "90033"),
+            new AddressDto("Czechia", "Brno", "Masarykova", null, "90033")
+    );
+
+    private static final UserCreateDto USER_TO_CREATE = new UserCreateDto(
+            "xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af",
+            "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE);
+
+    private static final UserLoginDto USER_TO_LOGIN = new UserLoginDto(
+            "xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af");
+
+    private static Stream<UserCreateDto> invalidUsers() {
+        var invalidUsers = Stream.of(
+                new UserCreateDto("", "1c1bbf66-6585-4978-886b-b126335ff3af",
+                        "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE),
+                new UserCreateDto("xnovak", "",
+                        "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE),
+                new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af",
+                        "", "Peter", "Novak", ADDRESS_TO_CREATE),
+                new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af",
+                        "xnovak@emample.com", "", "Novak", ADDRESS_TO_CREATE),
+                new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af",
+                        "xnovak@emample.com", "Peter", "", ADDRESS_TO_CREATE),
+                new UserCreateDto(null, "1c1bbf66-6585-4978-886b-b126335ff3af",
+                        "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE),
+                new UserCreateDto("xnovak", null,
+                        "xnovak@emample.com", "Peter", "Novak", ADDRESS_TO_CREATE),
+                new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af",
+                        null, "Peter", "Novak", ADDRESS_TO_CREATE),
+                new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af",
+                        "xnovak@emample.com", null, "Novak", ADDRESS_TO_CREATE),
+                new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af",
+                        "xnovak@emample.com", "Peter", null, ADDRESS_TO_CREATE),
+                new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af",
+                        "xnovak@emample.com", "Peter", "Novak", null)
+        );
+
+        var invalidAddressUsers = new ArrayList<UserCreateDto>();
+        for (var invalidAddress : INVALID_ADDRESSES) {
+            invalidAddressUsers.add(new UserCreateDto("xnovak", "1c1bbf66-6585-4978-886b-b126335ff3af",
+                    "xnovak@emample.com", "Peter", "Novak", invalidAddress));
+        }
+
+        return Stream.concat(invalidUsers, invalidAddressUsers.stream());
+    }
+
+    private static Stream<UserLoginDto> invalidLoginDtoStream() {
+        return Stream.of(
+                new UserLoginDto("", "1c1bbf66-6585-4978-886b-b126335ff3af"),
+                new UserLoginDto("xnovak", ""),
+                new UserLoginDto(null, "1c1bbf66-6585-4978-886b-b126335ff3af"),
+                new UserLoginDto("xnovak", null));
+    }
+
+    @Test
+    void create() throws Exception {
+        mockMvc.perform(post("/users")
+                        .content(asJsonString(USER_TO_CREATE))
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk());
+    }
+
+    @ParameterizedTest
+    @MethodSource("invalidUsers")
+    void createInvalidUser(UserCreateDto user) throws Exception {
+        mockMvc.perform(post("/users")
+                        .content(asJsonString(user))
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().is4xxClientError());
+    }
+
+    @Test
+    void findUser() throws Exception {
+        String response = mockMvc.perform(post("/users")
+                        .content(asJsonString(USER_TO_CREATE))
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
+
+        String id = objectMapper.readValue(response, UserDto.class).getId();
+
+        mockMvc.perform(get("/users/{id}", id))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.id").value(id));
+    }
+
+    @Test
+    void findAll() throws Exception {
+        mockMvc.perform(get("/users/all")
+                        .param("page", "0"))
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    void deleteUser() throws Exception {
+        String response = mockMvc.perform(post("/users")
+                        .content(asJsonString(USER_TO_CREATE))
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
+
+        String id = objectMapper.readValue(response, UserDto.class).getId();
+
+        mockMvc.perform(delete("/users/{id}", id)
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    void update() throws Exception {
+        String response = mockMvc.perform(post("/users")
+                        .content(asJsonString(USER_TO_CREATE))
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
+
+        String id = objectMapper.readValue(response, UserDto.class).getId();
+
+        var updatedUsername = "novak";
+        var userToUpdate = new UserCreateDto(
+                USER_TO_CREATE.getUsername(), USER_TO_CREATE.getPassword(), USER_TO_CREATE.getEmail(),
+                USER_TO_CREATE.getFirstName(), USER_TO_CREATE.getLastName(), USER_TO_CREATE.getAddress());
+        userToUpdate.setUsername(updatedUsername);
+
+        mockMvc.perform(put("/users/update/{id}", id)
+                        .content(asJsonString(userToUpdate))
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.username").value(updatedUsername));
+    }
+
+    @Test
+    void login() throws Exception {
+        mockMvc.perform(post("/users/login")
+                        .content(asJsonString(USER_TO_LOGIN))
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk());
+    }
+
+    @ParameterizedTest
+    @MethodSource("invalidLoginDtoStream")
+    void loginInvalidDto(UserLoginDto loginDto) throws Exception {
+        mockMvc.perform(post("/users/login")
+                        .content(asJsonString(loginDto))
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().is4xxClientError());
+    }
+
+    @Test
+    void logout() throws Exception {
+        String response = mockMvc.perform(post("/users")
+                        .content(asJsonString(USER_TO_CREATE))
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
+
+        String id = objectMapper.readValue(response, UserDto.class).getId();
+
+        mockMvc.perform(post("/users/logout/{id}", id))
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    void getFinished() throws Exception {
+        mockMvc.perform(get("/users/finished/{id}", "1c1bbf66-6585-4978-886b-b126335ff3af"))
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    void getEnrolled() throws Exception {
+        mockMvc.perform(get("/users/enrolled/{id}", "1c1bbf66-6585-4978-886b-b126335ff3af"))
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    void addLanguage() throws Exception {
+        mockMvc.perform(put("/users/addLanguage/{id}", "1c1bbf66-6585-4978-886b-b126335ff3af")
+                        .content(asJsonString(new UserAddLanguageDto(LanguageTypeDto.ENGLISH, ProficiencyLevelDto.B2)))
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isOk());
+    }
+
+    private static String asJsonString(final Object obj) throws JsonProcessingException {
+        return new ObjectMapper().writeValueAsString(obj);
+    }
+}
\ No newline at end of file