feat: Initial commit
This commit is contained in:
36
README.md
Normal file
36
README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# 🏢 OfficeSpace - Приложение для бронирования рабочих мест
|
||||
|
||||
### Эндпоинты сервера
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|-------|------|-----------|
|
||||
| `GET` | `api/{code}/auth` | Проверка авторизации |
|
||||
| `GET` | `api/{code}/info` | Получение информации о пользователе |
|
||||
| `GET` | `api/{code}/booking` | Получение доступных для бронирования мест |
|
||||
| `POST` | `api/{code}/book` | Создание нового бронирования |
|
||||
|
||||
### Примеры ответов
|
||||
|
||||
**Информация о пользователе:**
|
||||
```json
|
||||
{
|
||||
"name": "Иванов Петр Федорович",
|
||||
"photoUrl": "https://example.com/photo.jpg",
|
||||
"booking": {
|
||||
"2025-01-05": {"id": 1, "place": "102"},
|
||||
"2025-01-06": {"id": 2, "place": "209.13"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Доступные для бронирования места:**
|
||||
```json
|
||||
{
|
||||
"2025-01-05": [
|
||||
{"id": 1, "place": "102"},
|
||||
{"id": 2, "place": "209.13"}
|
||||
]
|
||||
}
|
||||
```
|
||||
**Swagger-ui:**
|
||||
http://localhost:8080/swagger-ui/index.html#/
|
||||
61
pom.xml
Normal file
61
pom.xml
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.example</groupId>
|
||||
<artifactId>NTO-2025-Backend-Team-Task</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.5</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.8.8</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
11
src/main/java/com/example/nto/App.java
Normal file
11
src/main/java/com/example/nto/App.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.example.nto;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class App {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(App.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.example.nto.controller;
|
||||
|
||||
import com.example.nto.controller.dto.BookingCreateDto;
|
||||
import com.example.nto.controller.dto.PlaceDto;
|
||||
import com.example.nto.service.BookingService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("api")
|
||||
@RequiredArgsConstructor
|
||||
public class BookingController {
|
||||
|
||||
private final BookingService bookingService;
|
||||
|
||||
@GetMapping("/{code}/booking")
|
||||
@ResponseStatus(code = HttpStatus.OK)
|
||||
public Map<LocalDate, List<PlaceDto>> getByDate(@PathVariable String code) {
|
||||
return bookingService.getFreePlace(code);
|
||||
}
|
||||
|
||||
@PostMapping("/{code}/book")
|
||||
@ResponseStatus(code = HttpStatus.CREATED)
|
||||
public void create(@PathVariable String code, @RequestBody BookingCreateDto bookingCreateDto) {
|
||||
bookingService.create(code, bookingCreateDto);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.example.nto.controller;
|
||||
|
||||
|
||||
import com.example.nto.controller.dto.EmployeeDto;
|
||||
import com.example.nto.service.EmployeeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api")
|
||||
@RequiredArgsConstructor
|
||||
public class EmployeeController {
|
||||
|
||||
private final EmployeeService employeeService;
|
||||
|
||||
@GetMapping("/{code}/auth")
|
||||
@ResponseStatus(code = HttpStatus.OK)
|
||||
public void login(@PathVariable String code) {
|
||||
employeeService.auth(code);
|
||||
}
|
||||
|
||||
@GetMapping("/{code}/info")
|
||||
@ResponseStatus(code = HttpStatus.OK)
|
||||
public EmployeeDto getByCode(@PathVariable String code) {
|
||||
return employeeService.getByCode(code);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.nto.controller.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BookingCreateDto {
|
||||
@NotNull
|
||||
private LocalDate date;
|
||||
@Positive
|
||||
private long placeId;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.nto.controller.dto;
|
||||
|
||||
import com.example.nto.entity.Booking;
|
||||
import com.example.nto.entity.Employee;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class EmployeeDto {
|
||||
private String name;
|
||||
private String photoUrl;
|
||||
private Map<LocalDate, PlaceDto> booking;
|
||||
|
||||
public static EmployeeDto toDto(Employee employee) {
|
||||
Map<LocalDate, PlaceDto> dtoTreeMap = new TreeMap<>();
|
||||
for (Booking booking : employee.getBookingList()) {
|
||||
dtoTreeMap.put(booking.getDate(), PlaceDto.toDto(booking.getPlace()));
|
||||
}
|
||||
|
||||
return new EmployeeDto(employee.getName(), employee.getPhotoUrl(), dtoTreeMap);
|
||||
}
|
||||
}
|
||||
20
src/main/java/com/example/nto/controller/dto/PlaceDto.java
Normal file
20
src/main/java/com/example/nto/controller/dto/PlaceDto.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.example.nto.controller.dto;
|
||||
|
||||
import com.example.nto.entity.Place;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PlaceDto {
|
||||
private long id;
|
||||
private String place;
|
||||
|
||||
public static PlaceDto toDto(Place place) {
|
||||
return new PlaceDto(place.getId(), place.getPlace());
|
||||
}
|
||||
}
|
||||
33
src/main/java/com/example/nto/entity/Booking.java
Normal file
33
src/main/java/com/example/nto/entity/Booking.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.example.nto.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Table(name = "booking")
|
||||
public class Booking {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private long id;
|
||||
|
||||
@Column(name = "date")
|
||||
private LocalDate date;
|
||||
|
||||
@ManyToOne(targetEntity = Place.class, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "place_id")
|
||||
private Place place;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "employee_id")
|
||||
private Employee employee;
|
||||
}
|
||||
34
src/main/java/com/example/nto/entity/Employee.java
Normal file
34
src/main/java/com/example/nto/entity/Employee.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package com.example.nto.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Table(name = "employee")
|
||||
public class Employee {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private long id;
|
||||
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
|
||||
@Column(name = "code")
|
||||
private String code;
|
||||
|
||||
@Column(name = "photo_url")
|
||||
private String photoUrl;
|
||||
|
||||
@OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private List<Booking> bookingList;
|
||||
}
|
||||
23
src/main/java/com/example/nto/entity/Place.java
Normal file
23
src/main/java/com/example/nto/entity/Place.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.example.nto.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Table(name = "place")
|
||||
public class Place {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private long id;
|
||||
|
||||
@Column(name = "place_name")
|
||||
private String place;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.nto.exception;
|
||||
|
||||
public class BookingAlreadyExistsException extends RuntimeException {
|
||||
public BookingAlreadyExistsException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.nto.exception;
|
||||
|
||||
public class EmployeeNotFoundException extends RuntimeException {
|
||||
public EmployeeNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.nto.exception;
|
||||
|
||||
public class PlaceNotFoundException extends RuntimeException {
|
||||
public PlaceNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.example.nto.exception.handler;
|
||||
|
||||
import com.example.nto.exception.BookingAlreadyExistsException;
|
||||
import com.example.nto.exception.EmployeeNotFoundException;
|
||||
import com.example.nto.exception.PlaceNotFoundException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
|
||||
@ControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(EmployeeNotFoundException.class)
|
||||
public ResponseEntity<String> handleEmployeeNotFoundException(EmployeeNotFoundException e) {
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@ExceptionHandler(BookingAlreadyExistsException.class)
|
||||
public ResponseEntity<String> handleBookingAlreadyExistsException(BookingAlreadyExistsException e) {
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
@ExceptionHandler(PlaceNotFoundException.class)
|
||||
public ResponseEntity<String> handlePlaceNotFoundException(PlaceNotFoundException e) {
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<String> handleGenericException(Exception e) {
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.example.nto.repository;
|
||||
|
||||
import com.example.nto.entity.Booking;
|
||||
import com.example.nto.entity.Employee;
|
||||
import com.example.nto.entity.Place;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface BookingRepository extends JpaRepository<Booking, Long> {
|
||||
List<Booking> findByDateBetween(LocalDate start, LocalDate end);
|
||||
|
||||
Optional<Booking> findByDateAndPlace(LocalDate date, Place place);
|
||||
|
||||
Optional<Booking> findByDateAndEmployee(LocalDate date, Employee employee);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.example.nto.repository;
|
||||
|
||||
import com.example.nto.entity.Employee;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
|
||||
@EntityGraph(attributePaths = {"bookingList", "bookingList.place"})
|
||||
Optional<Employee> findByCode(String code);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.example.nto.repository;
|
||||
|
||||
import com.example.nto.entity.Place;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface PlaceRepository extends JpaRepository<Place, Long> {
|
||||
}
|
||||
15
src/main/java/com/example/nto/service/BookingService.java
Normal file
15
src/main/java/com/example/nto/service/BookingService.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.example.nto.service;
|
||||
|
||||
import com.example.nto.controller.dto.BookingCreateDto;
|
||||
import com.example.nto.controller.dto.PlaceDto;
|
||||
import com.example.nto.entity.Booking;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface BookingService {
|
||||
Map<LocalDate, List<PlaceDto>> getFreePlace(String code);
|
||||
|
||||
Booking create(String code, BookingCreateDto bookingCreateDto);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.nto.service;
|
||||
|
||||
import com.example.nto.controller.dto.EmployeeDto;
|
||||
|
||||
public interface EmployeeService {
|
||||
EmployeeDto getByCode(String code);
|
||||
|
||||
void auth(String code);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.example.nto.service.impl;
|
||||
|
||||
import com.example.nto.controller.dto.BookingCreateDto;
|
||||
import com.example.nto.controller.dto.PlaceDto;
|
||||
import com.example.nto.entity.Booking;
|
||||
import com.example.nto.entity.Employee;
|
||||
import com.example.nto.entity.Place;
|
||||
import com.example.nto.exception.BookingAlreadyExistsException;
|
||||
import com.example.nto.exception.EmployeeNotFoundException;
|
||||
import com.example.nto.exception.PlaceNotFoundException;
|
||||
import com.example.nto.repository.BookingRepository;
|
||||
import com.example.nto.repository.EmployeeRepository;
|
||||
import com.example.nto.repository.PlaceRepository;
|
||||
import com.example.nto.service.BookingService;
|
||||
import com.example.nto.service.EmployeeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BookingServiceImpl implements BookingService {
|
||||
|
||||
private final BookingRepository bookingRepository;
|
||||
private final EmployeeRepository employeeRepository;
|
||||
private final PlaceRepository placeRepository;
|
||||
private final EmployeeService employeeService;
|
||||
|
||||
@Value("${booking.days-ahead}")
|
||||
private int daysAhead;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Map<LocalDate, List<PlaceDto>> getFreePlace(String code) {
|
||||
employeeService.auth(code);
|
||||
|
||||
List<Place> allPlaces = placeRepository.findAll();
|
||||
|
||||
LocalDate today = LocalDate.now(ZoneId.systemDefault());
|
||||
LocalDate end = today.plusDays(daysAhead);
|
||||
|
||||
List<Booking> bookings = bookingRepository.findByDateBetween(today, end);
|
||||
|
||||
Map<LocalDate, Set<Long>> busyByDate = bookings.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
Booking::getDate,
|
||||
Collectors.mapping(b -> b.getPlace().getId(), Collectors.toSet())
|
||||
));
|
||||
|
||||
Map<LocalDate, List<PlaceDto>> result = new LinkedHashMap<>();
|
||||
|
||||
for (int i = 0; i <= daysAhead; i++) {
|
||||
LocalDate currentDate = today.plusDays(i);
|
||||
Set<Long> busyPlaces = busyByDate.getOrDefault(currentDate, Collections.emptySet());
|
||||
|
||||
List<PlaceDto> freePlaces = allPlaces.stream()
|
||||
.filter(place -> !busyPlaces.contains(place.getId()))
|
||||
.map(place -> new PlaceDto(place.getId(), place.getPlace()))
|
||||
.toList();
|
||||
|
||||
result.put(currentDate, freePlaces);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Booking create(String code, BookingCreateDto bookingCreateDto) {
|
||||
LocalDate date = bookingCreateDto.getDate();
|
||||
LocalDate today = LocalDate.now(ZoneId.systemDefault());
|
||||
if (date.isBefore(today) || date.isAfter(today.plusDays(daysAhead))) {
|
||||
throw new IllegalArgumentException("Date is out of booking window");
|
||||
}
|
||||
|
||||
Employee employee = employeeRepository.findByCode(code)
|
||||
.orElseThrow(() -> new EmployeeNotFoundException("Employee with " + code + " code not found!"));
|
||||
|
||||
long placeId = bookingCreateDto.getPlaceId();
|
||||
Place place = placeRepository.findById(placeId)
|
||||
.orElseThrow(() -> new PlaceNotFoundException("Place with " + placeId + " id not found!"));
|
||||
|
||||
if (bookingRepository.findByDateAndPlace(date, place).isPresent()) {
|
||||
throw new BookingAlreadyExistsException("Booking already exists");
|
||||
}
|
||||
|
||||
if (bookingRepository.findByDateAndEmployee(date, employee).isPresent()) {
|
||||
throw new BookingAlreadyExistsException("This employee already has another booking on " + date);
|
||||
}
|
||||
|
||||
Booking booking = Booking.builder()
|
||||
.date(date)
|
||||
.employee(employee)
|
||||
.place(place)
|
||||
.build();
|
||||
|
||||
return bookingRepository.save(booking);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.nto.service.impl;
|
||||
|
||||
import com.example.nto.controller.dto.EmployeeDto;
|
||||
import com.example.nto.exception.EmployeeNotFoundException;
|
||||
import com.example.nto.repository.EmployeeRepository;
|
||||
import com.example.nto.service.EmployeeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EmployeeServiceImpl implements EmployeeService {
|
||||
|
||||
private final EmployeeRepository employeeRepository;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public EmployeeDto getByCode(String code) {
|
||||
return employeeRepository.findByCode(code).map(EmployeeDto::toDto)
|
||||
.orElseThrow(() -> new EmployeeNotFoundException("Employee with " + code + " code not found!"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public void auth(String code) {
|
||||
if (employeeRepository.findByCode(code).isEmpty()) {
|
||||
throw new EmployeeNotFoundException("Employee with " + code + " code not found!");
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/main/resources/application-test.yml
Normal file
22
src/main/resources/application-test.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb
|
||||
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
|
||||
jpa:
|
||||
generate-ddl: false
|
||||
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
|
||||
show-sql: true
|
||||
|
||||
liquibase:
|
||||
enabled: true
|
||||
change-log: classpath:db.changelog/db.changelog-test-master.xml
|
||||
|
||||
booking:
|
||||
days-ahead: 3
|
||||
22
src/main/resources/application.yml
Normal file
22
src/main/resources/application.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb
|
||||
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
|
||||
jpa:
|
||||
generate-ddl: false
|
||||
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
|
||||
show-sql: true
|
||||
|
||||
liquibase:
|
||||
enabled: true
|
||||
change-log: classpath:db.changelog/db.changelog-master.xml
|
||||
|
||||
booking:
|
||||
days-ahead: 3
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="2025-11-05--0001-employee" author="anepretimov">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="employee"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
|
||||
<createTable tableName="employee">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
|
||||
<column name="name" type="VARCHAR(100)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
|
||||
<column name="code" type="VARCHAR(100)">
|
||||
<constraints nullable="false" unique="true"/>
|
||||
</column>
|
||||
|
||||
<column name="photo_url" type="VARCHAR(100)"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="2025-11-05--0002-place" author="anepretimov">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="place"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
|
||||
<createTable tableName="place">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
|
||||
<column name="place_name" type="VARCHAR(100)">
|
||||
<constraints nullable="false" unique="true"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="2025-11-05--0003-booking" author="anepretimov">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="booking"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
|
||||
<createTable tableName="booking">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
|
||||
<column name="date" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
|
||||
<column name="employee_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_booking_employee" referencedTableName="employee"
|
||||
referencedColumnNames="id"/>
|
||||
</column>
|
||||
|
||||
<column name="place_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_booking_place" referencedTableName="place"
|
||||
referencedColumnNames="id"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="2025-11-05--0001-employee-data" author="anepretimov">
|
||||
<loadData tableName="employee" file="db.changelog/data/csv/2025-11-05--0001-employee-data.csv"
|
||||
separator=";"
|
||||
quotchar='"'
|
||||
encoding="UTF-8"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="2025-11-05--0002-place-data" author="anepretimov">
|
||||
<loadData tableName="place" file="db.changelog/data/csv/2025-11-05--0002-place-data.csv"
|
||||
separator=";"
|
||||
quotchar='"'
|
||||
encoding="UTF-8"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="2025-11-05--0003-booking-data" author="anepretimov">
|
||||
<loadData tableName="booking" file="db.changelog/data/csv/2025-11-05--0003-booking-data.csv"
|
||||
separator=";"
|
||||
quotchar='"'
|
||||
encoding="UTF-8"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,5 @@
|
||||
name;code;photo_url
|
||||
Ivanov Ivan;1111;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
|
||||
Petrov Petr;2222;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
|
||||
Kozlov Oleg;3333;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
|
||||
Smirnova Anna;4444;https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg
|
||||
|
@@ -0,0 +1,4 @@
|
||||
place_name
|
||||
K-19
|
||||
M-16
|
||||
T-1
|
||||
|
@@ -0,0 +1,3 @@
|
||||
date;place_id;employee_id
|
||||
2025-11-08;1;1
|
||||
2025-11-10;2;2
|
||||
|
14
src/main/resources/db.changelog/db.changelog-master.xml
Normal file
14
src/main/resources/db.changelog/db.changelog-master.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
|
||||
|
||||
<include file="db.changelog/1/0/2025-11-05--0001-employee.xml"/>
|
||||
<include file="db.changelog/1/0/2025-11-05--0002-place.xml"/>
|
||||
<include file="db.changelog/1/0/2025-11-05--0003-booking.xml"/>
|
||||
|
||||
<include file="db.changelog/data/2025-11-05--0001-employee-data.xml"/>
|
||||
<include file="db.changelog/data/2025-11-05--0002-place-data.xml"/>
|
||||
<include file="db.changelog/data/2025-11-05--0003-booking-data.xml"/>
|
||||
</databaseChangeLog>
|
||||
14
src/main/resources/db.changelog/db.changelog-test-master.xml
Normal file
14
src/main/resources/db.changelog/db.changelog-test-master.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
|
||||
|
||||
<include file="db.changelog/1/0/2025-11-05--0001-employee.xml"/>
|
||||
<include file="db.changelog/1/0/2025-11-05--0002-place.xml"/>
|
||||
<include file="db.changelog/1/0/2025-11-05--0003-booking.xml"/>
|
||||
|
||||
<include file="db.changelog/test-data/test-employee-data.xml" context="test"/>
|
||||
<include file="db.changelog/test-data/test-place-data.xml" context="test"/>
|
||||
<include file="db.changelog/test-data/test-booking-data.xml" context="test"/>
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,3 @@
|
||||
date;place_id;employee_id
|
||||
2025-11-08;1;1
|
||||
2025-11-09;2;2
|
||||
|
@@ -0,0 +1,3 @@
|
||||
name;code;photo_url
|
||||
Test User 1;2104asd;https://example.org/u1.jpg
|
||||
Test User 2;qwe1206;https://example.org/u2.jpg
|
||||
|
@@ -0,0 +1,4 @@
|
||||
place_name
|
||||
T-01
|
||||
T-02
|
||||
T-03
|
||||
|
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="test-0003-booking-data" author="anepretimov">
|
||||
<loadData tableName="booking" file="db.changelog/test-data/csv/test-booking-data.csv"
|
||||
separator=";" quotchar='"' encoding="UTF-8"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="test-0001-employee-data" author="anepretimov">
|
||||
<loadData tableName="employee" file="db.changelog/test-data/csv/test-employee-data.csv"
|
||||
separator=";" quotchar='"' encoding="UTF-8"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet id="test-0002-place-data" author="anepretimov">
|
||||
<loadData tableName="place" file="db.changelog/test-data/csv/test-place-data.csv"
|
||||
separator=";" quotchar='"' encoding="UTF-8"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.example.nto.intagration;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.annotation.Rollback;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@Rollback
|
||||
@Transactional
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class BookingControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
ObjectMapper objectMapper;
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
@DisplayName("GET /api/{code}/booking -> 200 и карта свободных мест на N дней вперёд")
|
||||
void getFreePlaces_ok_whenCodeValid() throws Exception {
|
||||
String today = java.time.LocalDate.now().toString();
|
||||
|
||||
mockMvc.perform(get("/api/{code}/booking", "2104asd")
|
||||
.accept(org.springframework.http.MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentTypeCompatibleWith(org.springframework.http.MediaType.APPLICATION_JSON))
|
||||
// мапа дат -> список доступных мест
|
||||
.andExpect(jsonPath("$").isMap())
|
||||
// размер мапы строго равен daysAhead + 1 (включая сегодня)
|
||||
.andExpect(jsonPath("$", aMapWithSize(3 + 1)))
|
||||
// на сегодняшнюю дату возвращается массив из трёх мест
|
||||
.andExpect(jsonPath("$['" + today + "']").isArray())
|
||||
.andExpect(jsonPath("$['" + today + "']", hasSize(3)))
|
||||
// проверяем состав мест без привязки к порядку
|
||||
.andExpect(jsonPath("$['" + today + "'][*].place",
|
||||
containsInAnyOrder("T-01", "T-02", "T-03")))
|
||||
.andExpect(jsonPath("$['" + today + "'][*].id",
|
||||
containsInAnyOrder(1, 2, 3)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
@DisplayName("GET /api/{code}/booking -> 401 при неверном коде")
|
||||
void getFreePlaces_unauthorized_whenCodeInvalid() throws Exception {
|
||||
mockMvc.perform(get("/api/{code}/booking", "0000"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
@DisplayName("POST /api/{code}/book -> 201 при успешном бронировании свободного места")
|
||||
void book_ok_createsBooking() throws Exception {
|
||||
LocalDate date = LocalDate.now();
|
||||
Map<String, Object> body = Map.of(
|
||||
"date", date.toString(),
|
||||
"placeId", 2
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/{code}/book", "2104asd")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(body)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(content().string(isEmptyString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
@DisplayName("POST /api/{code}/book -> 409 если место на дату уже занято")
|
||||
void book_conflict_whenPlaceAlreadyBooked() throws Exception {
|
||||
LocalDate date = LocalDate.now();
|
||||
Map<String, Object> body = Map.of(
|
||||
"date", date.toString(),
|
||||
"placeId", 2
|
||||
);
|
||||
mockMvc.perform(post("/api/{code}/book", "2104asd")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(body)));
|
||||
|
||||
|
||||
mockMvc.perform(post("/api/{code}/book", "qwe1206")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(body)))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
@DisplayName("POST /api/{code}/book -> 409 если у сотрудника на указанную дату уже забронировано другое место")
|
||||
void book_conflict_whenAlreadyHasAnotherBooking() throws Exception {
|
||||
LocalDate date = LocalDate.now();
|
||||
Map<String, Object> body = Map.of(
|
||||
"date", date.toString(),
|
||||
"placeId", 2
|
||||
);
|
||||
mockMvc.perform(post("/api/{code}/book", "2104asd")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(body)));
|
||||
|
||||
|
||||
body = Map.of(
|
||||
"date", date.toString(),
|
||||
"placeId", 1
|
||||
);
|
||||
mockMvc.perform(post("/api/{code}/book", "2104asd")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(body)))
|
||||
.andExpect(status().isConflict());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
@DisplayName("POST /api/{code}/book -> 400 если указан несуществующий placeId")
|
||||
void book_badRequest_whenPlaceNotFound() throws Exception {
|
||||
LocalDate anyDate = LocalDate.now().plusDays(1);
|
||||
Map<String, Object> body = Map.of(
|
||||
"date", anyDate.toString(),
|
||||
"placeId", 999_999
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/{code}/book", "2104asd")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(body)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(7)
|
||||
@DisplayName("POST /api/{code}/book -> 401 при неверном коде")
|
||||
void book_unauthorized_whenCodeInvalid() throws Exception {
|
||||
LocalDate anyDate = LocalDate.now().plusDays(1);
|
||||
Map<String, Object> body = Map.of(
|
||||
"date", anyDate.toString(),
|
||||
"placeId", 1
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/{code}/book", "0000")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(body)))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.example.nto.intagration;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.hamcrest.Matchers.aMapWithSize;
|
||||
import static org.hamcrest.Matchers.isEmptyString;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class EmployeeControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/{code}/auth -> 200 при существующем коде")
|
||||
void auth_ok_whenCodeExists() throws Exception {
|
||||
mockMvc.perform(get("/api/{code}/auth", "2104asd"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(isEmptyString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/{code}/auth -> 401 при несуществующем коде")
|
||||
void auth_unauthorized_whenCodeNotExists() throws Exception {
|
||||
mockMvc.perform(get("/api/{code}/auth", "9999"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/{code}/info -> 200 и корректный JSON профиля с бронированиями")
|
||||
void info_ok_returnsEmployeeDto() throws Exception {
|
||||
mockMvc.perform(get("/api/{code}/info", "qwe1206")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.name").value("Test User 2"))
|
||||
.andExpect(jsonPath("$.photoUrl").value("https://example.org/u2.jpg"))
|
||||
.andExpect(jsonPath("$.booking").isMap())
|
||||
|
||||
// есть ключ 2025-11-09 и это именно объект PlaceDto, НЕ массив
|
||||
.andExpect(jsonPath("$.booking['2025-11-09']").isMap())
|
||||
.andExpect(jsonPath("$.booking['2025-11-09'].id").value(2))
|
||||
.andExpect(jsonPath("$.booking['2025-11-09'].place").value("T-02"))
|
||||
|
||||
// других дат нет и размер мапы = 1
|
||||
.andExpect(jsonPath("$.booking['2025-11-08']").doesNotExist())
|
||||
.andExpect(jsonPath("$.booking", aMapWithSize(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/{code}/info -> 401 при неверном коде")
|
||||
void info_unauthorized_whenCodeInvalid() throws Exception {
|
||||
mockMvc.perform(get("/api/{code}/info", "0000"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user