1
0

Initial commit

This commit is contained in:
Владимир Шперлинг 2024-10-30 01:50:45 +07:00
commit 9daa2517e9
16 changed files with 627 additions and 0 deletions

32
.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

57
pom.xml Normal file
View File

@ -0,0 +1,57 @@
<?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-2024</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</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>2.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.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version>
</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>
</dependencies>
</project>

View 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);
}
}

View File

@ -0,0 +1,34 @@
package com.example.nto.controller;
import com.example.nto.entity.Code;
import com.example.nto.entity.Employee;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import com.example.nto.service.EmployeeService;
@RestController
@RequestMapping("api")
@RequiredArgsConstructor
public class EmployeeController {
private final EmployeeService employeeService;
@GetMapping("/{login}/auth")
@ResponseStatus(code = HttpStatus.OK)
public void login(@PathVariable String login) {
employeeService.getByLogin(login);
}
@GetMapping("/{login}/info")
@ResponseStatus(HttpStatus.OK)
public Employee getByLogin(@PathVariable String login) {
return employeeService.getByLogin(login);
}
@PatchMapping("/{login}/open")
@ResponseStatus(HttpStatus.OK)
public void updateLastVisitByLogin(@PathVariable String login, @RequestBody Code code) {
employeeService.updateLastVisitByLogin(login, code);
}
}

View File

@ -0,0 +1,25 @@
package com.example.nto.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "code")
public class Code {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "value")
private long value;
}

View File

@ -0,0 +1,37 @@
package com.example.nto.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "employee")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "login")
private String login;
@Column(name = "name")
private String name;
@Column(name = "photo")
private String photo;
@Column(name = "position")
private String position;
@Column(name = "last_visit")
private LocalDateTime lastVisit;
}

View File

@ -0,0 +1,7 @@
package com.example.nto.exception;
public class CodeNotFoundException extends RuntimeException {
public CodeNotFoundException(String msg) {
super(msg);
}
}

View File

@ -0,0 +1,7 @@
package com.example.nto.exception;
public class EmployeeNotFoundException extends RuntimeException {
public EmployeeNotFoundException(String msg) {
super(msg);
}
}

View File

@ -0,0 +1,19 @@
package com.example.nto.exception;
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(CodeNotFoundException.class)
public ResponseEntity<String> handleCodeNotFoundException(CodeNotFoundException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
}

View File

@ -0,0 +1,10 @@
package com.example.nto.repository;
import com.example.nto.entity.Code;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface CodeRepository extends JpaRepository<Code, Long> {
Optional<Code> findByValue(long value);
}

View File

@ -0,0 +1,10 @@
package com.example.nto.repository;
import com.example.nto.entity.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
Optional<Employee> findByLogin(String login);
}

View File

@ -0,0 +1,10 @@
package com.example.nto.service;
import com.example.nto.entity.Code;
import com.example.nto.entity.Employee;
public interface EmployeeService {
Employee getByLogin(String login);
Employee updateLastVisitByLogin(String login, Code code);
}

View File

@ -0,0 +1,40 @@
package com.example.nto.service.impl;
import com.example.nto.entity.Code;
import com.example.nto.entity.Employee;
import com.example.nto.exception.CodeNotFoundException;
import com.example.nto.exception.EmployeeNotFoundException;
import com.example.nto.repository.CodeRepository;
import com.example.nto.repository.EmployeeRepository;
import com.example.nto.service.EmployeeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.ZoneId;
@Service
@RequiredArgsConstructor
public class EmployeeServiceImpl implements EmployeeService {
private final EmployeeRepository employeeRepository;
private final CodeRepository codeRepository;
@Override
public Employee getByLogin(String login) {
return employeeRepository.findByLogin(login)
.orElseThrow(() -> new EmployeeNotFoundException("Employee with " + login + " login not found!"));
}
@Override
public Employee updateLastVisitByLogin(String login, Code code) {
Employee employee = getByLogin(login);
codeRepository.findByValue(code.getValue())
.orElseThrow(() -> new CodeNotFoundException("Code with " + code.getValue() + " value not found!"));
employee.setLastVisit(LocalDateTime.now(ZoneId.of("Europe/Moscow")));
return employeeRepository.save(employee);
}
}

View File

@ -0,0 +1,28 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
h2:
console:
#enabled: false
enabled: true
jpa:
#generate-ddl: false
generate-ddl: true
hibernate:
#ddl-auto: none
ddl-auto: create-drop
# Показываем запросы
show-sql: true
# Своевременный запуск data.sql
defer-datasource-initialization: true
spring-doc:
swagger-ui:
path: /swagger-ui.html
operationsSorter: method

View File

