diff --git a/docker-compose.yml b/docker-compose.yml index d3e93f6..b101d78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ services: - "8080:8080" depends_on: - main-db + - stats-server environment: - SPRING_DATASOURCE_URL=jdbc:postgresql://main-db:5432/maindb - SPRING_DATASOURCE_USERNAME=dbuser2 diff --git a/main/pom.xml b/main/pom.xml index f2f5f44..95f9b62 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -39,5 +39,48 @@ org.springframework.boot spring-boot-starter-actuator + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.postgresql + postgresql + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.h2database + h2 + runtime + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + \ No newline at end of file diff --git a/main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java b/main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java new file mode 100644 index 0000000..cf4dc0a --- /dev/null +++ b/main/src/main/java/ru/practicum/category/controller/CategoryAdminController.java @@ -0,0 +1,39 @@ +package ru.practicum.category.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.service.CategoryService; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/admin/categories") +public class CategoryAdminController { + + private final CategoryService categoryService; + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public CategoryDto createCategory(@Valid @RequestBody NewCategoryDto newCategoryDto) { + log.info("POST /admin/categories"); + return categoryService.createCategory(newCategoryDto); + } + + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("/{catId}") + public void deleteCategory(@PathVariable("catId") Long id) { + log.info("DELETE /admin/categories/{}", id); + categoryService.deleteCategory(id); + } + + @PatchMapping("/{catId}") + public CategoryDto updateCategory(@PathVariable("catId") Long id, @Valid @RequestBody NewCategoryDto newCategoryDto) { + log.info("PATCH /admin/categories/{}", id); + return categoryService.updateCategory(id, newCategoryDto); + } +} diff --git a/main/src/main/java/ru/practicum/category/controller/CategoryPublicController.java b/main/src/main/java/ru/practicum/category/controller/CategoryPublicController.java new file mode 100644 index 0000000..8ab7b2e --- /dev/null +++ b/main/src/main/java/ru/practicum/category/controller/CategoryPublicController.java @@ -0,0 +1,35 @@ +package ru.practicum.category.controller; + +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.service.CategoryService; + +import java.util.List; + +@Validated +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/categories") +public class CategoryPublicController { + + private final CategoryService categoryService; + + @GetMapping + public List getCategories(@RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + log.info("GET /categories?from={}&size={}", from, size); + return categoryService.getCategories(from, size); + } + + @GetMapping("/{catId}") + public CategoryDto getCategory(@PathVariable(value = "catId") Long id) { + log.info("GET /categories/{}", id); + return categoryService.getCategory(id); + } +} diff --git a/main/src/main/java/ru/practicum/category/dto/CategoryDto.java b/main/src/main/java/ru/practicum/category/dto/CategoryDto.java new file mode 100644 index 0000000..0423033 --- /dev/null +++ b/main/src/main/java/ru/practicum/category/dto/CategoryDto.java @@ -0,0 +1,15 @@ +package ru.practicum.category.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class CategoryDto { + private Long id; + private String name; +} diff --git a/main/src/main/java/ru/practicum/category/dto/NewCategoryDto.java b/main/src/main/java/ru/practicum/category/dto/NewCategoryDto.java new file mode 100644 index 0000000..5a768c8 --- /dev/null +++ b/main/src/main/java/ru/practicum/category/dto/NewCategoryDto.java @@ -0,0 +1,18 @@ +package ru.practicum.category.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class NewCategoryDto { + @NotBlank(message = "Название категории не может быть пустым") + @Size(min = 1, max = 50, message = "Название категории должно быть в границах от {min} до {max} символов") + private String name; +} diff --git a/main/src/main/java/ru/practicum/category/dto/mapper/CategoryMapper.java b/main/src/main/java/ru/practicum/category/dto/mapper/CategoryMapper.java new file mode 100644 index 0000000..d9b571c --- /dev/null +++ b/main/src/main/java/ru/practicum/category/dto/mapper/CategoryMapper.java @@ -0,0 +1,23 @@ +package ru.practicum.category.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.model.Category; + +@UtilityClass +public class CategoryMapper { + public static CategoryDto toCategoryDto(Category category) { + return new CategoryDto( + category.getId(), + category.getName() + ); + } + + public static Category toCategory(NewCategoryDto newCategoryDto) { + return new Category( + null, + newCategoryDto.getName() + ); + } +} diff --git a/main/src/main/java/ru/practicum/category/model/Category.java b/main/src/main/java/ru/practicum/category/model/Category.java new file mode 100644 index 0000000..9c4c844 --- /dev/null +++ b/main/src/main/java/ru/practicum/category/model/Category.java @@ -0,0 +1,34 @@ +package ru.practicum.category.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Objects; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Table(name = "categories") +@Entity +public class Category { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Category category)) return false; + return Objects.equals(id, category.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/main/src/main/java/ru/practicum/category/repository/CategoryRepository.java b/main/src/main/java/ru/practicum/category/repository/CategoryRepository.java new file mode 100644 index 0000000..5af656e --- /dev/null +++ b/main/src/main/java/ru/practicum/category/repository/CategoryRepository.java @@ -0,0 +1,12 @@ +package ru.practicum.category.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.category.model.Category; + +import java.util.Optional; + +public interface CategoryRepository extends JpaRepository { + boolean existsByName(String name); + + Optional findByName(String name); +} diff --git a/main/src/main/java/ru/practicum/category/service/CategoryService.java b/main/src/main/java/ru/practicum/category/service/CategoryService.java new file mode 100644 index 0000000..813ad36 --- /dev/null +++ b/main/src/main/java/ru/practicum/category/service/CategoryService.java @@ -0,0 +1,18 @@ +package ru.practicum.category.service; + +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; + +import java.util.List; + +public interface CategoryService { + CategoryDto createCategory(NewCategoryDto newCategoryDto); + + void deleteCategory(Long id); + + CategoryDto updateCategory(Long id, NewCategoryDto newCategoryDto); + + List getCategories(Integer from, Integer size); + + CategoryDto getCategory(Long id); +} diff --git a/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java b/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java new file mode 100644 index 0000000..059062e --- /dev/null +++ b/main/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java @@ -0,0 +1,97 @@ +package ru.practicum.category.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.dto.mapper.CategoryMapper; +import ru.practicum.category.model.Category; +import ru.practicum.category.repository.CategoryRepository; +import ru.practicum.event.repository.EventRepository; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; + +import java.util.List; +import java.util.stream.Collectors; + +@Transactional(readOnly = true) +@Slf4j +@RequiredArgsConstructor +@Service +public class CategoryServiceImpl implements CategoryService { + private final CategoryRepository categoryRepository; + private final EventRepository eventRepository; + + @Transactional + @Override + public CategoryDto createCategory(NewCategoryDto newCategoryDto) { + if (categoryRepository.existsByName(newCategoryDto.getName())) { + log.warn("Категория - {} уже существует", newCategoryDto.getName()); + throw new ConflictException("Категория - " + newCategoryDto.getName() + " уже существует"); + } + + return CategoryMapper.toCategoryDto(categoryRepository.save(CategoryMapper.toCategory(newCategoryDto))); + } + + @Transactional + @Override + public void deleteCategory(Long id) { + findCategoryById(id); + validateCategoryIsEmpty(id); + categoryRepository.deleteById(id); + } + + @Transactional + @Override + public CategoryDto updateCategory(Long id, NewCategoryDto newCategoryDto) { + Category category = findCategoryById(id); + + if (newCategoryDto.getName() != null && !category.getName().equals(newCategoryDto.getName())) { + categoryRepository.findByName(newCategoryDto.getName()) + .ifPresent(existing -> { + if (!existing.getId().equals(id)) { + log.warn("Категория с именем '{}' уже существует", newCategoryDto.getName()); + throw new ConflictException("Категория с именем '" + newCategoryDto.getName() + "' уже существует"); + } + }); + + category.setName(newCategoryDto.getName()); + } + + return CategoryMapper.toCategoryDto(categoryRepository.save(category)); + } + + @Override + public List getCategories(Integer from, Integer size) { + Pageable pageable = PageRequest.of(from / size, size, Sort.by("id").ascending()); + + return categoryRepository.findAll(pageable) + .stream() + .map(CategoryMapper::toCategoryDto) + .collect(Collectors.toList()); + } + + @Override + public CategoryDto getCategory(Long id) { + return CategoryMapper.toCategoryDto(findCategoryById(id)); + } + + private Category findCategoryById(Long categoryId) { + return categoryRepository.findById(categoryId) + .orElseThrow(() -> new NotFoundException("Категория с данным id: " + categoryId + " не найдена")); + } + + private void validateCategoryIsEmpty(Long categoryId) { + boolean isUsed = eventRepository.existsByCategoryId(categoryId); + + if (isUsed) { + log.warn("Категория с данным id: {} используется", categoryId); + throw new ConflictException("Категория используется"); + } + } +} diff --git a/main/src/main/java/ru/practicum/compilation/controller/CompilationAdminController.java b/main/src/main/java/ru/practicum/compilation/controller/CompilationAdminController.java new file mode 100644 index 0000000..42e3c46 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/controller/CompilationAdminController.java @@ -0,0 +1,40 @@ +package ru.practicum.compilation.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.UpdateCompilationRequest; +import ru.practicum.compilation.service.CompilationService; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/admin/compilations") +public class CompilationAdminController { + private final CompilationService compilationService; + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public CompilationDto createCompilation(@RequestBody @Valid NewCompilationDto newCompilationDto) { + log.info("POST /admin/compilations"); + return compilationService.createCompilation(newCompilationDto); + } + + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("/{compId}") + public void deleteCompilation(@PathVariable("compId") Long id) { + log.info("DELETE /admin/compilations"); + compilationService.deleteCompilation(id); + } + + @PatchMapping("/{compId}") + public CompilationDto updateCompilation(@PathVariable("compId") Long id, + @RequestBody(required = false) @Valid UpdateCompilationRequest updateCompilationRequest) { + log.info("PATCH /admin/compilations/{}", id); + return compilationService.updateCompilation(id, updateCompilationRequest); + } +} diff --git a/main/src/main/java/ru/practicum/compilation/controller/CompilationPublicController.java b/main/src/main/java/ru/practicum/compilation/controller/CompilationPublicController.java new file mode 100644 index 0000000..d46b6b8 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/controller/CompilationPublicController.java @@ -0,0 +1,35 @@ +package ru.practicum.compilation.controller; + +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.service.CompilationService; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/compilations") +public class CompilationPublicController { + private final CompilationService compilationService; + + @GetMapping + @ResponseStatus(HttpStatus.OK) + public List getCompilations(@RequestParam(required = false) Boolean pinned, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + log.info("GET /compilations"); + return compilationService.getCompilations(pinned, from, size); + } + + @GetMapping("/{compId}") + public CompilationDto getCompilationById(@PathVariable("compId") Long id) { + log.info("GET /compilations/{}", id); + return compilationService.getCompilationById(id); + } +} diff --git a/main/src/main/java/ru/practicum/compilation/dto/CompilationDto.java b/main/src/main/java/ru/practicum/compilation/dto/CompilationDto.java new file mode 100644 index 0000000..c4ab68d --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/dto/CompilationDto.java @@ -0,0 +1,20 @@ +package ru.practicum.compilation.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.event.dto.EventShortDto; + +import java.util.Set; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class CompilationDto { + private Long id; + private String title; + private Boolean pinned; + private Set events; +} diff --git a/main/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java b/main/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java new file mode 100644 index 0000000..7f6d1d1 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/dto/NewCompilationDto.java @@ -0,0 +1,24 @@ +package ru.practicum.compilation.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Set; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class NewCompilationDto { + private Set events; + + private Boolean pinned; + + @NotBlank(message = "Заголовок не может быть пустым") + @Size(min = 1, max = 50, message = "Заголовок должен быть в границах от {min} до {max} символов") + private String title; +} diff --git a/main/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java b/main/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java new file mode 100644 index 0000000..61ffca8 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/dto/UpdateCompilationRequest.java @@ -0,0 +1,22 @@ +package ru.practicum.compilation.dto; + +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Set; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class UpdateCompilationRequest { + private Set events; + + private Boolean pinned; + + @Size(min = 1, max = 50, message = "Заголовок должен быть в границах от {min} до {max} символов") + private String title; +} diff --git a/main/src/main/java/ru/practicum/compilation/dto/mapper/CompilationMapper.java b/main/src/main/java/ru/practicum/compilation/dto/mapper/CompilationMapper.java new file mode 100644 index 0000000..b39e8c7 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/dto/mapper/CompilationMapper.java @@ -0,0 +1,34 @@ +package ru.practicum.compilation.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.model.Compilation; +import ru.practicum.event.dto.mapper.EventMapper; + +import java.util.HashSet; +import java.util.stream.Collectors; + +@UtilityClass +public class CompilationMapper { + public static CompilationDto toCompilationDto(Compilation compilation) { + return new CompilationDto( + compilation.getId(), + compilation.getTitle(), + compilation.getPinned(), + compilation.getEvents() + .stream() + .map(EventMapper::toEventShortDto) + .collect(Collectors.toSet()) + ); + } + + public static Compilation toCompilation(NewCompilationDto newCompilationDto) { + return new Compilation( + null, + newCompilationDto.getTitle(), + newCompilationDto.getPinned() != null ? newCompilationDto.getPinned() : false, + new HashSet<>() + ); + } +} diff --git a/main/src/main/java/ru/practicum/compilation/model/Compilation.java b/main/src/main/java/ru/practicum/compilation/model/Compilation.java new file mode 100644 index 0000000..feac846 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/model/Compilation.java @@ -0,0 +1,50 @@ +package ru.practicum.compilation.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.event.model.Event; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "compilations") +public class Compilation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 50, nullable = false) + private String title; + + @Column(nullable = false) + private Boolean pinned = false; + + @ManyToMany + @JoinTable( + name = "compilations_events", + joinColumns = @JoinColumn(name = "compilation_id"), + inverseJoinColumns = @JoinColumn(name = "event_id") + ) + private Set events = new HashSet<>(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Compilation compilation)) return false; + return Objects.equals(id, compilation.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/main/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java b/main/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java new file mode 100644 index 0000000..d75872d --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/repository/CompilationRepository.java @@ -0,0 +1,10 @@ +package ru.practicum.compilation.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.compilation.model.Compilation; + +public interface CompilationRepository extends JpaRepository { + Page findAllByPinned(boolean pinned, Pageable pageable); +} diff --git a/main/src/main/java/ru/practicum/compilation/service/CompilationService.java b/main/src/main/java/ru/practicum/compilation/service/CompilationService.java new file mode 100644 index 0000000..641a966 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/service/CompilationService.java @@ -0,0 +1,19 @@ +package ru.practicum.compilation.service; + +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.UpdateCompilationRequest; + +import java.util.List; + +public interface CompilationService { + CompilationDto createCompilation(NewCompilationDto newCompilationDto); + + void deleteCompilation(Long id); + + CompilationDto updateCompilation(Long id, UpdateCompilationRequest updateCompilationRequest); + + List getCompilations(Boolean pinned, Integer from, Integer size); + + CompilationDto getCompilationById(Long id); +} diff --git a/main/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java b/main/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java new file mode 100644 index 0000000..f82c1a9 --- /dev/null +++ b/main/src/main/java/ru/practicum/compilation/service/CompilationServiceImpl.java @@ -0,0 +1,110 @@ +package ru.practicum.compilation.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.UpdateCompilationRequest; +import ru.practicum.compilation.dto.mapper.CompilationMapper; +import ru.practicum.compilation.model.Compilation; +import ru.practicum.compilation.repository.CompilationRepository; +import ru.practicum.event.model.Event; +import ru.practicum.event.repository.EventRepository; +import ru.practicum.exception.NotFoundException; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Transactional(readOnly = true) +@Slf4j +@RequiredArgsConstructor +@Service +public class CompilationServiceImpl implements CompilationService { + private final CompilationRepository compilationRepository; + private final EventRepository eventRepository; + + @Transactional + @Override + public CompilationDto createCompilation(NewCompilationDto newCompilationDto) { + Compilation compilation = CompilationMapper.toCompilation(newCompilationDto); + + if (newCompilationDto.getEvents() != null && !newCompilationDto.getEvents().isEmpty()) { + Set events = new HashSet<>(eventRepository.findAllById(newCompilationDto.getEvents())); + compilation.setEvents(events); + } + + return CompilationMapper.toCompilationDto(compilationRepository.save(compilation)); + } + + @Transactional + @Override + public void deleteCompilation(Long id) { + if (!compilationRepository.existsById(id)) { + log.warn("Подборка с id - {} не найдена", id); + throw new NotFoundException("Подборка с id - " + id + " не найдена"); + } + compilationRepository.deleteById(id); + } + + @Transactional + @Override + public CompilationDto updateCompilation(Long id, UpdateCompilationRequest updateCompilationRequest) { + Compilation compilation = validateCompilation(id); + + if (updateCompilationRequest == null) { + return CompilationMapper.toCompilationDto(compilationRepository.findById(id).get()); + } + + if (updateCompilationRequest.getEvents() != null) { + Set events = new HashSet<>( + eventRepository.findAllById(updateCompilationRequest.getEvents()) + ); + compilation.setEvents(events); + } + + if (updateCompilationRequest.getPinned() != null) { + compilation.setPinned(updateCompilationRequest.getPinned()); + } + + if (updateCompilationRequest.getTitle() != null) { + compilation.setTitle(updateCompilationRequest.getTitle()); + } + + return CompilationMapper.toCompilationDto(compilationRepository.save(compilation)); + } + + @Override + public List getCompilations(Boolean pinned, Integer from, Integer size) { + Pageable pageable = PageRequest.of(from / size, size); + + Page compilations; + if (pinned != null) { + compilations = compilationRepository.findAllByPinned(pinned, pageable); + } else { + compilations = compilationRepository.findAll(pageable); + } + + return compilations.stream() + .map(CompilationMapper::toCompilationDto) + .collect(Collectors.toList()); + } + + @Override + public CompilationDto getCompilationById(Long id) { + Compilation compilation = validateCompilation(id); + + return CompilationMapper.toCompilationDto(compilation); + } + + private Compilation validateCompilation(Long id) { + return compilationRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Подборка с id:" + id + " не найдена")); + } +} diff --git a/main/src/main/java/ru/practicum/error/ApiError.java b/main/src/main/java/ru/practicum/error/ApiError.java new file mode 100644 index 0000000..327ff47 --- /dev/null +++ b/main/src/main/java/ru/practicum/error/ApiError.java @@ -0,0 +1,19 @@ +package ru.practicum.error; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@AllArgsConstructor +@Getter +public class ApiError { + private final List errors; + private final String message; + private final String reason; + private final String status; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private final LocalDateTime timestamp; +} diff --git a/main/src/main/java/ru/practicum/error/ErrorHandler.java b/main/src/main/java/ru/practicum/error/ErrorHandler.java new file mode 100644 index 0000000..e7980c8 --- /dev/null +++ b/main/src/main/java/ru/practicum/error/ErrorHandler.java @@ -0,0 +1,103 @@ +package ru.practicum.error; + +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; +import ru.practicum.exception.ValidationException; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.stream.Collectors; + +@Slf4j +@RestControllerAdvice +public class ErrorHandler { + + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiError handleNotFoundException(NotFoundException e) { + return new ApiError( + Collections.emptyList(), + e.getMessage(), + "Not found.", + "NOT_FOUND", + LocalDateTime.now() + ); + } + + @ExceptionHandler({MethodArgumentNotValidException.class, + MissingServletRequestParameterException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(fe -> String.format("Field: %s. Error: %s. Value: %s", + fe.getField(), + fe.getDefaultMessage(), + fe.getRejectedValue() != null ? fe.getRejectedValue().toString() : "null")) + .collect(Collectors.joining("; ")); + + return new ApiError( + Collections.emptyList(), + message, + "Incorrectly made request.", + "BAD_REQUEST", + LocalDateTime.now() + ); + } + + @ExceptionHandler(ValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleValidationException(ValidationException e) { + return new ApiError( + Collections.emptyList(), + e.getMessage(), + "Incorrectly made request.", + "BAD_REQUEST", + LocalDateTime.now() + ); + } + + @ExceptionHandler({ConflictException.class}) + @ResponseStatus(HttpStatus.CONFLICT) + public ApiError handleConflictException(ConflictException e) { + return new ApiError( + Collections.emptyList(), + e.getMessage(), + "Integrity constraint has been violated.", + "CONFLICT", + LocalDateTime.now() + ); + } + + @ExceptionHandler({ConstraintViolationException.class}) + @ResponseStatus(HttpStatus.CONFLICT) + public ApiError handleConstraintViolationException(ConstraintViolationException e) { + return new ApiError( + Collections.emptyList(), + "В базе данных нарушено ограничение целостности.", + "Integrity constraint has been violated.", + "CONFLICT", + LocalDateTime.now() + ); + } + + @ExceptionHandler(Throwable.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiError handleThrowable(Throwable e) { + log.error("Неожиданная ошибка: ", e); + return new ApiError( + Collections.emptyList(), + "Произошла непредвиденная ошибка.", + "Internal server error.", + "INTERNAL_SERVER_ERROR", + LocalDateTime.now() + ); + } +} diff --git a/main/src/main/java/ru/practicum/event/controller/EventAdminController.java b/main/src/main/java/ru/practicum/event/controller/EventAdminController.java new file mode 100644 index 0000000..56ee807 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/controller/EventAdminController.java @@ -0,0 +1,44 @@ +package ru.practicum.event.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; +import ru.practicum.event.dto.EventFullDto; +import ru.practicum.event.dto.UpdateEventAdminRequest; +import ru.practicum.event.service.EventService; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/admin/events") +public class EventAdminController { + private final EventService eventService; + + @GetMapping + public List getAdminEvents( + @RequestParam(required = false) List users, + @RequestParam(required = false) List states, + @RequestParam(required = false) List categories, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeStart, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeEnd, + @RequestParam(defaultValue = "0") Integer from, + @RequestParam(defaultValue = "10") Integer size + ) { + log.info("GET /admin/events?users={}&states={}&categories={}&rangeStart={}&rangeEnd={}&from={}&size={}", + users, states, categories, rangeStart, rangeEnd, from, size); + + return eventService.getAdminEvents(users, states, categories, rangeStart, rangeEnd, from, size); + } + + @PatchMapping("/{eventId}") + public EventFullDto updateEventByAdmin(@PathVariable Long eventId, + @RequestBody @Valid UpdateEventAdminRequest updateEventAdminRequest) { + log.info("GET /admin/events/{}", eventId); + return eventService.updateEventByAdmin(eventId, updateEventAdminRequest); + } +} diff --git a/main/src/main/java/ru/practicum/event/controller/EventPrivateController.java b/main/src/main/java/ru/practicum/event/controller/EventPrivateController.java new file mode 100644 index 0000000..08487b2 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/controller/EventPrivateController.java @@ -0,0 +1,68 @@ +package ru.practicum.event.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.event.dto.*; +import ru.practicum.event.service.EventService; +import ru.practicum.request.dto.ParticipationRequestDto; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/users/{userId}/events") +public class EventPrivateController { + private final EventService eventService; + + @GetMapping + public List getEventsByUserId(@PathVariable("userId") Long userId, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + log.info("GET /users/{}/events?from={}&size={}", userId, from, size); + return eventService.getEventsByUserId(userId, from, size); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public EventFullDto createEvent(@PathVariable("userId") Long userId, + @Valid @RequestBody NewEventDto newEventDto) { + log.info("POST /users/{}/events", userId); + return eventService.createEvent(userId, newEventDto); + } + + @GetMapping("/{eventId}") + public EventFullDto getEventByUserIdAndEventId(@PathVariable("userId") Long userId, + @PathVariable("eventId") Long eventId) { + log.info("GET /users/{}/events/{}", userId, eventId); + return eventService.getEventByUserIdAndEventId(userId, eventId); + } + + @PatchMapping("/{eventId}") + public EventFullDto updateEventByUserIdAndEventId(@PathVariable("userId") Long userId, + @PathVariable("eventId") Long eventId, + @RequestBody @Valid UpdateEventUserRequest updateEventUserRequest) { + log.info("PATCH /users/{}/events/{}", userId, eventId); + return eventService.updateEventByUserIdAndEventId(userId, eventId, updateEventUserRequest); + } + + @GetMapping("/{eventId}/requests") + public List getEventRequestsByUserIdAndEventId(@PathVariable(value = "userId") Long userId, + @PathVariable(value = "eventId") Long eventId) { + log.info("GET /users/{}/events/{}/requests", userId, eventId); + return eventService.getEventRequestsByUserIdAndEventId(userId, eventId); + } + + @PatchMapping("/{eventId}/requests") + public EventRequestStatusUpdateResult changeStatus(@PathVariable final Long userId, + @PathVariable final Long eventId, + @RequestBody @Valid final EventRequestStatusUpdateRequest eventRequestStatusUpdateRequest) { + log.info("PATCH /users/{}/events/{}/requests", userId, eventId); + return eventService.changeStatus(userId, eventId, eventRequestStatusUpdateRequest); + } +} diff --git a/main/src/main/java/ru/practicum/event/controller/EventPublicController.java b/main/src/main/java/ru/practicum/event/controller/EventPublicController.java new file mode 100644 index 0000000..8c0718d --- /dev/null +++ b/main/src/main/java/ru/practicum/event/controller/EventPublicController.java @@ -0,0 +1,45 @@ +package ru.practicum.event.controller; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; +import ru.practicum.event.dto.EventFullDto; +import ru.practicum.event.dto.EventShortDto; +import ru.practicum.event.service.EventService; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/events") +public class EventPublicController { + private final EventService eventService; + + @GetMapping + public List getEvents( + @RequestParam(required = false) String text, + @RequestParam(required = false) List categories, + @RequestParam(required = false) Boolean paid, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeStart, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime rangeEnd, + @RequestParam(defaultValue = "false") Boolean onlyAvailable, + @RequestParam(required = false) String sort, + @RequestParam(defaultValue = "0") Integer from, + @RequestParam(defaultValue = "10") Integer size, + HttpServletRequest request) { + log.info("GET /events?text={}&categories={}&paid={}&rangeStart={}&rangeEnd={}&onlyAvailable={}&sort={}&from={}&size={}", + text, categories, paid, rangeStart, rangeEnd, onlyAvailable, sort, from, size); + + return eventService.getEvents(text, categories, paid, rangeStart, rangeEnd, onlyAvailable, sort, from, size, request); + } + + @GetMapping("/{id}") + public EventFullDto getEventById(@PathVariable Long id, HttpServletRequest request) { + log.info("GET /events/{}", id); + return eventService.getEventById(id, request); + } +} diff --git a/main/src/main/java/ru/practicum/event/dto/EventFullDto.java b/main/src/main/java/ru/practicum/event/dto/EventFullDto.java new file mode 100644 index 0000000..32dda75 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/EventFullDto.java @@ -0,0 +1,54 @@ +package ru.practicum.event.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.event.state.EventState; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.user.dto.UserShortDto; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class EventFullDto { + private Long id; + + private String annotation; + + private CategoryDto category; + + private Long confirmedRequests; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdOn; + + private String description; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventDate; + + private UserShortDto initiator; + + private LocationDto location; + + private Boolean paid; + + private Integer participantLimit; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime publishedOn; + + private Boolean requestModeration; + + private EventState state; + + private String title; + + private Long views; +} diff --git a/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java new file mode 100644 index 0000000..b81139a --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateRequest.java @@ -0,0 +1,21 @@ +package ru.practicum.event.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.request.status.RequestStatus; + +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class EventRequestStatusUpdateRequest { + @NotNull(message = "Идентификаторы запросов обязательны") + List requestIds; + @NotNull(message = "Статус обязателен") + RequestStatus status; +} diff --git a/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateResult.java b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateResult.java new file mode 100644 index 0000000..9168bd1 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/EventRequestStatusUpdateResult.java @@ -0,0 +1,18 @@ +package ru.practicum.event.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.request.dto.ParticipationRequestDto; + +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class EventRequestStatusUpdateResult { + List confirmedRequests; + List rejectedRequests; +} diff --git a/main/src/main/java/ru/practicum/event/dto/EventShortDto.java b/main/src/main/java/ru/practicum/event/dto/EventShortDto.java new file mode 100644 index 0000000..1efd876 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/EventShortDto.java @@ -0,0 +1,36 @@ +package ru.practicum.event.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.user.dto.UserShortDto; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class EventShortDto { + private Long id; + + private String annotation; + + private CategoryDto category; + + private Long confirmedRequests; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventDate; + + private UserShortDto initiator; + + private Boolean paid; + + private String title; + + private Long views; +} diff --git a/main/src/main/java/ru/practicum/event/dto/NewEventDto.java b/main/src/main/java/ru/practicum/event/dto/NewEventDto.java new file mode 100644 index 0000000..ca11e49 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/NewEventDto.java @@ -0,0 +1,49 @@ +package ru.practicum.event.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.location.dto.LocationDto; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class NewEventDto { + @NotBlank(message = "Аннотация не может быть пустой") + @Size(min = 20, max = 2000, message = "Поле Аннотация должно быть в границах от {min} до {max} символов") + private String annotation; + + @NotNull(message = "Категория обязательна") + private Long category; + + @NotBlank(message = "Описание не может быть пустым") + @Size(min = 20, max = 7000, message = "Поле Описание должно быть в границах от {min} до {max} символов") + private String description; + + @NotNull(message = "Дата события обязательна") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventDate; + + @NotNull(message = "Локация обязательна") + private LocationDto location; + + private Boolean paid; + + @Min(value = 0, message = "Лимит участников не может быть отрицательным") + private Integer participantLimit; + + private Boolean requestModeration; + + @NotBlank(message = "Заголовок не может быть пустым") + @Size(min = 3, max = 120, message = "Поле Заголовок должно быть в границах от {min} до {max} символов") + private String title; +} diff --git a/main/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java b/main/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java new file mode 100644 index 0000000..42308f5 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/UpdateEventAdminRequest.java @@ -0,0 +1,42 @@ +package ru.practicum.event.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.event.state.EventStateActionSolution; +import ru.practicum.location.dto.LocationDto; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class UpdateEventAdminRequest { + @Size(min = 20, max = 2000, message = "Поле Аннотация должно быть в границах от {min} до {max} символов") + private String annotation; + + private Long category; + + @Size(min = 20, max = 7000, message = "Поле Описание должно быть в границах от {min} до {max} символов") + private String description; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventDate; + + private LocationDto location; + + private Boolean paid; + + private Integer participantLimit; + + private Boolean requestModeration; + + private EventStateActionSolution stateAction; + + @Size(min = 3, max = 120, message = "Поле Заголовок должно быть в границах от {min} до {max} символов") + private String title; +} diff --git a/main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java b/main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java new file mode 100644 index 0000000..3d25dd5 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/UpdateEventUserRequest.java @@ -0,0 +1,44 @@ +package ru.practicum.event.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.event.state.EventStateAction; +import ru.practicum.location.dto.LocationDto; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class UpdateEventUserRequest { + @Size(min = 20, max = 2000, message = "Поле Аннотация должно быть в границах от {min} до {max} символов") + private String annotation; + + private Long category; + + @Size(min = 20, max = 7000, message = "Поле Описание должно быть в границах от {min} до {max} символов") + private String description; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventDate; + + private LocationDto location; + + private Boolean paid; + + @Min(value = 0, message = "Лимит участников не может быть отрицательным") + private Integer participantLimit; + + private Boolean requestModeration; + + private EventStateAction stateAction; + + @Size(min = 3, max = 120, message = "Поле Заголовок должно быть в границах от {min} до {max} символов") + private String title; +} diff --git a/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java b/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java new file mode 100644 index 0000000..a482770 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/dto/mapper/EventMapper.java @@ -0,0 +1,73 @@ +package ru.practicum.event.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.category.dto.mapper.CategoryMapper; +import ru.practicum.category.model.Category; +import ru.practicum.event.dto.EventFullDto; +import ru.practicum.event.dto.EventShortDto; +import ru.practicum.event.dto.NewEventDto; +import ru.practicum.event.model.Event; +import ru.practicum.event.state.EventState; +import ru.practicum.location.dto.mapper.LocationMapper; +import ru.practicum.user.dto.mapper.UserMapper; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; + +@UtilityClass +public class EventMapper { + public static EventFullDto toEventFullDto(Event event) { + return new EventFullDto( + event.getId(), + event.getAnnotation(), + CategoryMapper.toCategoryDto(event.getCategory()), + event.getConfirmedRequests(), + event.getCreatedOn(), + event.getDescription(), + event.getEventDate(), + UserMapper.toUserShortDto(event.getInitiator()), + LocationMapper.toLocationDto(event.getLocation()), + event.getPaid(), + event.getParticipantLimit(), + event.getPublishedOn(), + event.getRequestModeration(), + event.getState(), + event.getTitle(), + null + ); + } + + public static Event toEvent(NewEventDto newEventDto, Category category, User initiator) { + return new Event( + null, + newEventDto.getAnnotation(), + category, + 0L, + LocalDateTime.now(), + newEventDto.getDescription(), + newEventDto.getEventDate(), + initiator, + LocationMapper.toLocation(newEventDto.getLocation()), + newEventDto.getPaid(), + newEventDto.getParticipantLimit(), + null, + newEventDto.getRequestModeration(), + EventState.PENDING, + newEventDto.getTitle() + ); + } + + public static EventShortDto toEventShortDto(Event event) { + return new EventShortDto( + event.getId(), + event.getAnnotation(), + CategoryMapper.toCategoryDto(event.getCategory()), + event.getConfirmedRequests(), + event.getEventDate(), + UserMapper.toUserShortDto(event.getInitiator()), + event.getPaid(), + event.getTitle(), + null + ); + } +} diff --git a/main/src/main/java/ru/practicum/event/model/Event.java b/main/src/main/java/ru/practicum/event/model/Event.java new file mode 100644 index 0000000..9507b2c --- /dev/null +++ b/main/src/main/java/ru/practicum/event/model/Event.java @@ -0,0 +1,84 @@ +package ru.practicum.event.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.category.model.Category; +import ru.practicum.event.state.EventState; +import ru.practicum.location.model.Location; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; +import java.util.Objects; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Table(name = "events") +@Entity +public class Event { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 2000) + private String annotation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id") + private Category category; + + @Column(name = "confirmed_requests") + private Long confirmedRequests; + + @Column(name = "created_on", nullable = false) + private LocalDateTime createdOn; + + @Column(length = 7000, nullable = false) + private String description; + + @Column(name = "event_date", nullable = false) + private LocalDateTime eventDate; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "initiator_id") + private User initiator; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "location_id") + private Location location; + + @Column(nullable = false) + private Boolean paid = false; + + @Column(name = "participant_limit", nullable = false) + private Integer participantLimit = 0; + + @Column(name = "published_on") + private LocalDateTime publishedOn; + + @Column(name = "request_moderation", nullable = false) + private Boolean requestModeration = true; + + @Enumerated(EnumType.STRING) + @Column(length = 50, nullable = false) + private EventState state; + + @Column(nullable = false, length = 120) + private String title; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Event event)) return false; + return Objects.equals(id, event.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/main/src/main/java/ru/practicum/event/repository/EventRepository.java b/main/src/main/java/ru/practicum/event/repository/EventRepository.java new file mode 100644 index 0000000..faae129 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/repository/EventRepository.java @@ -0,0 +1,20 @@ +package ru.practicum.event.repository; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import ru.practicum.event.model.Event; +import ru.practicum.event.state.EventState; + +import java.util.List; +import java.util.Optional; + +public interface EventRepository extends JpaRepository, JpaSpecificationExecutor { + List findByInitiatorId(Long userId, Pageable pageable); + + Optional findByInitiatorIdAndId(Long userId, Long eventId); + + Optional findByIdAndState(Long id, EventState eventState); + + Boolean existsByCategoryId(Long categoryId); +} diff --git a/main/src/main/java/ru/practicum/event/service/EventService.java b/main/src/main/java/ru/practicum/event/service/EventService.java new file mode 100644 index 0000000..ece6bb7 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/service/EventService.java @@ -0,0 +1,45 @@ +package ru.practicum.event.service; + +import jakarta.servlet.http.HttpServletRequest; +import ru.practicum.event.dto.*; +import ru.practicum.request.dto.ParticipationRequestDto; + +import java.time.LocalDateTime; +import java.util.List; + +public interface EventService { + EventFullDto createEvent(Long userId, NewEventDto newEventDto); + + List getEventsByUserId(Long userId, Integer from, Integer size); + + EventFullDto getEventByUserIdAndEventId(Long userId, Long eventId); + + EventFullDto updateEventByUserIdAndEventId(Long userId, Long eventId, UpdateEventUserRequest updateEventUserRequest); + + List getEventRequestsByUserIdAndEventId(Long userId, Long eventId); + + EventRequestStatusUpdateResult changeStatus(final Long userId, + final Long eventId, + final EventRequestStatusUpdateRequest eventRequestStatusUpdateRequest); + + List getEvents(String text, List categories, Boolean paid, LocalDateTime rangeStart, + LocalDateTime rangeEnd, + Boolean onlyAvailable, + String sort, + Integer from, + Integer size, + HttpServletRequest request); + + EventFullDto getEventById(Long id, HttpServletRequest request); + + List getAdminEvents(List users, + List states, + List categories, + LocalDateTime rangeStart, + LocalDateTime rangeEnd, + Integer from, + Integer size); + + EventFullDto updateEventByAdmin(Long eventId, + UpdateEventAdminRequest updateEventAdminRequest); +} diff --git a/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java b/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java new file mode 100644 index 0000000..fde6046 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/service/EventServiceImpl.java @@ -0,0 +1,428 @@ +package ru.practicum.event.service; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.StatsClient; +import ru.practicum.StatsRequestDto; +import ru.practicum.StatsResponseDto; +import ru.practicum.StatsViewRequestDto; +import ru.practicum.category.model.Category; +import ru.practicum.category.repository.CategoryRepository; +import ru.practicum.event.dto.*; +import ru.practicum.event.dto.mapper.EventMapper; +import ru.practicum.event.model.Event; +import ru.practicum.event.repository.EventRepository; +import ru.practicum.event.specification.EventSpecifications; +import ru.practicum.event.state.EventState; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; +import ru.practicum.exception.ValidationException; +import ru.practicum.location.dto.mapper.LocationMapper; +import ru.practicum.location.model.Location; +import ru.practicum.location.repository.LocationRepository; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.dto.mapper.RequestMapper; +import ru.practicum.request.model.Request; +import ru.practicum.request.repository.RequestRepository; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.model.User; +import ru.practicum.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Transactional(readOnly = true) +@Slf4j +@RequiredArgsConstructor +@Service +public class EventServiceImpl implements EventService { + private final EventRepository eventRepository; + private final UserRepository userRepository; + private final CategoryRepository categoryRepository; + private final RequestRepository requestRepository; + private final LocationRepository locationRepository; + private final StatsClient statsClient; + + + @Transactional + @Override + public EventFullDto createEvent(Long userId, NewEventDto newEventDto) { + User initiator = findUser(userId); + Category category = findCategory(newEventDto.getCategory()); + validateEventDate(newEventDto.getEventDate()); + + Event event = EventMapper.toEvent(newEventDto, category, initiator); + + if (event.getPaid() == null) { + event.setPaid(false); + } + if (event.getParticipantLimit() == null) { + event.setParticipantLimit(0); + } + if (event.getRequestModeration() == null) { + event.setRequestModeration(true); + } + if (event.getLocation() != null) { + Location savedLocation = locationRepository.save(event.getLocation()); + event.setLocation(savedLocation); + } + + return EventMapper.toEventFullDto(eventRepository.save(event)); + } + + @Override + public List getEventsByUserId(Long userId, Integer from, Integer size) { + findUser(userId); + + Pageable pageable = PageRequest.of(from / size, size, Sort.by("eventDate").ascending()); + + return eventRepository.findByInitiatorId(userId, pageable) + .stream() + .map(EventMapper::toEventShortDto) + .collect(Collectors.toList()); + } + + @Override + public EventFullDto getEventByUserIdAndEventId(Long userId, Long eventId) { + findUser(userId); + Event event = findEvent(userId, eventId); + + return EventMapper.toEventFullDto(event); + } + + @Transactional + @Override + public EventFullDto updateEventByUserIdAndEventId(Long userId, + Long eventId, + UpdateEventUserRequest updateEventUserRequest) { + findUser(userId); + Event event = findEvent(userId, eventId); + + if (!(event.getState().equals(EventState.PENDING) || event.getState().equals(EventState.CANCELED))) { + log.warn("Изменять можно только события в состоянии ожидания модерации или отмененные. Текущее состояние: {}", event.getState()); + throw new ConflictException("Изменять можно только события в состоянии ожидания модерации или отмененные"); + } + + if (updateEventUserRequest.getEventDate() != null) { + validateEventDate(updateEventUserRequest.getEventDate()); + event.setEventDate(updateEventUserRequest.getEventDate()); + } + + if (updateEventUserRequest.getAnnotation() != null) { + event.setAnnotation(updateEventUserRequest.getAnnotation()); + } + if (updateEventUserRequest.getCategory() != null) { + Category category = findCategory(updateEventUserRequest.getCategory()); + event.setCategory(category); + } + if (updateEventUserRequest.getDescription() != null) { + event.setDescription(updateEventUserRequest.getDescription()); + } + if (updateEventUserRequest.getPaid() != null) { + event.setPaid(updateEventUserRequest.getPaid()); + } + if (updateEventUserRequest.getParticipantLimit() != null) { + event.setParticipantLimit(updateEventUserRequest.getParticipantLimit()); + } + if (updateEventUserRequest.getTitle() != null) { + event.setTitle(updateEventUserRequest.getTitle()); + } + + if (updateEventUserRequest.getStateAction() != null) { + switch (updateEventUserRequest.getStateAction()) { + case SEND_TO_REVIEW -> event.setState(EventState.PENDING); + case CANCEL_REVIEW -> event.setState(EventState.CANCELED); + default -> throw new ConflictException("Недопустимое состояние события для обновления"); + } + } + + return EventMapper.toEventFullDto(eventRepository.save(event)); + } + + @Override + public List getEventRequestsByUserIdAndEventId(Long userId, Long eventId) { + findEvent(userId, eventId); + + List requests = requestRepository.findAllByEventId(eventId); + + return requests.stream() + .map(RequestMapper::toParticipationRequestDto) + .toList(); + } + + @Transactional + @Override + public EventRequestStatusUpdateResult changeStatus(Long userId, Long eventId, EventRequestStatusUpdateRequest eventRequestStatusUpdateRequest) { + Event event = findEvent(userId, eventId); + + if (event.getParticipantLimit() == 0 || Boolean.FALSE.equals(event.getRequestModeration())) { + List requests = requestRepository.findAllByIdIn(eventRequestStatusUpdateRequest.getRequestIds()); + List confirmed = requests.stream() + .map(RequestMapper::toParticipationRequestDto) + .toList(); + return new EventRequestStatusUpdateResult(confirmed, Collections.emptyList()); + } + + long confirmedCount = requestRepository.countByEventIdAndStatus(eventId, RequestStatus.CONFIRMED); + + List requestsToUpdate = requestRepository.findAllByIdIn(eventRequestStatusUpdateRequest.getRequestIds()); + + List confirmedRequests = new ArrayList<>(); + List rejectedRequests = new ArrayList<>(); + + for (Request r : requestsToUpdate) { + if (!r.getStatus().equals(RequestStatus.PENDING)) { + throw new ConflictException("Запрос должен иметь статус PENDING"); + } + + if (eventRequestStatusUpdateRequest.getStatus().equals(RequestStatus.CONFIRMED)) { + if (confirmedCount >= event.getParticipantLimit()) { + throw new ConflictException("Лимит участников достигнут"); + } + r.setStatus(RequestStatus.CONFIRMED); + confirmedCount++; + confirmedRequests.add(RequestMapper.toParticipationRequestDto(r)); + } else if (eventRequestStatusUpdateRequest.getStatus().equals(RequestStatus.REJECTED)) { + r.setStatus(RequestStatus.REJECTED); + rejectedRequests.add(RequestMapper.toParticipationRequestDto(r)); + } else { + throw new ConflictException("Неверный статус для обновления"); + } + + if (confirmedCount >= event.getParticipantLimit()) { + List pendingRequests = requestRepository.findAllByEventIdAndStatus(eventId, RequestStatus.PENDING); + for (Request pr : pendingRequests) { + pr.setStatus(RequestStatus.REJECTED); + rejectedRequests.add(RequestMapper.toParticipationRequestDto(pr)); + } + break; + } + } + requestRepository.saveAll(requestsToUpdate); + + return new EventRequestStatusUpdateResult(confirmedRequests, rejectedRequests); + } + + @Override + public List getEvents(String text, List categories, Boolean paid, LocalDateTime rangeStart, + LocalDateTime rangeEnd, + Boolean onlyAvailable, + String sort, + Integer from, + Integer size, + HttpServletRequest request) { + if (rangeStart != null && rangeEnd != null && rangeEnd.isBefore(rangeStart)) { + log.warn("rangeEnd не может быть меньше rangeStart"); + throw new ValidationException("rangeEnd не может быть меньше rangeStart"); + } + + Specification spec = Specification.where(EventSpecifications.published()) + .and(EventSpecifications.textContains(text)) + .and(EventSpecifications.categoryIn(categories)) + .and(EventSpecifications.paidIs(paid)) + .and(EventSpecifications.dateBetween(rangeStart, rangeEnd)) + .and(EventSpecifications.onlyAvailable(onlyAvailable)); + + Pageable pageable = PageRequest.of(from / size, size, sortBy(sort)); + + List events = eventRepository.findAll(spec, pageable).getContent(); + + statsClient.createHit(new StatsRequestDto( + "ewm", + request.getRequestURI(), + request.getRemoteAddr(), + LocalDateTime.now() + )); + + Map eventUriAndIdMap = events.stream() + .map(Event::getId) + .collect(Collectors.toMap(id -> "/events/" + id, Function.identity())); + + List stats = statsClient.getStats( + StatsViewRequestDto.builder() + .uris(new ArrayList<>(eventUriAndIdMap.keySet())) + .unique(true) + .build() + ); + + Map views = stats.stream() + .collect(Collectors.toMap(StatsResponseDto::getUri, + StatsResponseDto::getHits + )); + + return events.stream() + .map(event -> { + EventShortDto eventShortDto = EventMapper.toEventShortDto(event); + eventShortDto.setViews(views.getOrDefault("/events/" + event.getId(), 0L)); + return eventShortDto; + }) + .collect(Collectors.toList()); + } + + @Override + public EventFullDto getEventById(Long id, HttpServletRequest request) { + Event event = eventRepository.findByIdAndState(id, EventState.PUBLISHED) + .orElseThrow(() -> new NotFoundException("Опубликованное событие с id:" + id + " не найдено")); + + statsClient.createHit(new StatsRequestDto( + "ewm", + request.getRequestURI(), + request.getRemoteAddr(), + LocalDateTime.now() + )); + + Long views = statsClient.getStats( + StatsViewRequestDto.builder() + .uris(List.of("/events/" + id)) + .start(event.getPublishedOn()) + .end(LocalDateTime.now()) + .unique(true) + .build() + + ).stream() + .findAny() + .map(StatsResponseDto::getHits) + .orElse(0L); + + EventFullDto eventFullDto = EventMapper.toEventFullDto(event); + eventFullDto.setViews(views); + + return eventFullDto; + } + + @Override + public List getAdminEvents(List users, List states, + List categories, + LocalDateTime rangeStart, + LocalDateTime rangeEnd, + Integer from, + Integer size) { + + Specification spec = Specification.where(EventSpecifications.userIn(users)) + .and(EventSpecifications.stateIn(states)) + .and(EventSpecifications.categoryIn(categories)) + .and(EventSpecifications.dateBetween(rangeStart, rangeEnd)); + + Pageable pageable = PageRequest.of(from / size, size, Sort.by(Sort.Direction.ASC, "eventDate")); + + List events = eventRepository.findAll(spec, pageable).getContent(); + + events.forEach(e -> { + long confirmed = requestRepository.countByEventIdAndStatus(e.getId(), RequestStatus.CONFIRMED); + e.setConfirmedRequests(confirmed); + }); + + + return events.stream() + .map(e -> { + EventFullDto dto = EventMapper.toEventFullDto(e); + Long confirmed = requestRepository.countByEventIdAndStatus(e.getId(), RequestStatus.CONFIRMED); + dto.setConfirmedRequests(confirmed); + return dto; + }) + .collect(Collectors.toList()); + } + + @Transactional + @Override + public EventFullDto updateEventByAdmin(Long eventId, UpdateEventAdminRequest updateEventAdminRequest) { + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new NotFoundException("Событие с id:" + eventId + " не найдено")); + + if (updateEventAdminRequest.getAnnotation() != null) { + event.setAnnotation(updateEventAdminRequest.getAnnotation()); + } + if (updateEventAdminRequest.getDescription() != null) { + event.setDescription(updateEventAdminRequest.getDescription()); + } + if (updateEventAdminRequest.getTitle() != null) { + event.setTitle(updateEventAdminRequest.getTitle()); + } + if (updateEventAdminRequest.getCategory() != null) { + Category category = findCategory(updateEventAdminRequest.getCategory()); + event.setCategory(category); + } + if (updateEventAdminRequest.getLocation() != null) { + Location location = locationRepository.save(LocationMapper.toLocation(updateEventAdminRequest.getLocation())); + event.setLocation(location); + } + if (updateEventAdminRequest.getEventDate() != null) { + if (updateEventAdminRequest.getEventDate().isBefore(LocalDateTime.now().plusHours(1))) { + throw new ValidationException("Дата события должна быть не позднее, чем через 1 час после публикации"); + } + event.setEventDate(updateEventAdminRequest.getEventDate()); + } + if (updateEventAdminRequest.getPaid() != null) { + event.setPaid(updateEventAdminRequest.getPaid()); + } + if (updateEventAdminRequest.getParticipantLimit() != null) { + event.setParticipantLimit(updateEventAdminRequest.getParticipantLimit()); + } + if (updateEventAdminRequest.getRequestModeration() != null) { + event.setRequestModeration(updateEventAdminRequest.getRequestModeration()); + } + + if (updateEventAdminRequest.getStateAction() != null) { + switch (updateEventAdminRequest.getStateAction()) { + case PUBLISH_EVENT: + if (event.getState() != EventState.PENDING) { + throw new ConflictException("Невозможно опубликовать событие, так как оно находится в неправильном состоянии: " + event.getState()); + } + event.setState(EventState.PUBLISHED); + event.setPublishedOn(LocalDateTime.now()); + break; + case REJECT_EVENT: + if (event.getState() == EventState.PUBLISHED) { + throw new ConflictException("Невозможно отклонить событие, так как оно уже опубликовано"); + } + event.setState(EventState.CANCELED); + break; + } + } + + return EventMapper.toEventFullDto(eventRepository.save(event)); + } + + private Sort sortBy(String sort) { + if ("views".equalsIgnoreCase(sort)) { + return Sort.by(Sort.Direction.DESC, "views"); + } + return Sort.by(Sort.Direction.ASC, "eventDate"); + } + + + private User findUser(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Пользователь с id: " + id + " не найден")); + } + + private Category findCategory(Long id) { + return categoryRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Категория с id: " + id + " не найдена")); + } + + private Event findEvent(Long userId, Long eventId) { + return eventRepository.findByInitiatorIdAndId(userId, eventId) + .orElseThrow(() -> new NotFoundException("Событие текущего пользователя не найдено")); + } + + private void validateEventDate(LocalDateTime date) { + LocalDateTime now = LocalDateTime.now(); + if (date.isBefore(now.plusHours(2))) { + log.warn("Дата события должна быть не менее чем через 2 часа от текущего времени"); + throw new ValidationException("Дата события должна быть не менее чем через 2 часа от текущего времени"); + } + } +} diff --git a/main/src/main/java/ru/practicum/event/specification/EventSpecifications.java b/main/src/main/java/ru/practicum/event/specification/EventSpecifications.java new file mode 100644 index 0000000..0b9b04e --- /dev/null +++ b/main/src/main/java/ru/practicum/event/specification/EventSpecifications.java @@ -0,0 +1,76 @@ +package ru.practicum.event.specification; + +import org.springframework.data.jpa.domain.Specification; +import ru.practicum.event.model.Event; +import ru.practicum.event.state.EventState; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +public class EventSpecifications { + public static Specification textContains(String text) { + return (root, query, cb) -> { + if (text == null || text.isBlank()) return null; + String pattern = "%" + text.toLowerCase() + "%"; + return cb.or( + cb.like(cb.lower(root.get("annotation")), pattern), + cb.like(cb.lower(root.get("description")), pattern) + ); + }; + } + + public static Specification categoryIn(List categories) { + return (root, query, cb) -> { + if (categories == null || categories.isEmpty()) return null; + return root.get("category").get("id").in(categories); + }; + } + + public static Specification paidIs(Boolean paid) { + return (root, query, cb) -> { + if (paid == null) return null; + return cb.equal(root.get("paid"), paid); + }; + } + + public static Specification dateBetween(LocalDateTime start, LocalDateTime end) { + LocalDateTime startDate = (start != null) ? start : LocalDateTime.now(); + + return (root, query, cb) -> { + if (end != null) { + return cb.between(root.get("eventDate"), startDate, end); + } else { + return cb.greaterThanOrEqualTo(root.get("eventDate"), startDate); + } + }; + } + + public static Specification onlyAvailable(Boolean onlyAvailable) { + return (root, query, cb) -> { + if (onlyAvailable == null || !onlyAvailable) return null; + return cb.greaterThan(root.get("participantLimit"), root.get("confirmedRequests")); + }; + } + + public static Specification published() { + return (root, query, cb) -> cb.equal(root.get("state"), EventState.PUBLISHED); + } + + public static Specification userIn(List userIds) { + return (root, query, cb) -> { + if (userIds == null || userIds.isEmpty()) return null; + return root.get("initiator").get("id").in(userIds); + }; + } + + public static Specification stateIn(List states) { + return (root, query, cb) -> { + if (states == null || states.isEmpty()) return null; + List eventStates = states.stream() + .map(EventState::valueOf) + .collect(Collectors.toList()); + return root.get("state").in(eventStates); + }; + } +} diff --git a/main/src/main/java/ru/practicum/event/state/EventState.java b/main/src/main/java/ru/practicum/event/state/EventState.java new file mode 100644 index 0000000..7849491 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/state/EventState.java @@ -0,0 +1,7 @@ +package ru.practicum.event.state; + +public enum EventState { + PENDING, + PUBLISHED, + CANCELED +} diff --git a/main/src/main/java/ru/practicum/event/state/EventStateAction.java b/main/src/main/java/ru/practicum/event/state/EventStateAction.java new file mode 100644 index 0000000..905addd --- /dev/null +++ b/main/src/main/java/ru/practicum/event/state/EventStateAction.java @@ -0,0 +1,6 @@ +package ru.practicum.event.state; + +public enum EventStateAction { + SEND_TO_REVIEW, + CANCEL_REVIEW +} diff --git a/main/src/main/java/ru/practicum/event/state/EventStateActionSolution.java b/main/src/main/java/ru/practicum/event/state/EventStateActionSolution.java new file mode 100644 index 0000000..8740cc3 --- /dev/null +++ b/main/src/main/java/ru/practicum/event/state/EventStateActionSolution.java @@ -0,0 +1,6 @@ +package ru.practicum.event.state; + +public enum EventStateActionSolution { + PUBLISH_EVENT, + REJECT_EVENT +} diff --git a/main/src/main/java/ru/practicum/exception/ConflictException.java b/main/src/main/java/ru/practicum/exception/ConflictException.java new file mode 100644 index 0000000..e2fddf1 --- /dev/null +++ b/main/src/main/java/ru/practicum/exception/ConflictException.java @@ -0,0 +1,7 @@ +package ru.practicum.exception; + +public class ConflictException extends RuntimeException { + public ConflictException(String message) { + super(message); + } +} diff --git a/main/src/main/java/ru/practicum/exception/NotFoundException.java b/main/src/main/java/ru/practicum/exception/NotFoundException.java new file mode 100644 index 0000000..628c788 --- /dev/null +++ b/main/src/main/java/ru/practicum/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.practicum.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/main/src/main/java/ru/practicum/exception/ValidationException.java b/main/src/main/java/ru/practicum/exception/ValidationException.java new file mode 100644 index 0000000..d751d21 --- /dev/null +++ b/main/src/main/java/ru/practicum/exception/ValidationException.java @@ -0,0 +1,7 @@ +package ru.practicum.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } +} diff --git a/main/src/main/java/ru/practicum/location/dto/LocationDto.java b/main/src/main/java/ru/practicum/location/dto/LocationDto.java new file mode 100644 index 0000000..2547c6c --- /dev/null +++ b/main/src/main/java/ru/practicum/location/dto/LocationDto.java @@ -0,0 +1,25 @@ +package ru.practicum.location.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class LocationDto { + @Min(-90) + @Max(90) + @NotNull + private Float lat; + + @Min(-180) + @Max(180) + @NotNull + private Float lon; +} diff --git a/main/src/main/java/ru/practicum/location/dto/mapper/LocationMapper.java b/main/src/main/java/ru/practicum/location/dto/mapper/LocationMapper.java new file mode 100644 index 0000000..996437e --- /dev/null +++ b/main/src/main/java/ru/practicum/location/dto/mapper/LocationMapper.java @@ -0,0 +1,23 @@ +package ru.practicum.location.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.location.model.Location; + +@UtilityClass +public class LocationMapper { + public static LocationDto toLocationDto(Location location) { + return new LocationDto( + location.getLat(), + location.getLon() + ); + } + + public static Location toLocation(LocationDto locationDto) { + return new Location( + null, + locationDto.getLat(), + locationDto.getLon() + ); + } +} diff --git a/main/src/main/java/ru/practicum/location/model/Location.java b/main/src/main/java/ru/practicum/location/model/Location.java new file mode 100644 index 0000000..779ea3d --- /dev/null +++ b/main/src/main/java/ru/practicum/location/model/Location.java @@ -0,0 +1,39 @@ +package ru.practicum.location.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Objects; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Table(name = "locations") +@Entity +public class Location { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Float lat; + + @Column(nullable = false) + private Float lon; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Location location)) return false; + return Objects.equals(id, location.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/main/src/main/java/ru/practicum/location/repository/LocationRepository.java b/main/src/main/java/ru/practicum/location/repository/LocationRepository.java new file mode 100644 index 0000000..c183685 --- /dev/null +++ b/main/src/main/java/ru/practicum/location/repository/LocationRepository.java @@ -0,0 +1,7 @@ +package ru.practicum.location.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.location.model.Location; + +public interface LocationRepository extends JpaRepository { +} diff --git a/main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java b/main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java new file mode 100644 index 0000000..cc3a2c2 --- /dev/null +++ b/main/src/main/java/ru/practicum/request/controller/RequestPrivateController.java @@ -0,0 +1,39 @@ +package ru.practicum.request.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.service.RequestService; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/users/{userId}/requests") +public class RequestPrivateController { + private final RequestService requestService; + + @GetMapping + public List getRequestsByUserId(@PathVariable(value = "userId") Long userId) { + log.info("GET /users/{}/requests", userId); + return requestService.getRequestsByUserId(userId); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public ParticipationRequestDto createRequest(@PathVariable(value = "userId") Long userId, + @RequestParam(name = "eventId") Long eventId) { + log.info("Запрос на участие в событии с id= {}. POST /users/{}/requests", eventId, userId); + return requestService.createRequest(userId, eventId); + } + + @PatchMapping("/{requestId}/cancel") + public ParticipationRequestDto cancelRequest(@PathVariable(value = "userId") Long userId, + @PathVariable(value = "requestId") Long requestId) { + log.info("GET /users/{}/requests/{}/cancel", userId, requestId); + return requestService.cancelRequest(userId, requestId); + } +} diff --git a/main/src/main/java/ru/practicum/request/dto/ParticipationRequestDto.java b/main/src/main/java/ru/practicum/request/dto/ParticipationRequestDto.java new file mode 100644 index 0000000..11e6670 --- /dev/null +++ b/main/src/main/java/ru/practicum/request/dto/ParticipationRequestDto.java @@ -0,0 +1,27 @@ +package ru.practicum.request.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.request.status.RequestStatus; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class ParticipationRequestDto { + private Long id; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime created; + + private Long event; + + private Long requester; + + private RequestStatus status; +} diff --git a/main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java b/main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java new file mode 100644 index 0000000..f3c04fc --- /dev/null +++ b/main/src/main/java/ru/practicum/request/dto/mapper/RequestMapper.java @@ -0,0 +1,18 @@ +package ru.practicum.request.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.model.Request; + +@UtilityClass +public class RequestMapper { + public static ParticipationRequestDto toParticipationRequestDto(Request request) { + return new ParticipationRequestDto( + request.getId(), + request.getCreated(), + request.getEvent().getId(), + request.getRequester().getId(), + request.getStatus() + ); + } +} diff --git a/main/src/main/java/ru/practicum/request/model/Request.java b/main/src/main/java/ru/practicum/request/model/Request.java new file mode 100644 index 0000000..67bdc0d --- /dev/null +++ b/main/src/main/java/ru/practicum/request/model/Request.java @@ -0,0 +1,55 @@ +package ru.practicum.request.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.practicum.event.model.Event; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; +import java.util.Objects; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Entity +@Table( + name = "requests", + uniqueConstraints = {@UniqueConstraint(columnNames = {"event_id", "requester_id"})} +) +public class Request { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "create_date", nullable = false) + private LocalDateTime created = LocalDateTime.now(); + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "event_id", nullable = false) + private Event event; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "requester_id", nullable = false) + private User requester; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private RequestStatus status = RequestStatus.PENDING; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Request request)) return false; + return Objects.equals(id, request.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/main/src/main/java/ru/practicum/request/repository/RequestRepository.java b/main/src/main/java/ru/practicum/request/repository/RequestRepository.java new file mode 100644 index 0000000..4c382dd --- /dev/null +++ b/main/src/main/java/ru/practicum/request/repository/RequestRepository.java @@ -0,0 +1,21 @@ +package ru.practicum.request.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.request.model.Request; +import ru.practicum.request.status.RequestStatus; + +import java.util.List; + +public interface RequestRepository extends JpaRepository { + Boolean existsByRequesterIdAndEventId(Long requesterId, Long eventId); + + Long countByEventIdAndStatus(Long eventId, RequestStatus status); + + List findAllByRequesterId(Long requesterId); + + List findAllByEventId(Long eventId); + + List findAllByIdIn(List ids); + + List findAllByEventIdAndStatus(Long eventId, RequestStatus status); +} diff --git a/main/src/main/java/ru/practicum/request/service/RequestService.java b/main/src/main/java/ru/practicum/request/service/RequestService.java new file mode 100644 index 0000000..f1dd929 --- /dev/null +++ b/main/src/main/java/ru/practicum/request/service/RequestService.java @@ -0,0 +1,13 @@ +package ru.practicum.request.service; + +import ru.practicum.request.dto.ParticipationRequestDto; + +import java.util.List; + +public interface RequestService { + ParticipationRequestDto createRequest(Long userId, Long eventId); + + List getRequestsByUserId(Long userId); + + ParticipationRequestDto cancelRequest(Long userId, Long requestId); +} diff --git a/main/src/main/java/ru/practicum/request/service/RequestServiceImpl.java b/main/src/main/java/ru/practicum/request/service/RequestServiceImpl.java new file mode 100644 index 0000000..0a4faf7 --- /dev/null +++ b/main/src/main/java/ru/practicum/request/service/RequestServiceImpl.java @@ -0,0 +1,108 @@ +package ru.practicum.request.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.event.model.Event; +import ru.practicum.event.repository.EventRepository; +import ru.practicum.event.state.EventState; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.dto.mapper.RequestMapper; +import ru.practicum.request.model.Request; +import ru.practicum.request.repository.RequestRepository; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.model.User; +import ru.practicum.user.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Transactional(readOnly = true) +@Slf4j +@RequiredArgsConstructor +@Service +public class RequestServiceImpl implements RequestService { + private final RequestRepository requestRepository; + private final UserRepository userRepository; + private final EventRepository eventRepository; + + @Transactional + @Override + public ParticipationRequestDto createRequest(Long userId, Long eventId) { + User user = findUser(userId); + Event event = findEvent(eventId); + + if (event.getInitiator().getId().equals(userId)) { + throw new ConflictException("Инициатор события не может добавить заявку на участие в своём событии"); + } + + if (!EventState.PUBLISHED.equals(event.getState())) { + throw new ConflictException("Нельзя участвовать в неопубликованном событии"); + } + + boolean exists = requestRepository.existsByRequesterIdAndEventId(userId, eventId); + if (exists) { + throw new ConflictException("Нельзя добавить повторный запрос на участие в событии"); + } + + long confirmedRequests = requestRepository.countByEventIdAndStatus(eventId, RequestStatus.CONFIRMED); + if (event.getParticipantLimit() != null && event.getParticipantLimit() > 0 && + confirmedRequests >= event.getParticipantLimit()) { + throw new ConflictException("Достигнут лимит запросов на участие в событии"); + } + + Request request = new Request(); + request.setEvent(event); + request.setRequester(user); + request.setCreated(LocalDateTime.now()); + + if (Boolean.FALSE.equals(event.getRequestModeration()) || event.getParticipantLimit() == 0) { + request.setStatus(RequestStatus.CONFIRMED); + } else { + request.setStatus(RequestStatus.PENDING); + } + + return RequestMapper.toParticipationRequestDto(requestRepository.save(request)); + } + + @Override + public List getRequestsByUserId(Long userId) { + findUser(userId); + + List requests = requestRepository.findAllByRequesterId(userId); + + return requests.stream() + .map(RequestMapper::toParticipationRequestDto) + .toList(); + } + + @Transactional + @Override + public ParticipationRequestDto cancelRequest(Long userId, Long requestId) { + findUser(userId); + + Request request = requestRepository.findById(requestId) + .orElseThrow(() -> new NotFoundException("Запрос с id:" + requestId + " не найден")); + + if (!request.getRequester().getId().equals(userId)) { + throw new NotFoundException("Заявка с id:" + requestId + " недоступна для пользователя c id:" + userId); + } + + request.setStatus(RequestStatus.CANCELED); + + return RequestMapper.toParticipationRequestDto(requestRepository.save(request)); + } + + private User findUser(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Пользователь с id:" + id + " не найден")); + } + + private Event findEvent(Long id) { + return eventRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Событие с id:" + id + " не найдено")); + } +} diff --git a/main/src/main/java/ru/practicum/request/status/RequestStatus.java b/main/src/main/java/ru/practicum/request/status/RequestStatus.java new file mode 100644 index 0000000..680575b --- /dev/null +++ b/main/src/main/java/ru/practicum/request/status/RequestStatus.java @@ -0,0 +1,8 @@ +package ru.practicum.request.status; + +public enum RequestStatus { + PENDING, + CONFIRMED, + REJECTED, + CANCELED +} diff --git a/main/src/main/java/ru/practicum/user/controller/UserAdminController.java b/main/src/main/java/ru/practicum/user/controller/UserAdminController.java new file mode 100644 index 0000000..9cce4cf --- /dev/null +++ b/main/src/main/java/ru/practicum/user/controller/UserAdminController.java @@ -0,0 +1,44 @@ +package ru.practicum.user.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; +import ru.practicum.user.service.UserService; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(path = "/admin/users") +public class UserAdminController { + private final UserService userService; + + @GetMapping + public List getUsers(@RequestParam(required = false) List ids, + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + log.info("GET /admin/users"); + return userService.getUsers(ids, from, size); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public UserDto createUser(@RequestBody @Valid NewUserRequest newUserRequest) { + log.info("POST /admin/users"); + return userService.createUser(newUserRequest); + } + + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping("/{userId}") + public void deleteUser(@PathVariable("userId") Long id) { + log.info("DELETE /admin/users/{}", id); + userService.deleteUser(id); + } +} diff --git a/main/src/main/java/ru/practicum/user/dto/NewUserRequest.java b/main/src/main/java/ru/practicum/user/dto/NewUserRequest.java new file mode 100644 index 0000000..aef7a3d --- /dev/null +++ b/main/src/main/java/ru/practicum/user/dto/NewUserRequest.java @@ -0,0 +1,24 @@ +package ru.practicum.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class NewUserRequest { + @Email(message = "Email должен быть в корректном формате") + @NotBlank(message = "Email не может быть пустым") + @Size(min = 6, max = 254, message = "Email должен быть в границах от {min} до {max} символов") + private String email; + + @NotBlank(message = "Имя пользователя не может быть пустым") + @Size(min = 2, max = 250, message = "Имя пользователя должно быть в границах от {min} до {max} символов") + private String name; +} diff --git a/main/src/main/java/ru/practicum/user/dto/UserDto.java b/main/src/main/java/ru/practicum/user/dto/UserDto.java new file mode 100644 index 0000000..66728db --- /dev/null +++ b/main/src/main/java/ru/practicum/user/dto/UserDto.java @@ -0,0 +1,16 @@ +package ru.practicum.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class UserDto { + private Long id; + private String email; + private String name; +} diff --git a/main/src/main/java/ru/practicum/user/dto/UserShortDto.java b/main/src/main/java/ru/practicum/user/dto/UserShortDto.java new file mode 100644 index 0000000..09df516 --- /dev/null +++ b/main/src/main/java/ru/practicum/user/dto/UserShortDto.java @@ -0,0 +1,15 @@ +package ru.practicum.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class UserShortDto { + private Long id; + private String name; +} diff --git a/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java b/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java new file mode 100644 index 0000000..e10d20f --- /dev/null +++ b/main/src/main/java/ru/practicum/user/dto/mapper/UserMapper.java @@ -0,0 +1,33 @@ +package ru.practicum.user.dto.mapper; + +import lombok.experimental.UtilityClass; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; +import ru.practicum.user.dto.UserShortDto; +import ru.practicum.user.model.User; + +@UtilityClass +public class UserMapper { + public static UserDto toUserDto(User user) { + return new UserDto( + user.getId(), + user.getEmail(), + user.getName() + ); + } + + public static User toUser(NewUserRequest newUserRequest) { + return new User( + null, + newUserRequest.getEmail(), + newUserRequest.getName() + ); + } + + public static UserShortDto toUserShortDto(User user) { + return new UserShortDto( + user.getId(), + user.getName() + ); + } +} diff --git a/main/src/main/java/ru/practicum/user/model/User.java b/main/src/main/java/ru/practicum/user/model/User.java new file mode 100644 index 0000000..9b43fa3 --- /dev/null +++ b/main/src/main/java/ru/practicum/user/model/User.java @@ -0,0 +1,39 @@ +package ru.practicum.user.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Objects; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Table(name = "users") +@Entity +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true, length = 254) + private String email; + + @Column(nullable = false, length = 250) + private String name; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof User user)) return false; + return Objects.equals(email, user.email); + } + + @Override + public int hashCode() { + return Objects.hash(email); + } +} diff --git a/main/src/main/java/ru/practicum/user/repository/UserRepository.java b/main/src/main/java/ru/practicum/user/repository/UserRepository.java new file mode 100644 index 0000000..1d4c386 --- /dev/null +++ b/main/src/main/java/ru/practicum/user/repository/UserRepository.java @@ -0,0 +1,13 @@ +package ru.practicum.user.repository; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.user.model.User; + +import java.util.List; + +public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); + + List findByIdIn(List ids, Pageable pageable); +} diff --git a/main/src/main/java/ru/practicum/user/service/UserService.java b/main/src/main/java/ru/practicum/user/service/UserService.java new file mode 100644 index 0000000..93ef241 --- /dev/null +++ b/main/src/main/java/ru/practicum/user/service/UserService.java @@ -0,0 +1,14 @@ +package ru.practicum.user.service; + +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; + +import java.util.List; + +public interface UserService { + List getUsers(List ids, Integer from, Integer size); + + UserDto createUser(NewUserRequest newUserRequest); + + void deleteUser(Long id); +} diff --git a/main/src/main/java/ru/practicum/user/service/UserServiceImpl.java b/main/src/main/java/ru/practicum/user/service/UserServiceImpl.java new file mode 100644 index 0000000..dad03eb --- /dev/null +++ b/main/src/main/java/ru/practicum/user/service/UserServiceImpl.java @@ -0,0 +1,61 @@ +package ru.practicum.user.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; +import ru.practicum.user.dto.mapper.UserMapper; +import ru.practicum.user.repository.UserRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Transactional(readOnly = true) +@Slf4j +@RequiredArgsConstructor +@Service +public class UserServiceImpl implements UserService { + private final UserRepository userRepository; + + @Override + public List getUsers(List ids, Integer from, Integer size) { + PageRequest page = PageRequest.of(from / size, size); + + if (ids != null && !ids.isEmpty()) { + return userRepository.findByIdIn(ids, page) + .stream() + .map(UserMapper::toUserDto) + .collect(Collectors.toList()); + } else { + return userRepository.findAll(page) + .stream() + .map(UserMapper::toUserDto) + .collect(Collectors.toList()); + } + } + + @Transactional + @Override + public UserDto createUser(NewUserRequest newUserRequest) { + if (userRepository.existsByEmail(newUserRequest.getEmail())) { + log.warn("Пользователь с email - {} уже существует", newUserRequest.getEmail()); + throw new ConflictException("Пользователь с email - " + newUserRequest.getEmail() + " уже существует"); + } + return UserMapper.toUserDto(userRepository.save(UserMapper.toUser(newUserRequest))); + } + + @Transactional + @Override + public void deleteUser(Long id) { + if (!userRepository.existsById(id)) { + log.warn("Пользователь с id - {} не найден", id); + throw new NotFoundException("Пользователь с id - " + id + " не найден"); + } + userRepository.deleteById(id); + } +} diff --git a/main/src/main/resources/application.yml b/main/src/main/resources/application.yml index 443332b..d39638a 100644 --- a/main/src/main/resources/application.yml +++ b/main/src/main/resources/application.yml @@ -1,7 +1,17 @@ spring: + profiles: + active: dev + +--- + +spring: + config: + activate: + on-profile: dev datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://localhost:6542/maindb +# url: jdbc:postgresql://localhost:6542/maindb + url: jdbc:postgresql://main-db:6542/maindb username: dbuser2 password: 1234567 jpa: @@ -15,5 +25,31 @@ spring: server: port: 8080 +#client: +# url: http://localhost:9090 +client: + url: http://stats-server:9090 + +--- + +spring: + config: + activate: + on-profile: test + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:maindb + username: dbuser2 + password: 1234567 + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + +server: + port: 8080 + client: url: http://localhost:9090 \ No newline at end of file diff --git a/main/src/main/resources/schema.sql b/main/src/main/resources/schema.sql new file mode 100644 index 0000000..d34c148 --- /dev/null +++ b/main/src/main/resources/schema.sql @@ -0,0 +1,70 @@ +create table IF NOT EXISTS categories ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + name VARCHAR(50) NOT NULL, + CONSTRAINT uq_category_name UNIQUE (name), + CONSTRAINT pk_category_id PRIMARY KEY (id) +); + +create table IF NOT EXISTS users ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + email VARCHAR(254) NOT NULL, + name VARCHAR(250) NOT NULL, + CONSTRAINT uq_user_email UNIQUE (email), + CONSTRAINT pk_user_id PRIMARY KEY (id) +); + +create table IF NOT EXISTS locations ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + lat REAL, + lon REAL, + CONSTRAINT pk_location_id PRIMARY KEY (id) +); + +create table IF NOT EXISTS events ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + annotation VARCHAR(2000) NOT NULL, + category_id BIGINT NOT NULL, + confirmed_requests BIGINT, + created_on TIMESTAMP WITHOUT TIME ZONE NOT NULL, + description VARCHAR(7000) NOT NULL, + event_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + initiator_id BIGINT NOT NULL, + location_id BIGINT NOT NULL, + paid Boolean NOT NULL, + participant_limit INTEGER DEFAULT 0, + published_on TIMESTAMP WITHOUT TIME ZONE, + request_moderation BOOLEAN DEFAULT true, + state VARCHAR(50) NOT NULL, + title VARCHAR(120) NOT NULL, + CONSTRAINT fk_event_category FOREIGN KEY (category_id) REFERENCES categories (id), + CONSTRAINT fk_event_user FOREIGN KEY (initiator_id) REFERENCES users (id), + CONSTRAINT fk_event_location FOREIGN KEY (location_id) REFERENCES locations (id), + CONSTRAINT pk_event_id PRIMARY KEY (id) +); + +create table IF NOT EXISTS requests ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + create_date TIMESTAMP WITHOUT TIME ZONE NOT NULL, + event_id BIGINT NOT NULL, + requester_id BIGINT NOT NULL, + status VARCHAR(20) NOT NULL, + CONSTRAINT fk_request_event FOREIGN KEY (event_id) REFERENCES events (id), + CONSTRAINT fk_request_user FOREIGN KEY (requester_id) REFERENCES users (id), + CONSTRAINT uq_request UNIQUE (event_id, requester_id), + CONSTRAINT pk_request_id PRIMARY KEY (id) +); + +create table IF NOT EXISTS compilations ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + title VARCHAR(50) NOT NULL, + pinned BOOLEAN NOT NULL, + CONSTRAINT pk_compilation_id PRIMARY KEY (id) +); + +create table IF NOT EXISTS compilations_events( + compilation_id BIGINT NOT NULL, + event_id BIGINT NOT NULL, + CONSTRAINT fk_compilations_events_compilation FOREIGN KEY (compilation_id) REFERENCES compilations (id) ON update CASCADE on delete cascade, + CONSTRAINT fk_compilations_events_event FOREIGN KEY (event_id) REFERENCES events (id) ON update CASCADE on delete cascade, + CONSTRAINT pk_compilation_event PRIMARY KEY (compilation_id, event_id) +); \ No newline at end of file diff --git a/main/src/test/java/ru/practicum/category/CategoryAdminControllerTest.java b/main/src/test/java/ru/practicum/category/CategoryAdminControllerTest.java new file mode 100644 index 0000000..bc8f9e2 --- /dev/null +++ b/main/src/test/java/ru/practicum/category/CategoryAdminControllerTest.java @@ -0,0 +1,92 @@ +package ru.practicum.category; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.category.controller.CategoryAdminController; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.service.CategoryService; +import ru.practicum.error.ErrorHandler; + +import java.nio.charset.StandardCharsets; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +class CategoryAdminControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + @Mock + private CategoryService categoryService; + @InjectMocks + private CategoryAdminController categoryAdminController; + private MockMvc mvc; + private CategoryDto categoryDto; + private NewCategoryDto newCategoryDto; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + mvc = MockMvcBuilders + .standaloneSetup(categoryAdminController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + categoryDto = new CategoryDto(1L, "Концерты"); + newCategoryDto = new NewCategoryDto("Концерты"); + } + + @Test + void mustCreateCategory() throws Exception { + when(categoryService.createCategory(any())) + .thenReturn(categoryDto); + + mvc.perform(post("/admin/categories") + .content(mapper.writeValueAsString(newCategoryDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.name", is("Концерты"))); + } + + @Test + void mustDeleteCategory() throws Exception { + mvc.perform(delete("/admin/categories/1")) + .andExpect(status().isNoContent()); + + verify(categoryService).deleteCategory(1L); + } + + @Test + void mustUpdateCategory() throws Exception { + CategoryDto updated = new CategoryDto(1L, "Фестивали"); + when(categoryService.updateCategory(eq(1L), any(NewCategoryDto.class))) + .thenReturn(updated); + + mvc.perform(patch("/admin/categories/1") + .content(mapper.writeValueAsString(newCategoryDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.name", is("Фестивали"))); + } +} diff --git a/main/src/test/java/ru/practicum/category/CategoryDtoTest.java b/main/src/test/java/ru/practicum/category/CategoryDtoTest.java new file mode 100644 index 0000000..8b9cda9 --- /dev/null +++ b/main/src/test/java/ru/practicum/category/CategoryDtoTest.java @@ -0,0 +1,41 @@ +package ru.practicum.category; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.MainServiceApp; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@ContextConfiguration(classes = MainServiceApp.class) +@JsonTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class CategoryDtoTest { + private final JacksonTester jsonCategoryDto; + private final JacksonTester jsonNewCategoryDto; + + @Test + void shouldReturnCategoryDto() throws Exception { + CategoryDto dto = new CategoryDto(1L, "Выставки"); + + JsonContent result = jsonCategoryDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Выставки"); + } + + @Test + void shouldReturnNewCategoryDto() throws Exception { + NewCategoryDto dto = new NewCategoryDto("Выставки"); + + JsonContent result = jsonNewCategoryDto.write(dto); + + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Выставки"); + } +} diff --git a/main/src/test/java/ru/practicum/category/CategoryMapperTest.java b/main/src/test/java/ru/practicum/category/CategoryMapperTest.java new file mode 100644 index 0000000..7c3deae --- /dev/null +++ b/main/src/test/java/ru/practicum/category/CategoryMapperTest.java @@ -0,0 +1,31 @@ +package ru.practicum.category; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.dto.mapper.CategoryMapper; +import ru.practicum.category.model.Category; + + +public class CategoryMapperTest { + @Test + void shouldMapCategoryToCategoryDto() { + Category category = new Category(1L, "Кино"); + + CategoryDto result = CategoryMapper.toCategoryDto(category); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Кино"); + } + + @Test + void shouldMapNewCategoryDtoToCategory() { + NewCategoryDto newCategoryDto = new NewCategoryDto("Кино"); + + Category result = CategoryMapper.toCategory(newCategoryDto); + + AssertionsForClassTypes.assertThat(result.getId()).isNull(); + AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Кино"); + } +} diff --git a/main/src/test/java/ru/practicum/category/CategoryPublicControllerTest.java b/main/src/test/java/ru/practicum/category/CategoryPublicControllerTest.java new file mode 100644 index 0000000..3b0ecbf --- /dev/null +++ b/main/src/test/java/ru/practicum/category/CategoryPublicControllerTest.java @@ -0,0 +1,83 @@ +package ru.practicum.category; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.category.controller.CategoryPublicController; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.service.CategoryService; +import ru.practicum.error.ErrorHandler; + +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +public class CategoryPublicControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + @Mock + private CategoryService categoryService; + @InjectMocks + private CategoryPublicController categoryController; + private MockMvc mvc; + private CategoryDto categoryDto; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(categoryController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + categoryDto = new CategoryDto(1L, "Кино"); + } + + @Test + void mustReturnAllCategories() throws Exception { + List categories = List.of( + new CategoryDto(1L, "Кино"), + new CategoryDto(2L, "Фестиваль") + ); + + when(categoryService.getCategories(anyInt(), anyInt())) + .thenReturn(categories); + + mvc.perform(get("/categories") + .param("from", "0") + .param("size", "10") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.size()").value(2)) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].name", is("Кино"))) + .andExpect(jsonPath("$[1].id", is(2))) + .andExpect(jsonPath("$[1].name", is("Фестиваль"))); + } + + @Test + void mustReturnCategoryById() throws Exception { + when(categoryService.getCategory(1L)) + .thenReturn(categoryDto); + + mvc.perform(get("/categories/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.name", is("Кино"))); + } +} diff --git a/main/src/test/java/ru/practicum/category/CategoryServiceImplTest.java b/main/src/test/java/ru/practicum/category/CategoryServiceImplTest.java new file mode 100644 index 0000000..8aa6a88 --- /dev/null +++ b/main/src/test/java/ru/practicum/category/CategoryServiceImplTest.java @@ -0,0 +1,117 @@ +package ru.practicum.category; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.MainServiceApp; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.model.Category; +import ru.practicum.category.service.CategoryService; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +@Transactional +@ActiveProfiles("test") +@SpringBootTest( + classes = MainServiceApp.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class CategoryServiceImplTest { + private final EntityManager em; + private final CategoryService categoryService; + + private Long categoryId; + + @BeforeEach + void setUp() { + Category category = new Category(); + category.setName("Кино"); + + em.persist(category); + em.flush(); + + categoryId = category.getId(); + } + + @Test + void mustReturnCategoryById() { + CategoryDto categoryDto = categoryService.getCategory(categoryId); + + assertThat(categoryDto.getId()).isEqualTo(categoryId); + assertThat(categoryDto.getName()).isEqualTo("Кино"); + } + + @Test + void shouldThrowNotFoundExceptionForInvalidId() { + Assertions.assertThrows(NotFoundException.class, () -> categoryService.getCategory(999999L)); + } + + @Test + void mustCreateCategory() { + NewCategoryDto newCategoryDto = new NewCategoryDto(); + newCategoryDto.setName("Фестиваль"); + + CategoryDto created = categoryService.createCategory(newCategoryDto); + + assertThat(created.getId()).isNotNull(); + assertThat(created.getName()).isEqualTo("Фестиваль"); + } + + @Test + void mustThrowConflictExceptionWhenCreatingDuplicateCategory() { + NewCategoryDto newCategoryDto = new NewCategoryDto(); + newCategoryDto.setName("Кино"); + + Assertions.assertThrows(ConflictException.class, () -> categoryService.createCategory(newCategoryDto)); + } + + @Test + void mustUpdateCategoryName() { + NewCategoryDto updateDto = new NewCategoryDto(); + updateDto.setName("Спектакль"); + + CategoryDto updated = categoryService.updateCategory(categoryId, updateDto); + + assertThat(updated.getId()).isEqualTo(categoryId); + assertThat(updated.getName()).isEqualTo("Спектакль"); + } + + @Test + void shouldThrowConflictExceptionWhenUpdatingToExistingName() { + Category another = new Category(); + another.setName("Спектакль"); + em.persist(another); + em.flush(); + + NewCategoryDto updateDto = new NewCategoryDto(); + updateDto.setName("Спектакль"); + + Assertions.assertThrows(ConflictException.class, () -> categoryService.updateCategory(categoryId, updateDto)); + } + + @Test + void mustDeleteCategory() { + categoryService.deleteCategory(categoryId); + + Assertions.assertThrows(NotFoundException.class, () -> categoryService.getCategory(categoryId)); + } + + @Test + void mustReturnPaginatedCategories() { + List categories = categoryService.getCategories(0, 10); + + assertThat(categories.get(0).getName()).isEqualTo("Кино"); + } +} diff --git a/main/src/test/java/ru/practicum/compilation/CompilationAdminControllerTest.java b/main/src/test/java/ru/practicum/compilation/CompilationAdminControllerTest.java new file mode 100644 index 0000000..c7b9741 --- /dev/null +++ b/main/src/test/java/ru/practicum/compilation/CompilationAdminControllerTest.java @@ -0,0 +1,103 @@ +package ru.practicum.compilation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.compilation.controller.CompilationAdminController; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.UpdateCompilationRequest; +import ru.practicum.compilation.service.CompilationService; +import ru.practicum.error.ErrorHandler; + +import java.nio.charset.StandardCharsets; +import java.util.Set; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +public class CompilationAdminControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private CompilationService compilationService; + + @InjectMocks + private CompilationAdminController compilationAdminController; + + private MockMvc mvc; + + private CompilationDto compilationDto; + private NewCompilationDto newCompilationDto; + private UpdateCompilationRequest updateCompilationRequest; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + mvc = MockMvcBuilders + .standaloneSetup(compilationAdminController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + compilationDto = new CompilationDto(1L, "Подборка сериалов", true, Set.of()); + newCompilationDto = new NewCompilationDto(Set.of(), true, "Подборка сериалов"); + updateCompilationRequest = new UpdateCompilationRequest(Set.of(), false, "Следующая подборка"); + } + + @Test + void mustCreateCompilation() throws Exception { + when(compilationService.createCompilation(any())) + .thenReturn(compilationDto); + + mvc.perform(post("/admin/compilations") + .content(mapper.writeValueAsString(newCompilationDto)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.title", is("Подборка сериалов"))) + .andExpect(jsonPath("$.pinned", is(true))); + } + + @Test + void mustDeleteCompilation() throws Exception { + mvc.perform(delete("/admin/compilations/1")) + .andExpect(status().isNoContent()); + + verify(compilationService).deleteCompilation(1L); + } + + @Test + void mustUpdateCompilation() throws Exception { + CompilationDto updated = new CompilationDto(1L, "Следующая подборка", false, Set.of()); + when(compilationService.updateCompilation(eq(1L), any(UpdateCompilationRequest.class))) + .thenReturn(updated); + + mvc.perform(patch("/admin/compilations/1") + .content(mapper.writeValueAsString(updateCompilationRequest)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.title", is("Следующая подборка"))) + .andExpect(jsonPath("$.pinned", is(false))); + } +} + diff --git a/main/src/test/java/ru/practicum/compilation/CompilationDtoTest.java b/main/src/test/java/ru/practicum/compilation/CompilationDtoTest.java new file mode 100644 index 0000000..636d5b1 --- /dev/null +++ b/main/src/test/java/ru/practicum/compilation/CompilationDtoTest.java @@ -0,0 +1,94 @@ +package ru.practicum.compilation; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.MainServiceApp; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.UpdateCompilationRequest; +import ru.practicum.event.dto.EventShortDto; +import ru.practicum.user.dto.UserShortDto; + +import java.time.LocalDateTime; +import java.util.Set; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@JsonTest +@ContextConfiguration(classes = MainServiceApp.class) +class CompilationDtoTest { + + @Autowired + private JacksonTester jsonCompilationDto; + + @Autowired + private JacksonTester jsonNewCompilationDto; + + @Autowired + private JacksonTester jsonUpdateCompilationRequest; + + @Test + void shouldReturnCompilationDto() throws Exception { + EventShortDto eventShortDto = new EventShortDto( + 10L, + "Интересный концерт", + new CategoryDto(1L, "Музыка"), + 50L, + LocalDateTime.of(2025, 1, 1, 19, 0), + new UserShortDto(5L, "Макс"), + true, + "Концерт в парке", + 1000L + ); + + CompilationDto compilationDto = new CompilationDto( + 1L, + "Необычные фотозоны", + true, + Set.of(eventShortDto) + ); + + JsonContent result = jsonCompilationDto.write(compilationDto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Необычные фотозоны"); + assertThat(result).extractingJsonPathBooleanValue("$.pinned").isEqualTo(true); + assertThat(result).extractingJsonPathArrayValue("$.events").hasSize(1); + assertThat(result).extractingJsonPathNumberValue("$.events[0].id").isEqualTo(10); + } + + @Test + void shouldReturnNewCompilationDto() throws Exception { + NewCompilationDto dto = new NewCompilationDto( + Set.of(100L, 200L), + false, + "Подборка сериалов" + ); + + JsonContent result = jsonNewCompilationDto.write(dto); + + assertThat(result).extractingJsonPathArrayValue("$.events").containsExactlyInAnyOrder(100, 200); + assertThat(result).extractingJsonPathBooleanValue("$.pinned").isEqualTo(false); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Подборка сериалов"); + } + + @Test + void shouldReturnUpdateCompilationRequest() throws Exception { + UpdateCompilationRequest dto = new UpdateCompilationRequest( + Set.of(300L), + true, + "Обновленная подборка" + ); + + JsonContent result = jsonUpdateCompilationRequest.write(dto); + + assertThat(result).extractingJsonPathArrayValue("$.events").containsExactly(300); + assertThat(result).extractingJsonPathBooleanValue("$.pinned").isEqualTo(true); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Обновленная подборка"); + } +} \ No newline at end of file diff --git a/main/src/test/java/ru/practicum/compilation/CompilationMapperTest.java b/main/src/test/java/ru/practicum/compilation/CompilationMapperTest.java new file mode 100644 index 0000000..4faea0c --- /dev/null +++ b/main/src/test/java/ru/practicum/compilation/CompilationMapperTest.java @@ -0,0 +1,77 @@ +package ru.practicum.compilation; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import ru.practicum.category.model.Category; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.mapper.CompilationMapper; +import ru.practicum.compilation.model.Compilation; +import ru.practicum.event.model.Event; +import ru.practicum.event.state.EventState; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + +public class CompilationMapperTest { + @Test + void shouldMapCompilationToCompilationDto() { + Category category = new Category(1L, "Сериал"); + + User user = new User(); + user.setId(10L); + user.setName("Макс Иванов"); + user.setEmail("Max@example.com"); + + Event event = new Event(); + event.setId(100L); + event.setCategory(category); + event.setInitiator(user); + event.setTitle("Друзья"); + event.setAnnotation("Аннотация"); + event.setDescription("Описание"); + event.setCreatedOn(LocalDateTime.now()); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setState(EventState.PUBLISHED); + + Set events = new HashSet<>(); + events.add(event); + + Compilation compilation = new Compilation( + 1L, + "Подборка сериалов", + true, + events + ); + + CompilationDto result = CompilationMapper.toCompilationDto(compilation); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Подборка сериалов"); + AssertionsForClassTypes.assertThat(result.getPinned()).isTrue(); + } + + @Test + void shouldMapNewCompilationDtoToCompilationPinnedTrue() { + NewCompilationDto dto = new NewCompilationDto(Set.of(), true, "Подборка сериалов"); + + Compilation result = CompilationMapper.toCompilation(dto); + + AssertionsForClassTypes.assertThat(result.getId()).isNull(); + AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Подборка сериалов"); + AssertionsForClassTypes.assertThat(result.getPinned()).isTrue(); + } + + @Test + void shouldMapNewCompilationDtoToCompilationPinnedNull() { + NewCompilationDto dto = new NewCompilationDto(Set.of(), null, "Подборка сериалов"); + + Compilation result = CompilationMapper.toCompilation(dto); + + AssertionsForClassTypes.assertThat(result.getId()).isNull(); + AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Подборка сериалов"); + AssertionsForClassTypes.assertThat(result.getPinned()).isFalse(); + } +} diff --git a/main/src/test/java/ru/practicum/compilation/CompilationPublicControllerTest.java b/main/src/test/java/ru/practicum/compilation/CompilationPublicControllerTest.java new file mode 100644 index 0000000..2783235 --- /dev/null +++ b/main/src/test/java/ru/practicum/compilation/CompilationPublicControllerTest.java @@ -0,0 +1,90 @@ +package ru.practicum.compilation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.compilation.controller.CompilationPublicController; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.service.CompilationService; +import ru.practicum.error.ErrorHandler; + +import java.util.List; +import java.util.Set; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +public class CompilationPublicControllerTest { + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private CompilationService compilationService; + + @InjectMocks + private CompilationPublicController compilationController; + + private MockMvc mvc; + + private CompilationDto compilationDto1; + private CompilationDto compilationDto2; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(compilationController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + compilationDto1 = new CompilationDto(1L, "Подборка сериалов", true, Set.of()); + compilationDto2 = new CompilationDto(2L, "Подборка фильмов", false, Set.of()); + } + + @Test + void mustReturnAllCompilations() throws Exception { + List compilations = List.of(compilationDto1, compilationDto2); + + when(compilationService.getCompilations(any(), anyInt(), anyInt())) + .thenReturn(compilations); + + mvc.perform(get("/compilations") + .param("from", "0") + .param("size", "10") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.size()").value(2)) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].title", is("Подборка сериалов"))) + .andExpect(jsonPath("$[0].pinned", is(true))) + .andExpect(jsonPath("$[1].id", is(2))) + .andExpect(jsonPath("$[1].title", is("Подборка фильмов"))) + .andExpect(jsonPath("$[1].pinned", is(false))); + } + + @Test + void mustReturnCompilationById() throws Exception { + when(compilationService.getCompilationById(1L)) + .thenReturn(compilationDto1); + + mvc.perform(get("/compilations/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.title", is("Подборка сериалов"))) + .andExpect(jsonPath("$.pinned", is(true))); + } +} diff --git a/main/src/test/java/ru/practicum/compilation/CompilationServiceImplTest.java b/main/src/test/java/ru/practicum/compilation/CompilationServiceImplTest.java new file mode 100644 index 0000000..f696f64 --- /dev/null +++ b/main/src/test/java/ru/practicum/compilation/CompilationServiceImplTest.java @@ -0,0 +1,152 @@ +package ru.practicum.compilation; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.MainServiceApp; +import ru.practicum.category.model.Category; +import ru.practicum.compilation.dto.CompilationDto; +import ru.practicum.compilation.dto.NewCompilationDto; +import ru.practicum.compilation.dto.UpdateCompilationRequest; +import ru.practicum.compilation.model.Compilation; +import ru.practicum.compilation.service.CompilationService; +import ru.practicum.event.model.Event; +import ru.practicum.event.state.EventState; +import ru.practicum.exception.NotFoundException; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Transactional +@ActiveProfiles("test") +@SpringBootTest(classes = MainServiceApp.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class CompilationServiceImplTest { + private final EntityManager em; + private final CompilationService compilationService; + + private Long compilationId; + private Long eventId; + + @BeforeEach + void setUp() { + Category category = new Category(); + category.setName("Сериалы"); + em.persist(category); + + User user = new User(); + user.setName("Макс Иванов"); + user.setEmail("Max@mail.com"); + em.persist(user); + + Event event = new Event(); + event.setTitle("Сериал Друзья"); + event.setAnnotation("Аннотация"); + event.setDescription("Описание"); + event.setCreatedOn(LocalDateTime.now()); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setState(EventState.PUBLISHED); + event.setCategory(category); + event.setInitiator(user); + + em.persist(event); + em.flush(); + + eventId = event.getId(); + + Compilation compilation = new Compilation(); + compilation.setTitle("Подборка сериалов"); + compilation.setPinned(true); + compilation.setEvents(Set.of(event)); + + em.persist(compilation); + em.flush(); + + compilationId = compilation.getId(); + } + + @Test + void mustReturnCompilationById() { + CompilationDto dto = compilationService.getCompilationById(compilationId); + + assertThat(dto.getId()).isEqualTo(compilationId); + assertThat(dto.getTitle()).isEqualTo("Подборка сериалов"); + assertThat(dto.getPinned()).isTrue(); + ; + } + + @Test + void shouldThrowNotFoundExceptionForInvalidId() { + assertThrows(NotFoundException.class, () -> compilationService.getCompilationById(999999L)); + } + + @Test + void mustCreateCompilation() { + NewCompilationDto newDto = new NewCompilationDto(Set.of(eventId), true, "Следующая подборка"); + + CompilationDto created = compilationService.createCompilation(newDto); + + assertThat(created.getId()).isNotNull(); + assertThat(created.getTitle()).isEqualTo("Следующая подборка"); + assertThat(created.getPinned()).isTrue(); + } + + @Test + void mustUpdateCompilation() { + UpdateCompilationRequest updateDto = new UpdateCompilationRequest(Set.of(eventId), false, "Обновленная подборка"); + + CompilationDto updated = compilationService.updateCompilation(compilationId, updateDto); + + assertThat(updated.getId()).isEqualTo(compilationId); + assertThat(updated.getTitle()).isEqualTo("Обновленная подборка"); + assertThat(updated.getPinned()).isFalse(); + } + + @Test + void mustUpdateCompilationWithNullRequest() { + CompilationDto updated = compilationService.updateCompilation(compilationId, null); + + assertThat(updated.getId()).isEqualTo(compilationId); + assertThat(updated.getTitle()).isEqualTo("Подборка сериалов"); + } + + @Test + void mustDeleteCompilation() { + compilationService.deleteCompilation(compilationId); + + assertThrows(NotFoundException.class, () -> compilationService.getCompilationById(compilationId)); + } + + @Test + void mustReturnPaginatedCompilations() { + List compilations = compilationService.getCompilations(null, 0, 10); + + assertThat(compilations).isNotEmpty(); + assertThat(compilations.get(0).getTitle()).isEqualTo("Подборка сериалов"); + } + + @Test + void mustReturnPinnedCompilationsOnly() { + List pinned = compilationService.getCompilations(true, 0, 10); + + assertThat(pinned).hasSize(1); + assertThat(pinned.get(0).getPinned()).isTrue(); + } + + @Test + void mustReturnEmptyListForUnpinnedFilter() { + List unpinned = compilationService.getCompilations(false, 0, 10); + + assertThat(unpinned).isEmpty(); + } +} diff --git a/main/src/test/java/ru/practicum/event/EventAdminControllerTest.java b/main/src/test/java/ru/practicum/event/EventAdminControllerTest.java new file mode 100644 index 0000000..3ce5b48 --- /dev/null +++ b/main/src/test/java/ru/practicum/event/EventAdminControllerTest.java @@ -0,0 +1,126 @@ +package ru.practicum.event; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.error.ErrorHandler; +import ru.practicum.event.controller.EventAdminController; +import ru.practicum.event.dto.EventFullDto; +import ru.practicum.event.dto.UpdateEventAdminRequest; +import ru.practicum.event.service.EventService; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.user.dto.UserShortDto; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +class EventAdminControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private EventService eventService; + + @InjectMocks + private EventAdminController eventAdminController; + + private MockMvc mvc; + + private EventFullDto eventFullDto; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(eventAdminController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + CategoryDto categoryDto = new CategoryDto(1L, "Категория"); + UserShortDto userShortDto = new UserShortDto(1L, "Макс Иванов"); + LocationDto locationDto = new LocationDto(50.01f, 31.25f); + + eventFullDto = new EventFullDto( + 1L, + "Аннотация", + categoryDto, + 5L, + LocalDateTime.now(), + "Описание", + LocalDateTime.now().plusDays(1), + userShortDto, + locationDto, + true, + 10, + LocalDateTime.now(), + true, + null, + "Заголовок", + 100L + ); + } + + @Test + void mustReturnAdminEvents() throws Exception { + List states = List.of("PUBLISHED"); + List users = List.of(1L, 2L); + List categories = List.of(1L, 2L); + + when(eventService.getAdminEvents( + eq(users), eq(states), eq(categories), + any(), any(), eq(0), eq(10) + )).thenReturn(List.of(eventFullDto)); + + mvc.perform(get("/admin/events") + .param("users", "1", "2") + .param("states", "PUBLISHED") + .param("categories", "1", "2") + .param("from", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(eventFullDto.getId())) + .andExpect(jsonPath("$[0].annotation").value(eventFullDto.getAnnotation())) + .andExpect(jsonPath("$[0].category.id").value(eventFullDto.getCategory().getId())) + .andExpect(jsonPath("$[0].category.name").value(eventFullDto.getCategory().getName())) + .andExpect(jsonPath("$[0].description").value(eventFullDto.getDescription())) + .andExpect(jsonPath("$[0].title").value(eventFullDto.getTitle())); + } + + @Test + void mustUpdateEventByAdmin() throws Exception { + UpdateEventAdminRequest updateRequest = new UpdateEventAdminRequest(); + updateRequest.setAnnotation("Обновленная аннотация"); + updateRequest.setDescription("Обновленное описание"); + updateRequest.setTitle("Обновленный заголовок"); + + when(eventService.updateEventByAdmin(eq(1L), any(UpdateEventAdminRequest.class))) + .thenReturn(eventFullDto); + + mvc.perform(patch("/admin/events/1") + .content(mapper.writeValueAsString(updateRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(eventFullDto.getId())) + .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation())) + .andExpect(jsonPath("$.description").value(eventFullDto.getDescription())) + .andExpect(jsonPath("$.title").value(eventFullDto.getTitle())); + } +} diff --git a/main/src/test/java/ru/practicum/event/EventDtoTest.java b/main/src/test/java/ru/practicum/event/EventDtoTest.java new file mode 100644 index 0000000..2cc2048 --- /dev/null +++ b/main/src/test/java/ru/practicum/event/EventDtoTest.java @@ -0,0 +1,151 @@ +package ru.practicum.event; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.MainServiceApp; +import ru.practicum.event.dto.*; +import ru.practicum.request.status.RequestStatus; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +@ContextConfiguration(classes = MainServiceApp.class) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class EventDtoTest { + + private final JacksonTester jsonEventFullDto; + private final JacksonTester jsonEventShortDto; + private final JacksonTester jsonNewEventDto; + private final JacksonTester jsonUpdateEventAdminRequest; + private final JacksonTester jsonUpdateEventUserRequest; + private final JacksonTester jsonEventRequestStatusUpdateRequest; + private final JacksonTester jsonEventRequestStatusUpdateResult; + + @Test + void shouldReturnEventFullDto() throws Exception { + EventFullDto dto = new EventFullDto( + 1L, "Аннотация", null, 5L, + LocalDateTime.of(2025, 5, 1, 12, 0, 0), "Описание", + LocalDateTime.of(2025, 5, 2, 12, 0, 0), null, null, + true, 10, LocalDateTime.of(2025, 5, 3, 12, 0, 0), + true, null, "Концерт в парке", 100L + ); + + JsonContent result = jsonEventFullDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.annotation").isEqualTo("Аннотация"); + assertThat(result).extractingJsonPathStringValue("$.description").isEqualTo("Описание"); + assertThat(result).extractingJsonPathStringValue("$.createdOn").isEqualTo("2025-05-01 12:00:00"); + assertThat(result).extractingJsonPathStringValue("$.eventDate").isEqualTo("2025-05-02 12:00:00"); + assertThat(result).extractingJsonPathStringValue("$.publishedOn").isEqualTo("2025-05-03 12:00:00"); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Концерт в парке"); + assertThat(result).extractingJsonPathNumberValue("$.confirmedRequests").isEqualTo(5); + assertThat(result).extractingJsonPathNumberValue("$.participantLimit").isEqualTo(10); + assertThat(result).extractingJsonPathNumberValue("$.views").isEqualTo(100); + assertThat(result).extractingJsonPathBooleanValue("$.paid").isTrue(); + assertThat(result).extractingJsonPathBooleanValue("$.requestModeration").isTrue(); + } + + @Test + void shouldReturnEventShortDto() throws Exception { + EventShortDto dto = new EventShortDto(1L, "Аннотация", null, 5L, + LocalDateTime.of(2025, 1, 2, 12, 0, 0), null, true, "Концерт в парке", 100L); + + JsonContent result = jsonEventShortDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.annotation").isEqualTo("Аннотация"); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Концерт в парке"); + assertThat(result).extractingJsonPathBooleanValue("$.paid").isTrue(); + assertThat(result).extractingJsonPathNumberValue("$.views").isEqualTo(100); + } + + @Test + void shouldReturnNewEventDto() throws Exception { + NewEventDto dto = new NewEventDto( + "Аннотация", + 1L, + "Описание", + LocalDateTime.of(2025, 5, 2, 12, 0, 0), + null, + true, + 10, + true, + "Концерт в парке" + ); + + JsonContent result = jsonNewEventDto.write(dto); + + assertThat(result).extractingJsonPathStringValue("$.annotation") + .isEqualTo("Аннотация"); + assertThat(result).extractingJsonPathStringValue("$.description") + .isEqualTo("Описание"); + assertThat(result).extractingJsonPathStringValue("$.title") + .isEqualTo("Концерт в парке"); + } + + @Test + void shouldReturnUpdateEventAdminRequest() throws Exception { + UpdateEventAdminRequest dto = new UpdateEventAdminRequest(); + dto.setAnnotation("Аннотация"); + dto.setDescription("Описание"); + dto.setTitle("Концерт в парке"); + + JsonContent result = jsonUpdateEventAdminRequest.write(dto); + + assertThat(result).extractingJsonPathStringValue("$.annotation") + .isEqualTo("Аннотация"); + assertThat(result).extractingJsonPathStringValue("$.description") + .isEqualTo("Описание"); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Концерт в парке"); + } + + @Test + void shouldReturnUpdateEventUserRequest() throws Exception { + UpdateEventUserRequest dto = new UpdateEventUserRequest(); + dto.setAnnotation("Аннотация"); + dto.setDescription("Описание"); + dto.setTitle("Концерт в парке"); + + JsonContent result = jsonUpdateEventUserRequest.write(dto); + + assertThat(result).extractingJsonPathStringValue("$.annotation") + .isEqualTo("Аннотация"); + assertThat(result).extractingJsonPathStringValue("$.description") + .isEqualTo("Описание"); + assertThat(result).extractingJsonPathStringValue("$.title").isEqualTo("Концерт в парке"); + } + + @Test + void shouldReturnEventRequestStatusUpdateRequest() throws Exception { + EventRequestStatusUpdateRequest dto = new EventRequestStatusUpdateRequest( + List.of(1L, 2L), RequestStatus.CONFIRMED + ); + + JsonContent result = jsonEventRequestStatusUpdateRequest.write(dto); + + assertThat(result).extractingJsonPathArrayValue("$.requestIds").containsExactly(1, 2); + assertThat(result).extractingJsonPathStringValue("$.status").isEqualTo("CONFIRMED"); + } + + @Test + void shouldReturnEventRequestStatusUpdateResult() throws Exception { + EventRequestStatusUpdateResult dto = new EventRequestStatusUpdateResult( + List.of(), List.of() + ); + + JsonContent result = jsonEventRequestStatusUpdateResult.write(dto); + + assertThat(result).extractingJsonPathArrayValue("$.confirmedRequests").isEmpty(); + assertThat(result).extractingJsonPathArrayValue("$.rejectedRequests").isEmpty(); + } +} diff --git a/main/src/test/java/ru/practicum/event/EventMapperTest.java b/main/src/test/java/ru/practicum/event/EventMapperTest.java new file mode 100644 index 0000000..428630d --- /dev/null +++ b/main/src/test/java/ru/practicum/event/EventMapperTest.java @@ -0,0 +1,114 @@ +package ru.practicum.event; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import ru.practicum.category.model.Category; +import ru.practicum.event.dto.EventFullDto; +import ru.practicum.event.dto.EventShortDto; +import ru.practicum.event.dto.NewEventDto; +import ru.practicum.event.dto.mapper.EventMapper; +import ru.practicum.event.model.Event; +import ru.practicum.event.state.EventState; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.location.model.Location; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; + +public class EventMapperTest { + @Test + void shouldMapEventToEventFullDto() { + Category category = new Category(1L, "Концерт"); + User initiator = new User(1L, "Макс Иванов", "Max@mail.ru"); + Location location = new Location(1L, 50.01f, 31.25f); + + Event event = new Event( + 1L, + "Аннотация", + category, + 5L, + LocalDateTime.of(2025, 5, 1, 12, 0), + "Описание", + LocalDateTime.of(2025, 5, 2, 12, 0), + initiator, + location, + true, + 10, + LocalDateTime.of(2025, 5, 3, 12, 0), + true, + EventState.PUBLISHED, + "Заголовок" + ); + + EventFullDto result = EventMapper.toEventFullDto(event); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getAnnotation()).isEqualTo("Аннотация"); + AssertionsForClassTypes.assertThat(result.getDescription()).isEqualTo("Описание"); + AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Заголовок"); + AssertionsForClassTypes.assertThat(result.getPaid()).isTrue(); + AssertionsForClassTypes.assertThat(result.getParticipantLimit()).isEqualTo(10); + AssertionsForClassTypes.assertThat(result.getConfirmedRequests()).isEqualTo(5L); + } + + @Test + void shouldMapEventToEventShortDto() { + Category category = new Category(1L, "Концерт"); + User initiator = new User(1L, "Макс Иванов", "Max@mail.ru"); + Location location = new Location(1L, 50.01f, 31.25f); + + Event event = new Event( + 1L, + "Аннотация", + category, + 5L, + LocalDateTime.of(2025, 5, 1, 12, 0), + "Описание", + LocalDateTime.of(2025, 5, 2, 12, 0), + initiator, + location, + true, + 10, + LocalDateTime.of(2025, 5, 3, 12, 0), + true, + EventState.PUBLISHED, + "Заголовок" + ); + + EventShortDto result = EventMapper.toEventShortDto(event); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getAnnotation()).isEqualTo("Аннотация"); + AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Заголовок"); + AssertionsForClassTypes.assertThat(result.getPaid()).isTrue(); + AssertionsForClassTypes.assertThat(result.getConfirmedRequests()).isEqualTo(5L); + } + + @Test + void shouldMapNewEventDtoToEvent() { + NewEventDto newEventDto = new NewEventDto( + "Аннотация", + 1L, + "Описание", + LocalDateTime.of(2025, 5, 2, 12, 0), + new LocationDto(50.01f, 31.25f), + true, + 10, + true, + "Заголовок" + ); + + Category category = new Category(1L, "Концерт"); + User initiator = new User(1L, "Макс Иванов", "Max@mail.ru"); + + Event result = EventMapper.toEvent(newEventDto, category, initiator); + + AssertionsForClassTypes.assertThat(result.getId()).isNull(); + AssertionsForClassTypes.assertThat(result.getAnnotation()).isEqualTo("Аннотация"); + AssertionsForClassTypes.assertThat(result.getDescription()).isEqualTo("Описание"); + AssertionsForClassTypes.assertThat(result.getTitle()).isEqualTo("Заголовок"); + AssertionsForClassTypes.assertThat(result.getPaid()).isTrue(); + AssertionsForClassTypes.assertThat(result.getParticipantLimit()).isEqualTo(10); + AssertionsForClassTypes.assertThat(result.getConfirmedRequests()).isEqualTo(0L); + } +} diff --git a/main/src/test/java/ru/practicum/event/EventPrivateControllerTest.java b/main/src/test/java/ru/practicum/event/EventPrivateControllerTest.java new file mode 100644 index 0000000..404d3f0 --- /dev/null +++ b/main/src/test/java/ru/practicum/event/EventPrivateControllerTest.java @@ -0,0 +1,248 @@ +package ru.practicum.event; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.error.ErrorHandler; +import ru.practicum.event.controller.EventPrivateController; +import ru.practicum.event.dto.*; +import ru.practicum.event.service.EventService; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.dto.UserShortDto; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ExtendWith(MockitoExtension.class) +class EventPrivateControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private EventService eventService; + + @InjectMocks + private EventPrivateController eventPrivateController; + + private MockMvc mvc; + + private EventFullDto eventFullDto; + private EventShortDto eventShortDto; + private NewEventDto newEventDto; + private UpdateEventUserRequest updateEventUserRequest; + private EventRequestStatusUpdateRequest updateRequest; + private EventRequestStatusUpdateResult updateResult; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(eventPrivateController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + CategoryDto categoryDto = new CategoryDto(1L, "Категория"); + UserShortDto userShortDto = new UserShortDto(1L, "Макс Иванов"); + LocationDto locationDto = new LocationDto(50.01f, 31.25f); + + eventFullDto = new EventFullDto(1L, + "Аннотация", + categoryDto, + 5L, + LocalDateTime.now(), + "Описание", + LocalDateTime.now().plusDays(1), + userShortDto, + locationDto, + true, + 10, + LocalDateTime.now(), + true, + null, + "Заголовок", + 100L + ); + + eventShortDto = new EventShortDto(1L, + "Аннотация", + null, + 5L, + LocalDateTime.now().plusDays(1), + null, + true, + "Заголовок", + 100L + ); + + newEventDto = new NewEventDto( + "Краткое описание, сжатый пересказ сути", + 1L, + "Наглядное представление о предмете", + LocalDateTime.now().plusDays(1), + locationDto, + true, + 10, + true, + "Заголовок" + ); + + updateEventUserRequest = new UpdateEventUserRequest( + "Обновленная аннотация", + 1L, + "Обновленное описание", + LocalDateTime.now().plusDays(2), + null, + true, + 15, + true, + null, + "Обновленный заголовок" + ); + + updateRequest = new EventRequestStatusUpdateRequest(); + updateRequest.setStatus(RequestStatus.CONFIRMED); + updateRequest.setRequestIds(List.of(1L, 2L)); + + updateResult = new EventRequestStatusUpdateResult( + List.of(new ParticipationRequestDto(1L, LocalDateTime.now(), 1L, 1L, RequestStatus.CONFIRMED)), + List.of() + ); + } + + @Test + void mustReturnEventsByUserId() throws Exception { + when(eventService.getEventsByUserId(eq(1L), anyInt(), anyInt())) + .thenReturn(List.of(eventShortDto)); + + mvc.perform(get("/users/1/events") + .param("from", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(eventShortDto.getId())) + .andExpect(jsonPath("$[0].annotation").value(eventShortDto.getAnnotation())) + .andExpect(jsonPath("$[0].category").doesNotExist()) + .andExpect(jsonPath("$[0].confirmedRequests").value(eventShortDto.getConfirmedRequests())) + .andExpect(jsonPath("$[0].eventDate").value(eventShortDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$[0].initiator").doesNotExist()) + .andExpect(jsonPath("$[0].paid").value(eventShortDto.getPaid())) + .andExpect(jsonPath("$[0].title").value(eventShortDto.getTitle())) + .andExpect(jsonPath("$[0].views").value(eventShortDto.getViews())); + } + + @Test + void mustCreateEvent() throws Exception { + when(eventService.createEvent(eq(1L), any(NewEventDto.class))) + .thenReturn(eventFullDto); + + mvc.perform(post("/users/1/events") + .content(mapper.writeValueAsString(newEventDto)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").value(eventFullDto.getId())) + .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation())) + .andExpect(jsonPath("$.category.id").value(eventFullDto.getCategory().getId())) + .andExpect(jsonPath("$.category.name").value(eventFullDto.getCategory().getName())) + .andExpect(jsonPath("$.confirmedRequests").value(eventFullDto.getConfirmedRequests())) + .andExpect(jsonPath("$.createdOn").value(eventFullDto.getCreatedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.description").value(eventFullDto.getDescription())) + .andExpect(jsonPath("$.eventDate").value(eventFullDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.initiator.id").value(eventFullDto.getInitiator().getId())) + .andExpect(jsonPath("$.initiator.name").value(eventFullDto.getInitiator().getName())) + .andExpect(jsonPath("$.location.lat").value(eventFullDto.getLocation().getLat())) + .andExpect(jsonPath("$.location.lon").value(eventFullDto.getLocation().getLon())) + .andExpect(jsonPath("$.paid").value(eventFullDto.getPaid())) + .andExpect(jsonPath("$.participantLimit").value(eventFullDto.getParticipantLimit())) + .andExpect(jsonPath("$.publishedOn").value(eventFullDto.getPublishedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.requestModeration").value(eventFullDto.getRequestModeration())) + .andExpect(jsonPath("$.state").doesNotExist()) + .andExpect(jsonPath("$.title").value(eventFullDto.getTitle())) + .andExpect(jsonPath("$.views").value(eventFullDto.getViews())); + } + + @Test + void mustReturnEventByUserIdAndEventId() throws Exception { + when(eventService.getEventByUserIdAndEventId(1L, 1L)) + .thenReturn(eventFullDto); + + mvc.perform(get("/users/1/events/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(eventFullDto.getId())) + .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation())) + .andExpect(jsonPath("$.category.id").value(eventFullDto.getCategory().getId())) + .andExpect(jsonPath("$.category.name").value(eventFullDto.getCategory().getName())) + .andExpect(jsonPath("$.confirmedRequests").value(eventFullDto.getConfirmedRequests())) + .andExpect(jsonPath("$.createdOn").value(eventFullDto.getCreatedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.description").value(eventFullDto.getDescription())) + .andExpect(jsonPath("$.eventDate").value(eventFullDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.initiator.id").value(eventFullDto.getInitiator().getId())) + .andExpect(jsonPath("$.initiator.name").value(eventFullDto.getInitiator().getName())) + .andExpect(jsonPath("$.location.lat").value(eventFullDto.getLocation().getLat())) + .andExpect(jsonPath("$.location.lon").value(eventFullDto.getLocation().getLon())) + .andExpect(jsonPath("$.paid").value(eventFullDto.getPaid())) + .andExpect(jsonPath("$.participantLimit").value(eventFullDto.getParticipantLimit())) + .andExpect(jsonPath("$.publishedOn").value(eventFullDto.getPublishedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.requestModeration").value(eventFullDto.getRequestModeration())) + .andExpect(jsonPath("$.state").doesNotExist()) + .andExpect(jsonPath("$.title").value(eventFullDto.getTitle())) + .andExpect(jsonPath("$.views").value(eventFullDto.getViews())); + } + + @Test + void mustUpdateEventByUserIdAndEventId() throws Exception { + when(eventService.updateEventByUserIdAndEventId(eq(1L), eq(1L), any(UpdateEventUserRequest.class))) + .thenReturn(eventFullDto); + + mvc.perform(patch("/users/1/events/1") + .content(mapper.writeValueAsString(updateEventUserRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(eventFullDto.getId())) + .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation())) + .andExpect(jsonPath("$.description").value(eventFullDto.getDescription())) + .andExpect(jsonPath("$.title").value(eventFullDto.getTitle())); + } + + @Test + void mustReturnEventRequests() throws Exception { + when(eventService.getEventRequestsByUserIdAndEventId(1L, 1L)) + .thenReturn(List.of()); + + mvc.perform(get("/users/1/events/1/requests")) + .andExpect(status().isOk()) + .andExpect(content().json("[]")); + } + + @Test + void mustChangeStatus() throws Exception { + when(eventService.changeStatus(eq(1L), eq(1L), any(EventRequestStatusUpdateRequest.class))) + .thenReturn(updateResult); + + mvc.perform(patch("/users/1/events/1/requests") + .content(mapper.writeValueAsString(updateRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.confirmedRequests[0].id").value(updateResult.getConfirmedRequests().get(0).getId())) + .andExpect(jsonPath("$.confirmedRequests[0].event").value(updateResult.getConfirmedRequests().get(0).getEvent())) + .andExpect(jsonPath("$.confirmedRequests[0].requester").value(updateResult.getConfirmedRequests().get(0).getRequester())) + .andExpect(jsonPath("$.confirmedRequests[0].status").value(updateResult.getConfirmedRequests().get(0).getStatus().toString())) + .andExpect(jsonPath("$.rejectedRequests").isEmpty()); + } +} diff --git a/main/src/test/java/ru/practicum/event/EventPublicControllerTest.java b/main/src/test/java/ru/practicum/event/EventPublicControllerTest.java new file mode 100644 index 0000000..775c92a --- /dev/null +++ b/main/src/test/java/ru/practicum/event/EventPublicControllerTest.java @@ -0,0 +1,154 @@ +package ru.practicum.event; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.BeforeEach; +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.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.error.ErrorHandler; +import ru.practicum.event.controller.EventPublicController; +import ru.practicum.event.dto.EventFullDto; +import ru.practicum.event.dto.EventShortDto; +import ru.practicum.event.service.EventService; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.user.dto.UserShortDto; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +class EventPublicControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private EventService eventService; + + @InjectMocks + private EventPublicController eventPublicController; + + private MockMvc mvc; + + private EventFullDto eventFullDto; + private EventShortDto eventShortDto; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + + mvc = MockMvcBuilders + .standaloneSetup(eventPublicController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + CategoryDto categoryDto = new CategoryDto(1L, "Категория"); + UserShortDto userShortDto = new UserShortDto(1L, "Макс Иванов"); + LocationDto locationDto = new LocationDto(50.01f, 31.25f); + + eventFullDto = new EventFullDto( + 1L, + "Аннотация", + categoryDto, + 10L, + LocalDateTime.now(), + "Описание", + LocalDateTime.now().plusDays(1), + userShortDto, + locationDto, + true, + 100, + LocalDateTime.now(), + true, + null, + "Заголовок", + 500L + ); + + eventShortDto = new EventShortDto( + 1L, + "Аннотация", + categoryDto, + 10L, + LocalDateTime.now().plusDays(1), + userShortDto, + true, + "Заголовок", + 500L + ); + } + + @Test + void mustReturnPublicEvents() throws Exception { + when(eventService.getEvents( + anyString(), anyList(), anyBoolean(), + any(), any(), + anyBoolean(), anyString(), + anyInt(), anyInt(), + any(HttpServletRequest.class) + )).thenReturn(List.of(eventShortDto)); + + mvc.perform(get("/events") + .param("text", "текст для поиска") + .param("categories", "1") + .param("paid", "true") + .param("rangeStart", "2025-05-22 12:00:00") + .param("rangeEnd", "2025-05-23 12:00:00") + .param("onlyAvailable", "true") + .param("sort", "EVENT_DATE") + .param("from", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(eventShortDto.getId())) + .andExpect(jsonPath("$[0].annotation").value(eventShortDto.getAnnotation())) + .andExpect(jsonPath("$[0].category.id").value(eventShortDto.getCategory().getId())) + .andExpect(jsonPath("$[0].category.name").value(eventShortDto.getCategory().getName())) + .andExpect(jsonPath("$[0].confirmedRequests").value(eventShortDto.getConfirmedRequests())) + .andExpect(jsonPath("$[0].eventDate").value(eventShortDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$[0].initiator.id").value(eventShortDto.getInitiator().getId())) + .andExpect(jsonPath("$[0].initiator.name").value(eventShortDto.getInitiator().getName())) + .andExpect(jsonPath("$[0].paid").value(eventShortDto.getPaid())) + .andExpect(jsonPath("$[0].title").value(eventShortDto.getTitle())) + .andExpect(jsonPath("$[0].views").value(eventShortDto.getViews())); + } + + @Test + void mustReturnPublicEventById() throws Exception { + when(eventService.getEventById(eq(1L), any(HttpServletRequest.class))) + .thenReturn(eventFullDto); + + mvc.perform(get("/events/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(eventFullDto.getId())) + .andExpect(jsonPath("$.annotation").value(eventFullDto.getAnnotation())) + .andExpect(jsonPath("$.category.id").value(eventFullDto.getCategory().getId())) + .andExpect(jsonPath("$.category.name").value(eventFullDto.getCategory().getName())) + .andExpect(jsonPath("$.confirmedRequests").value(eventFullDto.getConfirmedRequests())) + .andExpect(jsonPath("$.createdOn").value(eventFullDto.getCreatedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.description").value(eventFullDto.getDescription())) + .andExpect(jsonPath("$.eventDate").value(eventFullDto.getEventDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.initiator.id").value(eventFullDto.getInitiator().getId())) + .andExpect(jsonPath("$.initiator.name").value(eventFullDto.getInitiator().getName())) + .andExpect(jsonPath("$.location.lat").value(eventFullDto.getLocation().getLat())) + .andExpect(jsonPath("$.location.lon").value(eventFullDto.getLocation().getLon())) + .andExpect(jsonPath("$.paid").value(eventFullDto.getPaid())) + .andExpect(jsonPath("$.participantLimit").value(eventFullDto.getParticipantLimit())) + .andExpect(jsonPath("$.publishedOn").value(eventFullDto.getPublishedOn().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))) + .andExpect(jsonPath("$.requestModeration").value(eventFullDto.getRequestModeration())) + .andExpect(jsonPath("$.state").doesNotExist()) + .andExpect(jsonPath("$.title").value(eventFullDto.getTitle())) + .andExpect(jsonPath("$.views").value(eventFullDto.getViews())); + } +} diff --git a/main/src/test/java/ru/practicum/event/EventServiceImplTest.java b/main/src/test/java/ru/practicum/event/EventServiceImplTest.java new file mode 100644 index 0000000..3bcafb3 --- /dev/null +++ b/main/src/test/java/ru/practicum/event/EventServiceImplTest.java @@ -0,0 +1,332 @@ +package ru.practicum.event; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.MainServiceApp; +import ru.practicum.category.model.Category; +import ru.practicum.event.dto.*; +import ru.practicum.event.model.Event; +import ru.practicum.event.service.EventService; +import ru.practicum.event.state.EventState; +import ru.practicum.event.state.EventStateActionSolution; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.ValidationException; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.location.model.Location; +import ru.practicum.request.model.Request; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +@Transactional +@ActiveProfiles("test") +@SpringBootTest( + classes = MainServiceApp.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class EventServiceImplTest { + + private final EntityManager em; + private final EventService eventService; + private Long userId; + private Long categoryId; + private Long locationId; + + @BeforeEach + void setUp() { + User user = new User(); + user.setName("Макс Иванов"); + user.setEmail("Max@mail.com"); + em.persist(user); + + userId = user.getId(); + + Category category = new Category(); + category.setName("Кино"); + em.persist(category); + categoryId = category.getId(); + + Location location = new Location(); + location.setLat(50.01f); + location.setLon(31.25f); + em.persist(location); + locationId = location.getId(); + + em.flush(); + } + + @Test + void mustCreateEvent() { + NewEventDto dto = new NewEventDto( + "Аннотация", + categoryId, + "Описание", + LocalDateTime.now().plusDays(1), + new LocationDto(50.01f, 31.25f), + true, + 10, + true, + "Заголовок" + ); + + EventFullDto created = eventService.createEvent(userId, dto); + + assertThat(created.getId()).isNotNull(); + assertThat(created.getAnnotation()).isEqualTo(dto.getAnnotation()); + assertThat(created.getTitle()).isEqualTo(dto.getTitle()); + assertThat(created.getCategory().getId()).isEqualTo(categoryId); + } + + @Test + void mustGetEventByUserIdAndEventId() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(10); + event.setRequestModeration(true); + event.setState(EventState.PENDING); + event.setTitle("Заголовок"); + + em.persist(event); + em.flush(); + + EventFullDto found = eventService.getEventByUserIdAndEventId(userId, event.getId()); + + assertThat(found.getId()).isEqualTo(event.getId()); + assertThat(found.getAnnotation()).isEqualTo(event.getAnnotation()); + assertThat(found.getTitle()).isEqualTo(event.getTitle()); + } + + @Test + void mustUpdateEventByUser() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(10); + event.setRequestModeration(true); + event.setState(EventState.PENDING); + event.setTitle("Заголовок"); + + em.persist(event); + em.flush(); + + UpdateEventUserRequest updateDto = new UpdateEventUserRequest(); + updateDto.setAnnotation("Обновленная аннотация"); + updateDto.setTitle("Обновленный заголовок"); + + EventFullDto updated = eventService.updateEventByUserIdAndEventId(userId, event.getId(), updateDto); + + assertThat(updated.getAnnotation()).isEqualTo("Обновленная аннотация"); + assertThat(updated.getTitle()).isEqualTo("Обновленный заголовок"); + } + + @Test + void shouldThrowValidationExceptionForInvalidEventDate() { + NewEventDto dto = new NewEventDto( + "Аннотация", + categoryId, + "Описание", + LocalDateTime.now().plusMinutes(30), + new LocationDto(50.01f, 31.25f), + true, + 10, + true, + "Заголовок" + ); + + Assertions.assertThrows(ValidationException.class, () -> eventService.createEvent(userId, dto)); + } + + @Test + void mustReturnEventsByUserId() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(10); + event.setRequestModeration(true); + event.setState(EventState.PENDING); + event.setTitle("Заголовок"); + + em.persist(event); + em.flush(); + + List events = eventService.getEventsByUserId(userId, 0, 10); + + assertThat(events).isNotEmpty(); + assertThat(events.get(0).getId()).isEqualTo(event.getId()); + } + + @Test + void mustReturnAdminEvents() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(10); + event.setRequestModeration(true); + event.setState(EventState.PENDING); + event.setTitle("Заголовок"); + + em.persist(event); + em.flush(); + + List events = eventService.getAdminEvents( + List.of(userId), + List.of("PENDING"), + List.of(categoryId), + null, + null, + 0, + 10 + ); + + assertThat(events).hasSize(1); + EventFullDto dto = events.get(0); + assertThat(dto.getId()).isEqualTo(event.getId()); + assertThat(dto.getTitle()).isEqualTo("Заголовок"); + assertThat(dto.getCategory().getId()).isEqualTo(categoryId); + } + + @Test + void mustUpdateEventByAdmin() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(10); + event.setRequestModeration(true); + event.setState(EventState.PENDING); + event.setTitle("Заголовок"); + + em.persist(event); + em.flush(); + + UpdateEventAdminRequest updateRequest = new UpdateEventAdminRequest(); + updateRequest.setTitle("Обновленный заголовок"); + updateRequest.setAnnotation("Обновленная аннотация"); + updateRequest.setStateAction(EventStateActionSolution.PUBLISH_EVENT); + + EventFullDto updated = eventService.updateEventByAdmin(event.getId(), updateRequest); + + assertThat(updated.getTitle()).isEqualTo("Обновленный заголовок"); + assertThat(updated.getAnnotation()).isEqualTo("Обновленная аннотация"); + assertThat(updated.getState()).isEqualTo(EventState.PUBLISHED); + } + + @Test + void shouldThrowConflictExceptionWhenPublishingWrongState() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(10); + event.setRequestModeration(true); + event.setState(EventState.PUBLISHED); + event.setTitle("Заголовок"); + + em.persist(event); + em.flush(); + + UpdateEventAdminRequest updateRequest = new UpdateEventAdminRequest(); + updateRequest.setStateAction(EventStateActionSolution.PUBLISH_EVENT); + + Assertions.assertThrows(ConflictException.class, + () -> eventService.updateEventByAdmin(event.getId(), updateRequest)); + } + + @Test + void mustChangeRequestStatus() { + Event event = new Event(); + event.setAnnotation("Аннотация"); + event.setCategory(em.find(Category.class, categoryId)); + event.setCreatedOn(LocalDateTime.now()); + event.setDescription("Описание"); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setInitiator(em.find(User.class, userId)); + event.setLocation(em.find(Location.class, locationId)); + event.setPaid(true); + event.setParticipantLimit(1); + event.setRequestModeration(true); + event.setState(EventState.PENDING); + event.setTitle("Заголовок"); + + em.persist(event); + User requester1 = new User(); + requester1.setName("Макс Иванов"); + requester1.setEmail("Max@mail.ru"); + em.persist(requester1); + + User requester2 = new User(); + requester2.setName("Ирина Дубинина"); + requester2.setEmail("Iren@mail.ru"); + em.persist(requester2); + + Request request1 = new Request(); + request1.setEvent(event); + request1.setRequester(requester1); + request1.setStatus(RequestStatus.PENDING); + + Request request2 = new Request(); + request2.setEvent(event); + request2.setRequester(requester2); + request2.setStatus(RequestStatus.PENDING); + + em.persist(request1); + em.persist(request2); + em.flush(); + + EventRequestStatusUpdateRequest statusRequest = new EventRequestStatusUpdateRequest(); + statusRequest.setRequestIds(List.of(request1.getId(), request2.getId())); + statusRequest.setStatus(RequestStatus.CONFIRMED); + + EventRequestStatusUpdateResult result = eventService.changeStatus(event.getInitiator().getId(), event.getId(), statusRequest); + + assertThat(result.getConfirmedRequests()).hasSize(1); + assertThat(result.getRejectedRequests()).hasSize(1); + } +} \ No newline at end of file diff --git a/main/src/test/java/ru/practicum/location/LocationDtoTest.java b/main/src/test/java/ru/practicum/location/LocationDtoTest.java new file mode 100644 index 0000000..26b411f --- /dev/null +++ b/main/src/test/java/ru/practicum/location/LocationDtoTest.java @@ -0,0 +1,37 @@ +package ru.practicum.location; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import ru.practicum.location.dto.LocationDto; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@JsonTest +class LocationDtoTest { + + @Autowired + private JacksonTester jsonLocationDto; + + @Test + void shouldSerializeLocationDto() throws Exception { + LocationDto dto = new LocationDto(50.01f, 31.25f); + + JsonContent result = jsonLocationDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.lat").isEqualTo(50.01); + assertThat(result).extractingJsonPathNumberValue("$.lon").isEqualTo(31.25); + } + + @Test + void shouldDeserializeLocationDto() throws Exception { + String json = "{\"lat\": 50.01, \"lon\": 31.25}"; + + LocationDto dto = jsonLocationDto.parseObject(json); + + assertThat(dto.getLat()).isEqualTo(50.01f); + assertThat(dto.getLon()).isEqualTo(31.25f); + } +} diff --git a/main/src/test/java/ru/practicum/location/LocationMapperTest.java b/main/src/test/java/ru/practicum/location/LocationMapperTest.java new file mode 100644 index 0000000..230dccd --- /dev/null +++ b/main/src/test/java/ru/practicum/location/LocationMapperTest.java @@ -0,0 +1,31 @@ +package ru.practicum.location; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import ru.practicum.location.dto.LocationDto; +import ru.practicum.location.dto.mapper.LocationMapper; +import ru.practicum.location.model.Location; + +class LocationMapperTest { + + @Test + void shouldLocationToLocationDto() { + Location location = new Location(1L, 50.01f, 31.25f); + + LocationDto result = LocationMapper.toLocationDto(location); + + AssertionsForClassTypes.assertThat(result.getLat()).isEqualTo(50.01f); + AssertionsForClassTypes.assertThat(result.getLon()).isEqualTo(31.25f); + } + + @Test + void shouldLocationDtoToLocation() { + LocationDto dto = new LocationDto(50.01f, 31.25f); + + Location result = LocationMapper.toLocation(dto); + + AssertionsForClassTypes.assertThat(result.getId()).isNull(); + AssertionsForClassTypes.assertThat(result.getLat()).isEqualTo(50.01f); + AssertionsForClassTypes.assertThat(result.getLon()).isEqualTo(31.25f); + } +} diff --git a/main/src/test/java/ru/practicum/request/RequestDtoTest.java b/main/src/test/java/ru/practicum/request/RequestDtoTest.java new file mode 100644 index 0000000..4315154 --- /dev/null +++ b/main/src/test/java/ru/practicum/request/RequestDtoTest.java @@ -0,0 +1,41 @@ +package ru.practicum.request; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.status.RequestStatus; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@JsonTest +class RequestDtoTest { + + @Autowired + private JacksonTester jsonParticipationRequestDto; + + @Test + void shouldReturnParticipationRequestDto() throws Exception { + LocalDateTime created = LocalDateTime.of(2025, 1, 1, 12, 30, 45); + ParticipationRequestDto dto = new ParticipationRequestDto( + 10L, + created, + 1L, + 5L, + RequestStatus.CONFIRMED + ); + + JsonContent result = jsonParticipationRequestDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(10); + assertThat(result).extractingJsonPathStringValue("$.created") + .isEqualTo("2025-01-01 12:30:45"); + assertThat(result).extractingJsonPathNumberValue("$.event").isEqualTo(1); + assertThat(result).extractingJsonPathNumberValue("$.requester").isEqualTo(5); + assertThat(result).extractingJsonPathStringValue("$.status").isEqualTo("CONFIRMED"); + } +} diff --git a/main/src/test/java/ru/practicum/request/RequestMapperTest.java b/main/src/test/java/ru/practicum/request/RequestMapperTest.java new file mode 100644 index 0000000..610372f --- /dev/null +++ b/main/src/test/java/ru/practicum/request/RequestMapperTest.java @@ -0,0 +1,39 @@ +package ru.practicum.request; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import ru.practicum.event.model.Event; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.dto.mapper.RequestMapper; +import ru.practicum.request.model.Request; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; + +public class RequestMapperTest { + @Test + void shouldRequestToParticipationRequestDto() { + LocalDateTime created = LocalDateTime.of(2025, 1, 1, 12, 0, 0); + + User requester = new User(2L, "Max@mail.com", "Макс Иванов"); + Event event = new Event(); + event.setId(3L); + + Request request = new Request( + 1L, + created, + event, + requester, + RequestStatus.PENDING + ); + + ParticipationRequestDto result = RequestMapper.toParticipationRequestDto(request); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getCreated()).isEqualTo(created); + AssertionsForClassTypes.assertThat(result.getEvent()).isEqualTo(3L); + AssertionsForClassTypes.assertThat(result.getRequester()).isEqualTo(2L); + AssertionsForClassTypes.assertThat(result.getStatus()).isEqualTo(RequestStatus.PENDING); + } +} diff --git a/main/src/test/java/ru/practicum/request/RequestPrivateControllerTest.java b/main/src/test/java/ru/practicum/request/RequestPrivateControllerTest.java new file mode 100644 index 0000000..1967711 --- /dev/null +++ b/main/src/test/java/ru/practicum/request/RequestPrivateControllerTest.java @@ -0,0 +1,104 @@ +package ru.practicum.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.error.ErrorHandler; +import ru.practicum.request.controller.RequestPrivateController; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.service.RequestService; +import ru.practicum.request.status.RequestStatus; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +public class RequestPrivateControllerTest { + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private RequestService requestService; + + @InjectMocks + private RequestPrivateController requestPrivateController; + + private MockMvc mvc; + private ParticipationRequestDto requestDto; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + mvc = MockMvcBuilders + .standaloneSetup(requestPrivateController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + requestDto = new ParticipationRequestDto( + 1L, + LocalDateTime.of(2025, 5, 1, 12, 0, 0), + 1L, + 5L, + RequestStatus.PENDING + ); + } + + @Test + void mustGetRequestsByUserId() throws Exception { + when(requestService.getRequestsByUserId(anyLong())) + .thenReturn(List.of(requestDto)); + + mvc.perform(get("/users/5/requests") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].event", is(1))) + .andExpect(jsonPath("$[0].requester", is(5))) + .andExpect(jsonPath("$[0].status", is("PENDING"))) + .andExpect(jsonPath("$[0].created", is("2025-05-01 12:00:00"))); + } + + @Test + void mustCreateRequest() throws Exception { + when(requestService.createRequest(anyLong(), anyLong())) + .thenReturn(requestDto); + + mvc.perform(post("/users/5/requests") + .param("eventId", "100") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.event", is(1))) + .andExpect(jsonPath("$.requester", is(5))) + .andExpect(jsonPath("$.status", is("PENDING"))) + .andExpect(jsonPath("$.created", is("2025-05-01 12:00:00"))); + } + + @Test + void mustCancelRequest() throws Exception { + when(requestService.cancelRequest(anyLong(), anyLong())) + .thenReturn(requestDto); + + mvc.perform(patch("/users/5/requests/1/cancel") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.event", is(1))) + .andExpect(jsonPath("$.requester", is(5))) + .andExpect(jsonPath("$.status", is("PENDING"))) + .andExpect(jsonPath("$.created", is("2025-05-01 12:00:00"))); + } +} diff --git a/main/src/test/java/ru/practicum/request/RequestServiceImplTest.java b/main/src/test/java/ru/practicum/request/RequestServiceImplTest.java new file mode 100644 index 0000000..9b30016 --- /dev/null +++ b/main/src/test/java/ru/practicum/request/RequestServiceImplTest.java @@ -0,0 +1,140 @@ +package ru.practicum.request; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.MainServiceApp; +import ru.practicum.category.model.Category; +import ru.practicum.event.model.Event; +import ru.practicum.event.state.EventState; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; +import ru.practicum.location.model.Location; +import ru.practicum.request.dto.ParticipationRequestDto; +import ru.practicum.request.service.RequestService; +import ru.practicum.request.status.RequestStatus; +import ru.practicum.user.model.User; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Transactional +@ActiveProfiles("test") +@SpringBootTest(classes = MainServiceApp.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class RequestServiceImplTest { + private final EntityManager em; + private final RequestService requestService; + + private User user; + private User initiator; + private Category category; + private Location location; + private Event event; + + @BeforeEach + void setUp() { + category = new Category(); + category.setName("Концерт"); + em.persist(category); + + location = new Location(); + location.setLat(50.01f); + location.setLon(31.25f); + em.persist(location); + + user = new User(); + user.setName("Макс Иванов"); + user.setEmail("Max@mail.com"); + em.persist(user); + + initiator = new User(); + initiator.setName("Ирина Дубова"); + initiator.setEmail("Iren@mail.com"); + em.persist(initiator); + + event = new Event(); + event.setTitle("Концерт в парке"); + event.setAnnotation("Аннотация"); + event.setDescription("Описание"); + event.setCategory(category); + event.setLocation(location); + event.setInitiator(initiator); + event.setCreatedOn(LocalDateTime.now()); + event.setEventDate(LocalDateTime.now().plusDays(1)); + event.setState(EventState.PUBLISHED); + event.setParticipantLimit(1); + em.persist(event); + + em.flush(); + } + + @Test + void mustCreateRequest() { + ParticipationRequestDto requestDto = requestService.createRequest(user.getId(), event.getId()); + + assertThat(requestDto.getId()).isNotNull(); + assertThat(requestDto.getEvent()).isEqualTo(event.getId()); + assertThat(requestDto.getRequester()).isEqualTo(user.getId()); + assertThat(requestDto.getStatus()).isEqualTo(RequestStatus.PENDING); + } + + @Test + void shouldThrowConflictWhenUserIsInitiator() { + assertThrows(ConflictException.class, + () -> requestService.createRequest(initiator.getId(), event.getId())); + } + + @Test + void shouldThrowConflictWhenEventNotPublished() { + event.setState(EventState.PENDING); + em.flush(); + + assertThrows(ConflictException.class, + () -> requestService.createRequest(user.getId(), event.getId())); + } + + @Test + void shouldThrowConflictWhenDuplicateRequest() { + requestService.createRequest(user.getId(), event.getId()); + + assertThrows(ConflictException.class, + () -> requestService.createRequest(user.getId(), event.getId())); + } + + @Test + void mustReturnRequestsByUserId() { + ParticipationRequestDto requestDto = requestService.createRequest(user.getId(), event.getId()); + + List requests = requestService.getRequestsByUserId(user.getId()); + + assertThat(requests).hasSize(1); + assertThat(requests.get(0).getId()).isEqualTo(requestDto.getId()); + } + + @Test + void mustCancelRequest() { + ParticipationRequestDto requestDto = requestService.createRequest(user.getId(), event.getId()); + + ParticipationRequestDto canceled = requestService.cancelRequest(user.getId(), requestDto.getId()); + + assertThat(canceled.getStatus()).isEqualTo(RequestStatus.CANCELED); + } + + @Test + void shouldThrowNotFoundWhenCancelingOtherUserRequest() { + ParticipationRequestDto requestDto = requestService.createRequest(user.getId(), event.getId()); + + assertThrows(NotFoundException.class, + () -> requestService.cancelRequest(initiator.getId(), requestDto.getId())); + } +} + diff --git a/main/src/test/java/ru/practicum/user/UserAdminControllerTest.java b/main/src/test/java/ru/practicum/user/UserAdminControllerTest.java new file mode 100644 index 0000000..2198ef4 --- /dev/null +++ b/main/src/test/java/ru/practicum/user/UserAdminControllerTest.java @@ -0,0 +1,96 @@ +package ru.practicum.user; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import ru.practicum.error.ErrorHandler; +import ru.practicum.user.controller.UserAdminController; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; +import ru.practicum.user.service.UserService; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +class UserAdminControllerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Mock + private UserService userService; + + @InjectMocks + private UserAdminController userAdminController; + + private MockMvc mvc; + + private UserDto userDto; + private NewUserRequest newUserRequest; + + @BeforeEach + void setUp() { + mapper.findAndRegisterModules(); + mvc = MockMvcBuilders + .standaloneSetup(userAdminController) + .setControllerAdvice(new ErrorHandler()) + .build(); + + userDto = new UserDto(1L, "Max@mail.com", "Макс"); + newUserRequest = new NewUserRequest("Max@mail.com", "Макс"); + } + + @Test + void mustGetUsers() throws Exception { + when(userService.getUsers(null, 0, 10)) + .thenReturn(List.of(userDto)); + + mvc.perform(get("/admin/users") + .param("from", "0") + .param("size", "10") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].email", is("Max@mail.com"))) + .andExpect(jsonPath("$[0].name", is("Макс"))); + } + + @Test + void mustCreateUser() throws Exception { + when(userService.createUser(any())) + .thenReturn(userDto); + + mvc.perform(post("/admin/users") + .content(mapper.writeValueAsString(newUserRequest)) + .characterEncoding(StandardCharsets.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.email", is("Max@mail.com"))) + .andExpect(jsonPath("$.name", is("Макс"))); + } + + @Test + void mustDeleteUser() throws Exception { + mvc.perform(delete("/admin/users/1")) + .andExpect(status().isNoContent()); + + verify(userService).deleteUser(1L); + } +} diff --git a/main/src/test/java/ru/practicum/user/UserDtoTest.java b/main/src/test/java/ru/practicum/user/UserDtoTest.java new file mode 100644 index 0000000..f07959b --- /dev/null +++ b/main/src/test/java/ru/practicum/user/UserDtoTest.java @@ -0,0 +1,56 @@ +package ru.practicum.user; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.json.JsonContent; +import org.springframework.test.context.ContextConfiguration; +import ru.practicum.MainServiceApp; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; +import ru.practicum.user.dto.UserShortDto; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@ContextConfiguration(classes = MainServiceApp.class) +@JsonTest +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class UserDtoTest { + + private final JacksonTester jsonNewUserRequest; + private final JacksonTester jsonUserDto; + private final JacksonTester jsonUserShortDto; + + @Test + void shouldReturnNewUserRequest() throws Exception { + NewUserRequest dto = new NewUserRequest("Max@mail.com", "Макс"); + + JsonContent result = jsonNewUserRequest.write(dto); + + assertThat(result).extractingJsonPathStringValue("$.email").isEqualTo("Max@mail.com"); + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Макс"); + } + + @Test + void shouldReturnUserDto() throws Exception { + UserDto dto = new UserDto(1L, "Max@mail.com", "Макс"); + + JsonContent result = jsonUserDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(1); + assertThat(result).extractingJsonPathStringValue("$.email").isEqualTo("Max@mail.com"); + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Макс"); + } + + @Test + void shouldReturnUserShortDto() throws Exception { + UserShortDto dto = new UserShortDto(2L, "Макс"); + + JsonContent result = jsonUserShortDto.write(dto); + + assertThat(result).extractingJsonPathNumberValue("$.id").isEqualTo(2); + assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Макс"); + } +} diff --git a/main/src/test/java/ru/practicum/user/UserMapperTest.java b/main/src/test/java/ru/practicum/user/UserMapperTest.java new file mode 100644 index 0000000..c5fb454 --- /dev/null +++ b/main/src/test/java/ru/practicum/user/UserMapperTest.java @@ -0,0 +1,44 @@ +package ru.practicum.user; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.Test; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; +import ru.practicum.user.dto.UserShortDto; +import ru.practicum.user.dto.mapper.UserMapper; +import ru.practicum.user.model.User; + +class UserMapperTest { + + @Test + void shouldMapUserToUserDto() { + User user = new User(1L, "Max@mail.com", "Макс"); + + UserDto result = UserMapper.toUserDto(user); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(1L); + AssertionsForClassTypes.assertThat(result.getEmail()).isEqualTo("Max@mail.com"); + AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Макс"); + } + + @Test + void shouldMapNewUserRequestToUser() { + NewUserRequest newUserRequest = new NewUserRequest("Max@mail.com", "Макс"); + + User result = UserMapper.toUser(newUserRequest); + + AssertionsForClassTypes.assertThat(result.getId()).isNull(); + AssertionsForClassTypes.assertThat(result.getEmail()).isEqualTo("Max@mail.com"); + AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Макс"); + } + + @Test + void shouldMapUserToUserShortDto() { + User user = new User(2L, "Max@mail.com", "Макс"); + + UserShortDto result = UserMapper.toUserShortDto(user); + + AssertionsForClassTypes.assertThat(result.getId()).isEqualTo(2L); + AssertionsForClassTypes.assertThat(result.getName()).isEqualTo("Макс"); + } +} \ No newline at end of file diff --git a/main/src/test/java/ru/practicum/user/UserServiceImplTest.java b/main/src/test/java/ru/practicum/user/UserServiceImplTest.java new file mode 100644 index 0000000..b06caba --- /dev/null +++ b/main/src/test/java/ru/practicum/user/UserServiceImplTest.java @@ -0,0 +1,99 @@ +package ru.practicum.user; + +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.MainServiceApp; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; +import ru.practicum.user.dto.NewUserRequest; +import ru.practicum.user.dto.UserDto; +import ru.practicum.user.model.User; +import ru.practicum.user.service.UserService; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Transactional +@ActiveProfiles("test") +@SpringBootTest( + classes = MainServiceApp.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +class UserServiceImplTest { + + @Autowired + private EntityManager em; + + @Autowired + private UserService userService; + + private Long userId; + + @BeforeEach + void setUp() { + User user = new User(); + user.setEmail("Max@mail.com"); + user.setName("Макс"); + + em.persist(user); + em.flush(); + + userId = user.getId(); + } + + @Test + void mustCreateUser() { + NewUserRequest newUser = new NewUserRequest("Petr@mail.com", "Петр"); + + UserDto created = userService.createUser(newUser); + + assertThat(created.getId()).isNotNull(); + assertThat(created.getEmail()).isEqualTo("Petr@mail.com"); + assertThat(created.getName()).isEqualTo("Петр"); + } + + @Test + void mustThrowConflictExceptionWhenCreatingDuplicateUser() { + NewUserRequest duplicate = new NewUserRequest("Max@mail.com", "Макс"); + + assertThrows(ConflictException.class, () -> userService.createUser(duplicate)); + } + + @Test + void mustReturnUsers() { + List users = userService.getUsers(null, 0, 10); + + assertThat(users.get(0).getId()).isEqualTo(userId); + assertThat(users.get(0).getEmail()).isEqualTo("Max@mail.com"); + } + + @Test + void mustDeleteUser() { + userService.deleteUser(userId); + + assertThrows(NotFoundException.class, () -> userService.deleteUser(userId)); + } + + @Test + void shouldThrowNotFoundExceptionWhenDeletingNonExistingUser() { + assertThrows(NotFoundException.class, () -> userService.deleteUser(10000L)); + } + + @Test + void mustReturnPaginatedUsers() { + User u2 = new User(); + u2.setEmail("second@mail.com"); + u2.setName("Евгений"); + em.persist(u2); + em.flush(); + + List users = userService.getUsers(null, 0, 1); + assertThat(users.get(0).getEmail()).isEqualTo("Max@mail.com"); + } +} diff --git a/main/src/test/java/ru/practicum/user/UserTest.java b/main/src/test/java/ru/practicum/user/UserTest.java new file mode 100644 index 0000000..c9c482f --- /dev/null +++ b/main/src/test/java/ru/practicum/user/UserTest.java @@ -0,0 +1,28 @@ +package ru.practicum.user; + +import org.junit.jupiter.api.Test; +import ru.practicum.user.model.User; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +class UserTest { + + @Test + void testEqualsAndHashCode() { + User user1 = new User(1L, "test@mail.com", "Макс"); + User user2 = new User(2L, "test@mail.com", "Петр"); + User user3 = new User(3L, "other@mail.com", "Макс"); + + assertThat(user1).isEqualTo(user1); + assertThat(user1.hashCode()).isEqualTo(user1.hashCode()); + + assertThat(user1).isEqualTo(user2); + assertThat(user1.hashCode()).isEqualTo(user2.hashCode()); + + assertThat(user1).isNotEqualTo(user3); + assertThat(user1.hashCode()).isNotEqualTo(user3.hashCode()); + + assertThat(user1).isNotEqualTo(null); + assertThat(user1).isNotEqualTo("string"); + } +} diff --git a/stats/client/pom.xml b/stats/client/pom.xml index 3ef2b52..b947d24 100644 --- a/stats/client/pom.xml +++ b/stats/client/pom.xml @@ -35,5 +35,11 @@ org.springframework.boot spring-boot-starter-actuator + + + org.projectlombok + lombok + true + \ No newline at end of file diff --git a/stats/client/src/main/java/ru/practicum/StatsClient.java b/stats/client/src/main/java/ru/practicum/StatsClient.java index 96c7c34..4f6b564 100644 --- a/stats/client/src/main/java/ru/practicum/StatsClient.java +++ b/stats/client/src/main/java/ru/practicum/StatsClient.java @@ -3,17 +3,23 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.stereotype.Service; import org.springframework.web.util.DefaultUriBuilderFactory; -import java.util.HashMap; +import java.time.format.DateTimeFormatter; +import java.util.Collections; import java.util.List; import java.util.Map; @Service public class StatsClient extends BaseClient { + private String uri; + @Autowired public StatsClient(@Value("${client.url}") String serviceUrl, RestTemplateBuilder builder) { super( @@ -22,21 +28,37 @@ public StatsClient(@Value("${client.url}") String serviceUrl, RestTemplateBuilde .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) .build() ); + uri = serviceUrl; } public ResponseEntity createHit(StatsRequestDto statsRequestDto) { return post("/hit", statsRequestDto); } - public ResponseEntity getStats(String start, String end, List uris, Boolean unique) { - Map parameters = new HashMap<>(); - parameters.put("start", start); - parameters.put("end", end); - if (uris != null && !uris.isEmpty()) { - parameters.put("uris", String.join(",", uris)); - } - parameters.put("unique", unique != null ? unique : false); + public List getStats(StatsViewRequestDto statsViewRequestDto) { + String urisCsv = String.join(",", statsViewRequestDto.getUris()); + + final String url = uri + "/stats?start={start}&end={end}&uris={uris}&unique={unique}"; - return get("/stats", parameters); + try { + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + Map.of( + "start", statsViewRequestDto.getStart().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), + "end", statsViewRequestDto.getEnd().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), + "uris", urisCsv, + "unique", statsViewRequestDto.getUnique() + ) + ); + + List stats = response.getBody(); + return stats != null ? stats : Collections.emptyList(); + } catch (Exception e) { + return Collections.emptyList(); + } } -} +} \ No newline at end of file diff --git a/stats/dto/src/main/java/ru/practicum/StatsViewRequestDto.java b/stats/dto/src/main/java/ru/practicum/StatsViewRequestDto.java new file mode 100644 index 0000000..be2757c --- /dev/null +++ b/stats/dto/src/main/java/ru/practicum/StatsViewRequestDto.java @@ -0,0 +1,21 @@ +package ru.practicum; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class StatsViewRequestDto { + private List uris; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime start; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime end; + private Boolean unique; +} diff --git a/stats/server/src/main/java/ru/practicum/error/ApiError.java b/stats/server/src/main/java/ru/practicum/error/ApiError.java new file mode 100644 index 0000000..327ff47 --- /dev/null +++ b/stats/server/src/main/java/ru/practicum/error/ApiError.java @@ -0,0 +1,19 @@ +package ru.practicum.error; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@AllArgsConstructor +@Getter +public class ApiError { + private final List errors; + private final String message; + private final String reason; + private final String status; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private final LocalDateTime timestamp; +} diff --git a/stats/server/src/main/java/ru/practicum/error/ErrorHandler.java b/stats/server/src/main/java/ru/practicum/error/ErrorHandler.java new file mode 100644 index 0000000..d277ca0 --- /dev/null +++ b/stats/server/src/main/java/ru/practicum/error/ErrorHandler.java @@ -0,0 +1,63 @@ +package ru.practicum.error; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.practicum.exception.ValidationException; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.stream.Collectors; + +@Slf4j +@RestControllerAdvice +public class ErrorHandler { + @ExceptionHandler({MethodArgumentNotValidException.class, + MissingServletRequestParameterException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(fe -> String.format("Field: %s. Error: %s. Value: %s", + fe.getField(), + fe.getDefaultMessage(), + fe.getRejectedValue() != null ? fe.getRejectedValue().toString() : "null")) + .collect(Collectors.joining("; ")); + + return new ApiError( + Collections.emptyList(), + message, + "Incorrectly made request.", + "BAD_REQUEST", + LocalDateTime.now() + ); + } + + @ExceptionHandler(ValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleValidationException(ValidationException e) { + return new ApiError( + Collections.emptyList(), + e.getMessage(), + "Incorrectly made request.", + "BAD_REQUEST", + LocalDateTime.now() + ); + } + + @ExceptionHandler(Throwable.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiError handleThrowable(Throwable e) { + log.error("Неожиданная ошибка: ", e); + return new ApiError( + Collections.emptyList(), + "Произошла непредвиденная ошибка.", + "Internal server error.", + "INTERNAL_SERVER_ERROR", + LocalDateTime.now() + ); + } +} diff --git a/stats/server/src/main/java/ru/practicum/repository/StatsRepository.java b/stats/server/src/main/java/ru/practicum/repository/StatsRepository.java index ebaa56e..04647de 100644 --- a/stats/server/src/main/java/ru/practicum/repository/StatsRepository.java +++ b/stats/server/src/main/java/ru/practicum/repository/StatsRepository.java @@ -2,8 +2,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import ru.practicum.view.StatsView; import ru.practicum.model.Stats; +import ru.practicum.view.StatsView; import java.time.LocalDateTime; import java.util.List; diff --git a/stats/server/src/main/java/ru/practicum/service/StatsServiceImpl.java b/stats/server/src/main/java/ru/practicum/service/StatsServiceImpl.java index 11fecb7..e3a940a 100644 --- a/stats/server/src/main/java/ru/practicum/service/StatsServiceImpl.java +++ b/stats/server/src/main/java/ru/practicum/service/StatsServiceImpl.java @@ -3,12 +3,13 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.practicum.StatsRequestDto; import ru.practicum.StatsResponseDto; -import ru.practicum.view.StatsView; import ru.practicum.exception.ValidationException; import ru.practicum.mapper.StatsMapper; import ru.practicum.repository.StatsRepository; +import ru.practicum.view.StatsView; import java.time.LocalDateTime; import java.util.List; @@ -19,11 +20,13 @@ public class StatsServiceImpl implements StatsService { private final StatsRepository statsRepository; + @Transactional @Override public StatsRequestDto createHit(StatsRequestDto statsRequestDto) { return StatsMapper.toStatsDto(statsRepository.save(StatsMapper.toStats(statsRequestDto))); } + @Transactional(readOnly = true) @Override public List getStats(LocalDateTime start, LocalDateTime end, diff --git a/stats/server/src/main/resources/application.yml b/stats/server/src/main/resources/application.yml index aeb5141..e493d28 100644 --- a/stats/server/src/main/resources/application.yml +++ b/stats/server/src/main/resources/application.yml @@ -1,7 +1,8 @@ spring: datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://localhost:6541/statsdb +# url: jdbc:postgresql://localhost:6541/statsdb + url: jdbc:postgresql://stats-db:6541/statsdb username: dbuser password: 12345 jpa: