Compare commits
5 Commits
3530e9e28d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
076b2320bc | ||
|
|
c5f0d5443d | ||
|
|
6d907a725d | ||
|
|
2f2eacbcef | ||
|
|
a217ef0363 |
79
README.md
79
README.md
@@ -0,0 +1,79 @@
|
|||||||
|
# Sweater — клон популярной социальной сети
|
||||||
|
|
||||||
|
Учебный проект, реализующий основные функции социальной сети: регистрация, авторизация, публикация сообщений, подписки, профиль пользователя и др. Разработан с использованием **Spring Boot**, **Spring Security**, **MySQL**, **Thymeleaf/Freemarker**, **Flyway**, **Testcontainers** и других технологий.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Функциональность
|
||||||
|
|
||||||
|
* **Сообщения**
|
||||||
|
|
||||||
|
* Добавление, просмотр, фильтрация
|
||||||
|
* Редактирование
|
||||||
|
* Пагинация с сокращённым списком ссылок
|
||||||
|
|
||||||
|
* **Пользователи**
|
||||||
|
|
||||||
|
* Регистрация с активацией по email
|
||||||
|
* Аутентификация (Spring Security)
|
||||||
|
* Авторизация по ролям (`USER`, `ADMIN`)
|
||||||
|
* Подписка/отписка от пользователей
|
||||||
|
* Просмотр списка подписчиков/подписок
|
||||||
|
* Редактирование профиля
|
||||||
|
* Хранение сессий в БД (`rememberMe`)
|
||||||
|
|
||||||
|
* **Безопасность**
|
||||||
|
|
||||||
|
* Spring Security + CustomUserDetailsService
|
||||||
|
* Шифрование паролей
|
||||||
|
* Валидация данных
|
||||||
|
* reCAPTCHA при регистрации
|
||||||
|
* Защита маршрутов по ролям (@EnableMethodSecurity)
|
||||||
|
|
||||||
|
* **Файлы и внешний вид**
|
||||||
|
|
||||||
|
* Загрузка изображений и других файлов
|
||||||
|
* Доступ к статическим ресурсам
|
||||||
|
* Bootstrap для стилизации форм и страниц
|
||||||
|
* Freemarker вместо Mustache
|
||||||
|
|
||||||
|
* **Инфраструктура и миграции**
|
||||||
|
|
||||||
|
* MySQL вместо PostgreSQL
|
||||||
|
* Flyway — миграции базы данных
|
||||||
|
* Перевод генерации ID на `IDENTITY`
|
||||||
|
|
||||||
|
* **Тестирование**
|
||||||
|
|
||||||
|
* Интеграционные тесты c Testcontainers
|
||||||
|
* Модульные тесты пользовательского сервиса
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Стек технологий
|
||||||
|
|
||||||
|
* **Backend**: Spring Boot, Spring MVC, Spring Data JPA, Spring Security
|
||||||
|
* **Frontend**: Freemarker, Bootstrap
|
||||||
|
* **Database**: MySQL
|
||||||
|
* **Migration**: Flyway
|
||||||
|
* **Тесты**: JUnit 5, Testcontainers
|
||||||
|
* **Прочее**: reCAPTCHA, email-activation, file upload
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Источники и вдохновение
|
||||||
|
|
||||||
|
- Проект реализован основываясь на видеокурсе Андрея Letscode, за что ему большое спасибо!
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как запустить
|
||||||
|
|
||||||
|
1. Установите и настройте **MySQL**
|
||||||
|
2. Укажите параметры подключения в `application.properties`
|
||||||
|
3. Запустите проект:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./mvnw spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Откройте в браузере: `http://localhost:8080`
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ package net.sytes.kashey.sweater.controller;
|
|||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import net.sytes.kashey.sweater.domain.Message;
|
import net.sytes.kashey.sweater.domain.Message;
|
||||||
|
import net.sytes.kashey.sweater.domain.User;
|
||||||
|
import net.sytes.kashey.sweater.domain.dto.MessageDto;
|
||||||
import net.sytes.kashey.sweater.repository.MessageRepository;
|
import net.sytes.kashey.sweater.repository.MessageRepository;
|
||||||
import net.sytes.kashey.sweater.security.CustomUserDetails;
|
import net.sytes.kashey.sweater.security.CustomUserDetails;
|
||||||
|
import net.sytes.kashey.sweater.service.MessageService;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
@@ -21,12 +28,15 @@ import java.util.Map;
|
|||||||
public class MainController {
|
public class MainController {
|
||||||
|
|
||||||
private final MessageRepository messageRepository;
|
private final MessageRepository messageRepository;
|
||||||
|
private final MessageService messageService;
|
||||||
|
|
||||||
@Value("${upload.path}")
|
@Value("${upload.path}")
|
||||||
private String uploadPath;
|
private String uploadPath;
|
||||||
|
|
||||||
public MainController(MessageRepository messageRepository) {
|
public MainController(MessageRepository messageRepository, MessageService messageService) {
|
||||||
this.messageRepository = messageRepository;
|
this.messageRepository = messageRepository;
|
||||||
|
this.messageService = messageService;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
@@ -35,14 +45,15 @@ public class MainController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/main")
|
@GetMapping("/main")
|
||||||
public String findAllMessages(@RequestParam(required = false, defaultValue = "") String tag, Model model) {
|
public String findAllMessages(@RequestParam(required = false, defaultValue = "") String tag,
|
||||||
|
Model model,
|
||||||
|
@PageableDefault(sort = {"id"}, direction = Sort.Direction.DESC) Pageable pageable,
|
||||||
|
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
|
||||||
|
|
||||||
if (tag == null || tag.isEmpty()) {
|
Page<MessageDto> page = messageService.getFilteredMessagePage(tag, pageable, customUserDetails);
|
||||||
model.addAttribute("messages", messageRepository.findAll());
|
model.addAttribute("page", page);
|
||||||
} else {
|
|
||||||
model.addAttribute("messages", messageRepository.findByTag(tag));
|
|
||||||
}
|
|
||||||
model.addAttribute("tag", tag);
|
model.addAttribute("tag", tag);
|
||||||
|
model.addAttribute("url", "/main");
|
||||||
return "main";
|
return "main";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,10 +62,12 @@ public class MainController {
|
|||||||
@Valid Message message,
|
@Valid Message message,
|
||||||
BindingResult bindingResult,
|
BindingResult bindingResult,
|
||||||
Model model,
|
Model model,
|
||||||
|
@PageableDefault(sort = {"id"}, direction = Sort.Direction.DESC) Pageable pageable,
|
||||||
@RequestParam("file") MultipartFile file
|
@RequestParam("file") MultipartFile file
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
|
|
||||||
message.setAuthor(customUserDetails.getUser());
|
User user = customUserDetails.getUser();
|
||||||
|
message.setAuthor(user);
|
||||||
|
|
||||||
if (bindingResult.hasErrors()) {
|
if (bindingResult.hasErrors()) {
|
||||||
Map<String, String> errorsMap = ControllerUtils.getErrors(bindingResult);
|
Map<String, String> errorsMap = ControllerUtils.getErrors(bindingResult);
|
||||||
@@ -65,7 +78,8 @@ public class MainController {
|
|||||||
messageRepository.save(message);
|
messageRepository.save(message);
|
||||||
model.addAttribute("message", null);
|
model.addAttribute("message", null);
|
||||||
}
|
}
|
||||||
model.addAttribute("messages", messageRepository.findAll());
|
model.addAttribute("page", messageRepository.findAll(pageable, user));
|
||||||
|
model.addAttribute("url", "/main");
|
||||||
model.addAttribute("tag", "");
|
model.addAttribute("tag", "");
|
||||||
return "main";
|
return "main";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package net.sytes.kashey.sweater.controller;
|
||||||
|
|
||||||
|
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import net.sytes.kashey.sweater.domain.Message;
|
||||||
|
import net.sytes.kashey.sweater.domain.User;
|
||||||
|
import net.sytes.kashey.sweater.domain.dto.MessageDto;
|
||||||
|
import net.sytes.kashey.sweater.repository.MessageRepository;
|
||||||
|
import net.sytes.kashey.sweater.security.CustomUserDetails;
|
||||||
|
import net.sytes.kashey.sweater.service.MessageService;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
import org.springframework.web.util.UriComponents;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/")
|
||||||
|
public class MessageController {
|
||||||
|
|
||||||
|
private final MessageRepository messageRepository;
|
||||||
|
private final MessageService messageService;
|
||||||
|
|
||||||
|
@Value("${upload.path}")
|
||||||
|
private String uploadPath;
|
||||||
|
|
||||||
|
public MessageController(MessageRepository messageRepository, MessageService messageService) {
|
||||||
|
|
||||||
|
this.messageRepository = messageRepository;
|
||||||
|
this.messageService = messageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/user-messages/{author}")
|
||||||
|
public String getUserMessages(
|
||||||
|
@PathVariable User author,
|
||||||
|
@RequestParam(required = false) Message message,
|
||||||
|
@AuthenticationPrincipal CustomUserDetails customUserDetails,
|
||||||
|
@PageableDefault(sort = {"id"}, direction = Sort.Direction.DESC) Pageable pageable,
|
||||||
|
Model model) {
|
||||||
|
|
||||||
|
User currentUser = customUserDetails.getUser();
|
||||||
|
model.addAttribute("isCurrentUser", author.equals(currentUser));
|
||||||
|
model.addAttribute("message", message);
|
||||||
|
model.addAttribute("userChannel", author);
|
||||||
|
model.addAttribute("subscribersCount", author.getSubscribers().size());
|
||||||
|
model.addAttribute("subscriptionsCount", author.getSubscriptions().size());
|
||||||
|
model.addAttribute("isSubscriber", author.getSubscribers().contains(currentUser));
|
||||||
|
model.addAttribute("url", "/user-messages/" + author.getId());
|
||||||
|
Page<MessageDto> page = messageService.getMessagesByUserPage(currentUser, pageable, author);
|
||||||
|
model.addAttribute("page", page);
|
||||||
|
|
||||||
|
return "user_messages";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/user-messages/{userId}")
|
||||||
|
public String getUserMessage(@AuthenticationPrincipal CustomUserDetails customUserDetails,
|
||||||
|
@PathVariable(name = "userId") Long userId,
|
||||||
|
@ModelAttribute Message message,
|
||||||
|
@RequestParam String text,
|
||||||
|
@RequestParam String tag,
|
||||||
|
@RequestParam("file") MultipartFile file
|
||||||
|
) throws IOException {
|
||||||
|
|
||||||
|
message.setAuthor(customUserDetails.getUser());
|
||||||
|
|
||||||
|
if (!ObjectUtils.isEmpty(text)) {
|
||||||
|
message.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ObjectUtils.isEmpty(tag)) {
|
||||||
|
message.setTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
ControllerUtils.saveFile(message, file, uploadPath);
|
||||||
|
messageRepository.save(message);
|
||||||
|
return "redirect:/user-messages/" + userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/messages/{message}/like")
|
||||||
|
@Transactional
|
||||||
|
public String like(@AuthenticationPrincipal CustomUserDetails customUserDetails,
|
||||||
|
@PathVariable Message message,
|
||||||
|
RedirectAttributes redirectAttributes,
|
||||||
|
@RequestHeader(required = false) String referer) {
|
||||||
|
|
||||||
|
User currentUser = customUserDetails.getUser();
|
||||||
|
Set<User> likes = message.getLikes();
|
||||||
|
|
||||||
|
if (likes.contains(currentUser)) {
|
||||||
|
likes.remove(currentUser);
|
||||||
|
} else {
|
||||||
|
likes.add(currentUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
UriComponents components = UriComponentsBuilder.fromHttpUrl(referer).build();
|
||||||
|
|
||||||
|
components.getQueryParams()
|
||||||
|
.entrySet()
|
||||||
|
.forEach(pair -> redirectAttributes.addAttribute(pair.getKey(), pair.getValue()));
|
||||||
|
|
||||||
|
return "redirect:" + components.getPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,7 +79,7 @@ public class UserController {
|
|||||||
if ("subscribers".equals(type)) {
|
if ("subscribers".equals(type)) {
|
||||||
model.addAttribute("users", user.getSubscribers());
|
model.addAttribute("users", user.getSubscribers());
|
||||||
} else if ("subscriptions".equals(type)) {
|
} else if ("subscriptions".equals(type)) {
|
||||||
model.addAttribute("users", user.getSubscribtions());
|
model.addAttribute("users", user.getSubscriptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
return "subscriptions";
|
return "subscriptions";
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
package net.sytes.kashey.sweater.controller;
|
|
||||||
|
|
||||||
|
|
||||||
import net.sytes.kashey.sweater.domain.Message;
|
|
||||||
import net.sytes.kashey.sweater.domain.User;
|
|
||||||
import net.sytes.kashey.sweater.repository.MessageRepository;
|
|
||||||
import net.sytes.kashey.sweater.security.CustomUserDetails;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.ui.Model;
|
|
||||||
import org.springframework.util.ObjectUtils;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
@RequestMapping("/")
|
|
||||||
public class UserMessagesController {
|
|
||||||
|
|
||||||
private final MessageRepository messageRepository;
|
|
||||||
|
|
||||||
@Value("${upload.path}")
|
|
||||||
private String uploadPath;
|
|
||||||
|
|
||||||
public UserMessagesController(MessageRepository messageRepository) {
|
|
||||||
|
|
||||||
this.messageRepository = messageRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/user-messages/{user}")
|
|
||||||
public String getUserMessages(@PathVariable User user,
|
|
||||||
@RequestParam(required = false) Message message,
|
|
||||||
@AuthenticationPrincipal CustomUserDetails customUserDetails,
|
|
||||||
Model model) {
|
|
||||||
|
|
||||||
User currentUser = customUserDetails.getUser();
|
|
||||||
model.addAttribute("isCurrentUser", user.equals(currentUser));
|
|
||||||
model.addAttribute("messages", user.getMessages());
|
|
||||||
model.addAttribute("message", message);
|
|
||||||
model.addAttribute("userChannel", user);
|
|
||||||
model.addAttribute("subscribersCount", user.getSubscribers().size());
|
|
||||||
model.addAttribute("subscriptionsCount", user.getSubscribtions().size());
|
|
||||||
model.addAttribute("isSubscriber", user.getSubscribers().contains(currentUser));
|
|
||||||
|
|
||||||
return "user_messages";
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/user-messages/{userId}")
|
|
||||||
public String getUserMessage(@AuthenticationPrincipal CustomUserDetails customUserDetails,
|
|
||||||
@PathVariable(name = "userId") Long userId,
|
|
||||||
@ModelAttribute Message message,
|
|
||||||
@RequestParam String text,
|
|
||||||
@RequestParam String tag,
|
|
||||||
@RequestParam("file") MultipartFile file
|
|
||||||
) throws IOException {
|
|
||||||
|
|
||||||
message.setAuthor(customUserDetails.getUser());
|
|
||||||
|
|
||||||
if (!ObjectUtils.isEmpty(text)) {
|
|
||||||
message.setText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ObjectUtils.isEmpty(tag)) {
|
|
||||||
message.setTag(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
ControllerUtils.saveFile(message, file, uploadPath);
|
|
||||||
messageRepository.save(message);
|
|
||||||
return "redirect:/user-messages/" + userId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,12 @@ package net.sytes.kashey.sweater.domain;
|
|||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import net.sytes.kashey.sweater.domain.util.MessageHelper;
|
||||||
import org.hibernate.validator.constraints.Length;
|
import org.hibernate.validator.constraints.Length;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
public class Message {
|
public class Message {
|
||||||
@Id
|
@Id
|
||||||
@@ -22,6 +26,14 @@ public class Message {
|
|||||||
@JoinColumn(name = "user_id")
|
@JoinColumn(name = "user_id")
|
||||||
private User author;
|
private User author;
|
||||||
|
|
||||||
|
@ManyToMany
|
||||||
|
@JoinTable(
|
||||||
|
name = "message_likes",
|
||||||
|
joinColumns = {@JoinColumn(name = "message_id")},
|
||||||
|
inverseJoinColumns = {@JoinColumn(name = "user_id")}
|
||||||
|
)
|
||||||
|
private Set<User> likes = new HashSet<>();
|
||||||
|
|
||||||
public Message() {
|
public Message() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +76,8 @@ public class Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getAuthorName() {
|
public String getAuthorName() {
|
||||||
return author == null ? "<none>" : author.getUsername();
|
|
||||||
|
return MessageHelper.getAuthorName(author);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFilename() {
|
public String getFilename() {
|
||||||
@@ -74,4 +87,12 @@ public class Message {
|
|||||||
public void setFilename(String filename) {
|
public void setFilename(String filename) {
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<User> getLikes() {
|
||||||
|
return likes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLikes(Set<User> likes) {
|
||||||
|
this.likes = likes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,11 +145,11 @@ public class User implements Serializable {
|
|||||||
this.subscribers = subscribers;
|
this.subscribers = subscribers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<User> getSubscribtions() {
|
public Set<User> getSubscriptions() {
|
||||||
return subscribtions;
|
return subscribtions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSubscribtions(Set<User> subscribtions) {
|
public void setSubscriptions(Set<User> subscriptions) {
|
||||||
this.subscribtions = subscribtions;
|
this.subscribtions = subscriptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package net.sytes.kashey.sweater.domain.dto;
|
||||||
|
|
||||||
|
import net.sytes.kashey.sweater.domain.Message;
|
||||||
|
import net.sytes.kashey.sweater.domain.User;
|
||||||
|
import net.sytes.kashey.sweater.domain.util.MessageHelper;
|
||||||
|
|
||||||
|
public class MessageDto {
|
||||||
|
private final Long id;
|
||||||
|
private final String text;
|
||||||
|
private final String tag;
|
||||||
|
private final User author;
|
||||||
|
private final String filename;
|
||||||
|
private final Long likes;
|
||||||
|
private final Boolean meLiked;
|
||||||
|
|
||||||
|
public MessageDto(Message message, Long likes, Boolean meLiked) {
|
||||||
|
this.id = message.getId();
|
||||||
|
this.text = message.getText();
|
||||||
|
this.tag = message.getTag();
|
||||||
|
this.author = message.getAuthor();
|
||||||
|
this.filename = message.getFilename();
|
||||||
|
this.likes = likes;
|
||||||
|
this.meLiked = meLiked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthorName() {
|
||||||
|
return MessageHelper.getAuthorName(author);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTag() {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getAuthor() {
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getFilename() {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getLikes() {
|
||||||
|
return likes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getMeLiked() {
|
||||||
|
return meLiked;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "MessageDto{" +
|
||||||
|
"id=" + id +
|
||||||
|
", author=" + author +
|
||||||
|
", likes=" + likes +
|
||||||
|
", meLiked=" + meLiked +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package net.sytes.kashey.sweater.domain.util;
|
||||||
|
|
||||||
|
import net.sytes.kashey.sweater.domain.User;
|
||||||
|
|
||||||
|
public abstract class MessageHelper {
|
||||||
|
public static String getAuthorName(User author) {
|
||||||
|
return author == null ? "<none>" : author.getUsername();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,45 @@
|
|||||||
package net.sytes.kashey.sweater.repository;
|
package net.sytes.kashey.sweater.repository;
|
||||||
|
|
||||||
import net.sytes.kashey.sweater.domain.Message;
|
import net.sytes.kashey.sweater.domain.Message;
|
||||||
|
import net.sytes.kashey.sweater.domain.User;
|
||||||
|
import net.sytes.kashey.sweater.domain.dto.MessageDto;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.CrudRepository;
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface MessageRepository extends CrudRepository<Message, Integer> {
|
public interface MessageRepository extends CrudRepository<Message, Integer> {
|
||||||
List<Message> findByTag(String tag);
|
|
||||||
|
@Query("select new net.sytes.kashey.sweater.domain.dto.MessageDto(\n" +
|
||||||
|
" m, \n" +
|
||||||
|
" count(ml), \n" +
|
||||||
|
" sum(case when ml = :user then 1 else 0 end) > 0\n" +
|
||||||
|
") \n" +
|
||||||
|
"from Message as m " +
|
||||||
|
"left join m.likes as ml \n" +
|
||||||
|
"where m.tag = :tag \n" +
|
||||||
|
"group by m")
|
||||||
|
Page<MessageDto> findByTag(@Param("tag") String tag, Pageable pageable, @Param("user") User user);
|
||||||
|
|
||||||
|
@Query("select new net.sytes.kashey.sweater.domain.dto.MessageDto(\n" +
|
||||||
|
" m, \n" +
|
||||||
|
" count(ml), \n" +
|
||||||
|
" sum(case when ml = :user then 1 else 0 end) > 0\n" +
|
||||||
|
") \n" +
|
||||||
|
"from Message as m \n" +
|
||||||
|
"left join m.likes as ml \n" +
|
||||||
|
"group by m")
|
||||||
|
Page<MessageDto> findAll(Pageable pageable, @Param("user") User user);
|
||||||
|
|
||||||
|
@Query("select new net.sytes.kashey.sweater.domain.dto.MessageDto(\n" +
|
||||||
|
" m, \n" +
|
||||||
|
" count(ml), \n" +
|
||||||
|
" sum(case when ml = :user then 1 else 0 end) > 0\n" +
|
||||||
|
") \n" +
|
||||||
|
"from Message as m " +
|
||||||
|
"left join m.likes as ml \n" +
|
||||||
|
"where m.author = :author\n" +
|
||||||
|
"group by m")
|
||||||
|
Page<MessageDto> findByUser(@Param("author") User author, Pageable pageable, @Param("user") User user);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.sytes.kashey.sweater.service;
|
||||||
|
|
||||||
|
import net.sytes.kashey.sweater.domain.User;
|
||||||
|
import net.sytes.kashey.sweater.domain.dto.MessageDto;
|
||||||
|
import net.sytes.kashey.sweater.repository.MessageRepository;
|
||||||
|
import net.sytes.kashey.sweater.security.CustomUserDetails;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MessageService {
|
||||||
|
|
||||||
|
private final MessageRepository messageRepository;
|
||||||
|
|
||||||
|
public MessageService(MessageRepository messageRepository) {
|
||||||
|
this.messageRepository = messageRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<MessageDto> getMessagesByUserPage(User user, Pageable pageable, User author) {
|
||||||
|
|
||||||
|
return messageRepository.findByUser(user, pageable, author);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<MessageDto> getFilteredMessagePage(String tag, Pageable pageable, CustomUserDetails customUserDetails) {
|
||||||
|
User user = customUserDetails.getUser();
|
||||||
|
if (tag == null || tag.isEmpty()) {
|
||||||
|
return messageRepository.findAll(pageable, user);
|
||||||
|
} else {
|
||||||
|
return messageRepository.findByTag(tag, pageable, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS message_likes
|
||||||
|
(
|
||||||
|
message_id BIGINT NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (message_id, user_id),
|
||||||
|
CONSTRAINT fk_message_likes_message FOREIGN KEY (message_id) REFERENCES message (id),
|
||||||
|
CONSTRAINT fk_message_likes_user FOREIGN KEY (user_id) REFERENCES usr (id)
|
||||||
|
) ENGINE = InnoDB;
|
||||||
@@ -3,9 +3,14 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Sweater</title>
|
<title>Sweater</title>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"/>
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css"
|
||||||
|
integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU"
|
||||||
|
crossorigin="anonymous"/>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css"
|
||||||
|
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
|
||||||
|
crossorigin="anonymous"/>
|
||||||
<script src="https://www.google.com/recaptcha/api.js" async="async" defer="defer"></script>
|
<script src="https://www.google.com/recaptcha/api.js" async="async" defer="defer"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
@@ -15,9 +20,15 @@
|
|||||||
<#nested>
|
<#nested>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
|
||||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.3/dist/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.3/dist/umd/popper.min.js"
|
||||||
|
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/js/bootstrap.min.js"
|
||||||
|
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,25 +1,39 @@
|
|||||||
<#include "security.ftl">
|
<#include "security.ftl">
|
||||||
|
<#import "pager.ftl" as p>
|
||||||
|
|
||||||
<div class="row row-cols-1 row-cols-md-3 g-4" id="message-list">
|
<@p.pager url page! />
|
||||||
<#list messages as message>
|
<div class="card-columns" id="message-list">
|
||||||
<div class="col">
|
<#list page.content as message>
|
||||||
<div class="card h-100" data-id="${message.id}"> <!-- h-100 делает карточки одинаковой высоты -->
|
<div class="card my-3" data-id="${message.id}">
|
||||||
<#if message.filename??>
|
<#if message.filename??>
|
||||||
<img src="/img/${message.filename}" class="card-img-top">
|
<img src="/img/${message.filename}" class="card-img-top"/>
|
||||||
</#if>
|
</#if>
|
||||||
<div class="card-body">
|
<div class="m-2">
|
||||||
<span>${message.text}</span><br/>
|
<span>${message.text}</span><br/>
|
||||||
<i>#${message.tag}</i>
|
<i>#${message.tag}</i>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer d-flex justify-content-between">
|
<div class="card-footer text-muted container">
|
||||||
<a href="/user-messages/${message.author.id}"> ${message.authorName} </a>
|
<div class="row">
|
||||||
|
<a class="col align-self-center"
|
||||||
|
href="/user-messages/${message.author.id}">${message.authorName}</a>
|
||||||
|
<a class="col align-self-center" href="/messages/${message.id}/like">
|
||||||
|
<#if message.meLiked>
|
||||||
|
<i class="fas fa-heart"></i>
|
||||||
|
<#else>
|
||||||
|
<i class="far fa-heart"></i>
|
||||||
|
</#if>
|
||||||
|
${message.likes}
|
||||||
|
</a>
|
||||||
<#if message.author.id == currentUserId>
|
<#if message.author.id == currentUserId>
|
||||||
<a class="btn btn-primary btn-sm" href="/user-messages/${message.author.id}?message=${message.id}">Edit</a>
|
<a class="col btn btn-primary" href="/user-messages/${message.author.id}?message=${message.id}">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
</#if>
|
</#if>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<#else>
|
<#else>
|
||||||
<p>No messages found</p>
|
No message
|
||||||
</#list>
|
</#list>
|
||||||
</div>
|
</div>
|
||||||
|
<@p.pager url page />
|
||||||
60
src/main/resources/templates/parts/pager.ftl
Normal file
60
src/main/resources/templates/parts/pager.ftl
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<#macro pager url page>
|
||||||
|
<#if page.getTotalPages() gt 7>
|
||||||
|
<#assign
|
||||||
|
totalPages = page.getTotalPages()
|
||||||
|
pageNumber = page.getNumber() + 1
|
||||||
|
|
||||||
|
head = (pageNumber > 4)?then([1, -1], [1, 2, 3])
|
||||||
|
tail = (pageNumber < totalPages - 3)?then([-1, totalPages], [totalPages - 2, totalPages - 1, totalPages])
|
||||||
|
bodyBefore = (pageNumber > 4 && pageNumber < totalPages - 1)?then([pageNumber - 2, pageNumber - 1], [])
|
||||||
|
bodyAfter = (pageNumber > 2 && pageNumber < totalPages - 3)?then([pageNumber + 1, pageNumber + 2], [])
|
||||||
|
|
||||||
|
body = head + bodyBefore + (pageNumber > 3 && pageNumber < totalPages - 2)?then([pageNumber], []) + bodyAfter + tail
|
||||||
|
>
|
||||||
|
<#else>
|
||||||
|
<#assign body = 1..page.getTotalPages()>
|
||||||
|
</#if>
|
||||||
|
<div class="container mt-3">
|
||||||
|
<div class="row" >
|
||||||
|
<ul class="pagination col justify-content-center">
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" tabindex="-1">Pages</a>
|
||||||
|
</li>
|
||||||
|
<#list body as p>
|
||||||
|
<#if (p - 1) == page.getNumber()>
|
||||||
|
<li class="page-item active">
|
||||||
|
<a class="page-link" href="#" tabindex="-1">${p}</a>
|
||||||
|
</li>
|
||||||
|
<#elseif p == -1>
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" tabindex="-1">...</a>
|
||||||
|
</li>
|
||||||
|
<#else>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="${url}?page=${p - 1}&size=${page.getSize()}"
|
||||||
|
tabindex="-1">${p}</a>
|
||||||
|
</li>
|
||||||
|
</#if>
|
||||||
|
</#list>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="pagination col justify-content-center" >
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" tabindex="-1">Items per page</a>
|
||||||
|
</li>
|
||||||
|
<#list [5, 10, 25, 50] as c>
|
||||||
|
<#if c == page.getSize()>
|
||||||
|
<li class="page-item active">
|
||||||
|
<a class="page-link" href="#" tabindex="-1">${c}</a>
|
||||||
|
</li>
|
||||||
|
<#else>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="${url}?page=${page.getNumber()}&size=${c}"
|
||||||
|
tabindex="-1">${c}</a>
|
||||||
|
</li>
|
||||||
|
</#if>
|
||||||
|
</#list>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</#macro>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package net.sytes.kashey.sweater;
|
package net.sytes.kashey.sweater.controller;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package net.sytes.kashey.sweater;
|
package net.sytes.kashey.sweater.controller;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -79,8 +79,8 @@ public class MainControllerTest {
|
|||||||
.andDo(print())
|
.andDo(print())
|
||||||
.andExpect(authenticated())
|
.andExpect(authenticated())
|
||||||
.andExpect(xpath("//*[@id='message-list']/div").nodeCount(2))
|
.andExpect(xpath("//*[@id='message-list']/div").nodeCount(2))
|
||||||
.andExpect(xpath("//*[@id='message-list']/div/div[@data-id='1']").exists())
|
.andExpect(xpath("/html/body/div/div[4]/div[@data-id='1']").exists())
|
||||||
.andExpect(xpath("//*[@id='message-list']/div/div[@data-id='3']").exists());
|
.andExpect(xpath("/html/body/div/div[4]/div[@data-id='3']").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -95,8 +95,8 @@ public class MainControllerTest {
|
|||||||
.andDo(print())
|
.andDo(print())
|
||||||
.andExpect(authenticated())
|
.andExpect(authenticated())
|
||||||
.andExpect(xpath("//*[@id='message-list']/div").nodeCount(5))
|
.andExpect(xpath("//*[@id='message-list']/div").nodeCount(5))
|
||||||
.andExpect(xpath("//*[@id='message-list']/div/div[@data-id='5']").exists())
|
.andExpect(xpath("/html/body/div/div[4]/div[@data-id='5']").exists())
|
||||||
.andExpect(xpath("//*[@id='message-list']/div/div[@data-id='5']/div/span").string("fifth"))
|
.andExpect(xpath("/html/body/div/div[4]/div[@data-id='5']/div/span").string("fifth"))
|
||||||
.andExpect(xpath("//*[@id='message-list']/div/div[@data-id='5']/div/i").string("#new one"));
|
.andExpect(xpath("/html/body/div/div[4]/div[@data-id='5']/div/i").string("#new one"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package net.sytes.kashey.sweater.service;
|
||||||
|
|
||||||
|
import net.sytes.kashey.sweater.domain.Role;
|
||||||
|
import net.sytes.kashey.sweater.domain.User;
|
||||||
|
import net.sytes.kashey.sweater.repository.UserRepository;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CustomUserDetailsServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
UserRepository userRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
EmailService emailService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
CustomUserDetailsService customUserDetailsService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldAddUserWhenNewUserProvided() {
|
||||||
|
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername("Dummy");
|
||||||
|
user.setEmail("test@gmail.com");
|
||||||
|
|
||||||
|
String isUserAdded = customUserDetailsService.addUser(user);
|
||||||
|
|
||||||
|
assertThat(isUserAdded).isEmpty();
|
||||||
|
assertThat(user.getActivationCode()).isNotNull();
|
||||||
|
assertThat(user.getRoles()).contains(Role.USER);
|
||||||
|
verify(userRepository, times(1))
|
||||||
|
.save(user);
|
||||||
|
verify(emailService, times(1))
|
||||||
|
.send(eq(user.getEmail()), anyString(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotAddUserWhenUsernameIsEmpty() {
|
||||||
|
|
||||||
|
User user = new User();
|
||||||
|
|
||||||
|
String isUserAdded = customUserDetailsService.addUser(user);
|
||||||
|
|
||||||
|
assertThat(isUserAdded).contains("User name must not be empty!");
|
||||||
|
verify(userRepository, times(0))
|
||||||
|
.save(any(User.class));
|
||||||
|
verify(emailService, times(0))
|
||||||
|
.send(anyString(), anyString(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotAddUserWhenUserAlreadyExists() {
|
||||||
|
|
||||||
|
User user = new User("Dummy");
|
||||||
|
when(userRepository.findByUsername(user.getUsername()))
|
||||||
|
.thenReturn(Optional.of(user));
|
||||||
|
|
||||||
|
String isUserAdded = customUserDetailsService.addUser(user);
|
||||||
|
|
||||||
|
assertThat(isUserAdded).contains("User already exists!");
|
||||||
|
verify(userRepository, times(0))
|
||||||
|
.save(user);
|
||||||
|
verify(emailService, times(0))
|
||||||
|
.send(anyString(), anyString(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldActivateUserWhenValidActivationCodeProvided() {
|
||||||
|
|
||||||
|
User user = new User();
|
||||||
|
user.setActivationCode("dummy_activation_code");
|
||||||
|
when(userRepository.findByActivationCode(user.getActivationCode()))
|
||||||
|
.thenReturn(user);
|
||||||
|
|
||||||
|
boolean isUserActivated = customUserDetailsService.activateUser(user.getActivationCode());
|
||||||
|
|
||||||
|
assertThat(isUserActivated).isTrue();
|
||||||
|
assertThat(user.getActivationCode()).isNull();
|
||||||
|
verify(userRepository, times(1))
|
||||||
|
.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotActivateUserWhenInvalidActivationCodeProvided() {
|
||||||
|
|
||||||
|
boolean isUserActivated = customUserDetailsService.activateUser("dummy_activation_code");
|
||||||
|
|
||||||
|
assertThat(isUserActivated).isFalse();
|
||||||
|
verify(userRepository, times(0))
|
||||||
|
.save(any(User.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUpdateUsernameAndRolesWhenValidDataProvided() {
|
||||||
|
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername("oldUser");
|
||||||
|
user.setRoles(new HashSet<>(Collections.singleton(Role.USER)));
|
||||||
|
|
||||||
|
Map<String, String> form = new HashMap<>();
|
||||||
|
form.put("ADMIN", "true");
|
||||||
|
form.put("USER", "true");
|
||||||
|
|
||||||
|
customUserDetailsService.updateUser("newUser", form, user);
|
||||||
|
|
||||||
|
assertThat(user.getUsername()).isEqualTo("newUser");
|
||||||
|
assertThat(user.getRoles()).containsExactlyInAnyOrder(Role.ADMIN, Role.USER);
|
||||||
|
verify(userRepository, times(1)).save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUpdateEmailAndGenerateActivationCodeWhenEmailChanged() {
|
||||||
|
|
||||||
|
User user = new User();
|
||||||
|
user.setEmail("old@gmail.com");
|
||||||
|
|
||||||
|
customUserDetailsService.updateProfile(user, null, "new@gmail.com");
|
||||||
|
|
||||||
|
assertThat(user.getEmail()).isEqualTo("new@gmail.com");
|
||||||
|
assertThat(user.getActivationCode()).isNotNull();
|
||||||
|
verify(userRepository, times(1)).save(user);
|
||||||
|
verify(emailService, times(1)).send(anyString(), anyString(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUpdatePasswordWhenProvided() {
|
||||||
|
|
||||||
|
User user = new User();
|
||||||
|
user.setPassword("oldPassword");
|
||||||
|
|
||||||
|
customUserDetailsService.updateProfile(user, "newPassword", null);
|
||||||
|
|
||||||
|
assertThat(user.getPassword()).isEqualTo("newPassword");
|
||||||
|
verify(userRepository, times(1)).save(user);
|
||||||
|
verify(emailService, times(0)).send(anyString(), anyString(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUpdateBothEmailAndPasswordWhenBothProvided() {
|
||||||
|
|
||||||
|
User user = new User();
|
||||||
|
user.setEmail("old@gmail.com");
|
||||||
|
user.setPassword("oldPassword");
|
||||||
|
|
||||||
|
customUserDetailsService.updateProfile(user, "newPassword", "new@gmail.com");
|
||||||
|
|
||||||
|
assertThat(user.getEmail()).isEqualTo("new@gmail.com");
|
||||||
|
assertThat(user.getPassword()).isEqualTo("newPassword");
|
||||||
|
assertThat(user.getActivationCode()).isNotNull();
|
||||||
|
verify(userRepository, times(1)).save(user);
|
||||||
|
verify(emailService, times(1)).send(anyString(), anyString(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnAllUsers() {
|
||||||
|
|
||||||
|
User user1 = new User("Dummy1");
|
||||||
|
User user2 = new User("Dummy2");
|
||||||
|
List<User> users = Arrays.asList(user1, user2);
|
||||||
|
when(userRepository.findAll()).thenReturn(users);
|
||||||
|
|
||||||
|
List<User> result = customUserDetailsService.findAll();
|
||||||
|
|
||||||
|
assertThat(result).hasSize(2);
|
||||||
|
assertThat(result).contains(user1, user2);
|
||||||
|
verify(userRepository, times(1)).findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnEmptyListWhenNoUsersFound() {
|
||||||
|
|
||||||
|
when(userRepository.findAll()).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
List<User> result = customUserDetailsService.findAll();
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
verify(userRepository, times(1)).findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldSubscribeUser() {
|
||||||
|
|
||||||
|
User subscriber = new User("Subscriber");
|
||||||
|
User userChannel = new User("UserChannel");
|
||||||
|
|
||||||
|
customUserDetailsService.subscribe(subscriber, userChannel);
|
||||||
|
|
||||||
|
assertThat(userChannel.getSubscribers()).contains(subscriber);
|
||||||
|
verify(userRepository, times(1)).save(userChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUnsubscribeUser() {
|
||||||
|
|
||||||
|
User subscriber = new User("Subscriber");
|
||||||
|
User userChannel = new User("UserChannel");
|
||||||
|
userChannel.getSubscribers().add(subscriber);
|
||||||
|
|
||||||
|
customUserDetailsService.unsubscribe(subscriber, userChannel);
|
||||||
|
|
||||||
|
assertThat(userChannel.getSubscribers()).doesNotContain(subscriber);
|
||||||
|
verify(userRepository, times(1)).save(userChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotFailUnsubscribeIfUserIsNotSubscribed() {
|
||||||
|
|
||||||
|
User subscriber = new User("Subscriber");
|
||||||
|
User userChannel = new User("UserChannel");
|
||||||
|
|
||||||
|
customUserDetailsService.unsubscribe(subscriber, userChannel);
|
||||||
|
|
||||||
|
assertThat(userChannel.getSubscribers()).isEmpty();
|
||||||
|
verify(userRepository, times(1)).save(userChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
DROP TABLE IF EXISTS message_likes;
|
||||||
DROP TABLE IF EXISTS message;
|
DROP TABLE IF EXISTS message;
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS message
|
CREATE TABLE IF NOT EXISTS message
|
||||||
(
|
(
|
||||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -26,6 +28,15 @@ CREATE TABLE IF NOT EXISTS usr
|
|||||||
primary key (id)
|
primary key (id)
|
||||||
) engine = InnoDB;
|
) engine = InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS message_likes
|
||||||
|
(
|
||||||
|
message_id BIGINT NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (message_id, user_id),
|
||||||
|
CONSTRAINT fk_message_likes_message FOREIGN KEY (message_id) REFERENCES message (id),
|
||||||
|
CONSTRAINT fk_message_likes_user FOREIGN KEY (user_id) REFERENCES usr (id)
|
||||||
|
) ENGINE = InnoDB;
|
||||||
|
|
||||||
DELETE
|
DELETE
|
||||||
FROM user_role;
|
FROM user_role;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user