@ -0,0 +1,14 @@
INSERT INTO employee (id, login, name, photo, position, last_visit)
VALUES
(1, 'pivanov', 'Иванов Петр Федорович', 'https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg', 'Разработчик', '2024-02-12T08:30'),
(2, 'ipetrov', 'Петров Иван Константинович', 'https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg', 'Аналитик', '2024-02-13T08:35'),
(3, 'asemenov', 'Семенов Анатолий Анатольевич', 'https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg', 'Разработчик', '2024-02-13T08:31'),
(4, 'afedorov', 'Федоров Александр Сергеевич', 'https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg', 'Тестировщик', '2024-02-12T08:36');
INSERT INTO code (value)
VALUES
(1234567890123456789),
(9223372036854775807),
(1122334455667788990),
(998877665544332211),
(5566778899001122334);

View File

@ -0,0 +1,286 @@
package com.example.nto.intagration;
import com.example.nto.entity.Code;
import com.example.nto.entity.Employee;
import com.example.nto.repository.EmployeeRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import static org.assertj.core.api.Assertions.assertThat;
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.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Transactional
@SpringBootTest
@DisplayName("Микросервис должен")
public class EmployeeControllerIntegrationTest {
@Autowired
private WebApplicationContext webApplicationContext;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private EmployeeRepository employeeRepository;
private MockMvc mockMvc;
private List<Employee> expectedEmployeesList;
private List<Code> expectedCodeList;
private Employee testEmployee;
private long testEmployeeId;
private DateTimeFormatter formatter;
@BeforeEach
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
expectedEmployeesList = List.of(
Employee.builder()
.id(1).login("pivanov").name("Иванов Петр Федорович")
.photo("https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg")
.position("Разработчик").lastVisit(LocalDateTime.parse("2024-02-12T08:30"))
.build(),
Employee.builder()
.id(2).login("ipetrov").name("Петров Иван Константинович")
.photo("https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg")
.position("Аналитик").lastVisit(LocalDateTime.parse("2024-02-13T08:35"))
.build(),
Employee.builder()
.id(3).login("asemenov").name("Семенов Анатолий Анатольевич")
.photo("https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg")
.position("Разработчик").lastVisit(LocalDateTime.parse("2024-02-13T08:31"))
.build(),
Employee.builder()
.id(4).login("afedorov").name("Федоров Александр Сергеевич")
.photo("https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg")
.position("Тестировщик").lastVisit(LocalDateTime.parse("2024-02-12T08:36"))
.build()
);
expectedCodeList = List.of(
Code.builder().value(1234567890123456789L).build(),
Code.builder().value(9223372036854775807L).build(),
Code.builder().value(1122334455667788990L).build(),
Code.builder().value(998877665544332211L).build(),
Code.builder().value(5566778899001122334L).build()
);
testEmployee = Employee.builder()
.login("anepretimov")
.name("Непретимов Александр Евгеньевич")
.photo("https://testImg.jpg")
.position("Тестировщик")
.lastVisit(LocalDateTime.parse("2024-02-12T08:36:00"))
.build();
testEmployeeId = employeeRepository.save(testEmployee).getId();
// Используем ISO-формат для преобразования LocalDateTime
formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
}
@Test
@DisplayName("логинеться со всеми предзаполненными сотрудниками")
public void testLoginWithPrefilledData() throws Exception {
for (Employee employee : expectedEmployeesList) {
mockMvc.perform(get("/api/{login}/auth", employee.getLogin()))
.andExpect(status().isOk())
.andDo(print());
}
}
@Test
@DisplayName("вернуть 401 Unauthorized при логининге в несуществующим логином")
public void testLoginWithWrongLogin() throws Exception {
mockMvc.perform(get("/api/{login}/auth", "WrongEmployeeLogin"))
.andExpect(status().isUnauthorized())
.andDo(print());
}
@Test
@DisplayName("работать не только с предзаполненными данными")
public void testLoginWithNewEmployee() throws Exception {
mockMvc.perform(get("/api/{login}/auth", testEmployee.getLogin()))
.andExpect(status().isOk())
.andDo(print());
}
@Test
@DisplayName("получать информацию по логину о всех предзаполненных сотрудниках")
public void testGetInformationByLoginWithPrefilledData() throws Exception {
for (Employee employee : expectedEmployeesList) {
mockMvc.perform(get("/api/{login}/info", employee.getLogin()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.login").value(employee.getLogin()))
.andExpect(jsonPath("$.name").value(employee.getName()))
.andExpect(jsonPath("$.position").value(employee.getPosition()))
.andExpect(jsonPath("$.photo").value(employee.getPhoto()))
.andExpect(jsonPath("$.lastVisit").value(employee.getLastVisit().format(formatter)))
.andDo(print());
}
}
@Test
@DisplayName("получать информацию по логину о всех предзаполненных сотрудниках без учета дат")
public void testGetInformationByLoginWithPrefilledDataExcludeDate() throws Exception {
for (Employee employee : expectedEmployeesList) {
mockMvc.perform(get("/api/{login}/info", employee.getLogin()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.login").value(employee.getLogin()))
.andExpect(jsonPath("$.name").value(employee.getName()))
.andExpect(jsonPath("$.position").value(employee.getPosition()))
.andExpect(jsonPath("$.photo").value(employee.getPhoto()))
.andDo(print());
}
}
@Test
@DisplayName("получать информацию по логину о новых сотрудниках")
public void testGetInformationByLoginWithNewEmployee() throws Exception {
mockMvc.perform(get("/api/{login}/info", testEmployee.getLogin()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.login").value(testEmployee.getLogin()))
.andExpect(jsonPath("$.name").value(testEmployee.getName()))
.andExpect(jsonPath("$.position").value(testEmployee.getPosition()))
.andExpect(jsonPath("$.photo").value(testEmployee.getPhoto()))
.andExpect(jsonPath("$.lastVisit").value(testEmployee.getLastVisit().format(formatter)))
.andDo(print());
}
@Test
@DisplayName("получать информацию по логину о новых сотрудниках без учета даты")
public void testGetInformationByLoginWithNewEmployeeExcludeDate() throws Exception {
mockMvc.perform(get("/api/{login}/info", testEmployee.getLogin()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.login").value(testEmployee.getLogin()))
.andExpect(jsonPath("$.name").value(testEmployee.getName()))
.andExpect(jsonPath("$.position").value(testEmployee.getPosition()))
.andExpect(jsonPath("$.photo").value(testEmployee.getPhoto()))
.andDo(print());
}
@Test
@DisplayName("вернуть 401 Unauthorized при попытке получить информацию в несуществующим логином")
public void testGetInformationByLoginWithWrongLogin() throws Exception {
mockMvc.perform(get("/api/{login}/auth", "WrongEmployeeLogin"))
.andExpect(status().isUnauthorized())
.andDo(print());
}
@Test
@DisplayName("открыть дверь новому сотруднику, который предоставил верный код")
public void testUpdateLastVisitByLoginOpenNewEmployeeAnyCode() throws Exception {
Code expectedCode = expectedCodeList.stream()
.skip(ThreadLocalRandom.current().nextInt(expectedCodeList.size()))
.findFirst()
.orElseThrow();
mockMvc.perform(patch("/api/{login}/open", testEmployee.getLogin())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(expectedCode)))
.andExpect(status().isOk())
.andDo(print());
}
@Test
@DisplayName("открыть дверь каждому предзаполненному пользователю, который предоставил верный код")
public void testUpdateLastVisitByLoginOpenAnyPrefilled() throws Exception {
for (Employee employee : expectedEmployeesList) {
for (Code code : expectedCodeList) {
mockMvc.perform(patch("/api/{login}/open", employee.getLogin())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(code)))
.andExpect(status().isOk())
.andDo(print());
}
}
}
@Test
@DisplayName("открыть дверь новому сотруднику, который предоставил верный код и менять время последнего визита")
public void testUpdateLastVisitByLoginOpenNewEmployeeAndUpdateDate() throws Exception {
Employee expectedEmployee = Employee.builder()
.id(testEmployeeId)
.login("anepretimov")
.name("Непретимов Александр Евгеньевич")
.photo("https://testImg.jpg")
.position("Тестировщик")
.lastVisit(LocalDateTime.parse("2024-03-12T08:36:00"))
.build();
Optional<Employee> actualEmployeeOptional = employeeRepository.findById(testEmployeeId);
actualEmployeeOptional.ifPresent(
employee -> assertThat(testEmployee.getLastVisit())
.isEqualTo(employee.getLastVisit().format(formatter))
);
Code expectedCode = expectedCodeList.stream()
.skip(ThreadLocalRandom.current().nextInt(expectedCodeList.size()))
.findFirst()
.orElseThrow();
mockMvc.perform(patch("/api/{login}/open", expectedEmployee.getLogin())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(expectedCode)))
.andExpect(status().isOk())
.andDo(print());
actualEmployeeOptional = employeeRepository.findById(testEmployeeId);
actualEmployeeOptional.ifPresent(
employee -> assertThat(expectedEmployee.getLastVisit())
.isNotEqualTo(employee.getLastVisit())
);
}
@Test
@DisplayName("вернуть 400 Bad Request, если код не верный")
public void testUpdateLastVisitByLoginWrongCode() throws Exception {
Code expectedCode = Code.builder().value(-1L).build();
mockMvc.perform(patch("/api/{login}/open", expectedEmployeesList.get(0).getLogin())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(expectedCode)))
.andExpect(status().isBadRequest())
.andDo(print());
}
@Test
@DisplayName("не обновлять визит, если код не верный")
public void testUpdateLastVisitByLoginWrongCodeUpdateData() throws Exception {
Code expectedCode = Code.builder().value(-1L).build();
Employee expectedEmployee = employeeRepository.findAll().stream().findFirst().orElseThrow();
mockMvc.perform(patch("/api/{login}/open", expectedEmployee.getLogin())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(expectedCode)))
.andExpect(status().isBadRequest())
.andDo(print());
Employee actualEmployee = employeeRepository.findById(expectedEmployee.getId()).orElseThrow();
assertThat(actualEmployee).isEqualTo(expectedEmployee);
}
}