dev #2

Merged
backdev merged 9 commits from dev into main 2025-11-25 13:32:46 +08:00
9 changed files with 139 additions and 41 deletions
Showing only changes of commit 74ae769de0 - Show all commits
@@ -30,12 +30,10 @@ public class AuthController {
} }
@PostMapping("/login") @PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) { public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
//TODO add try-catch and response code AuthToken login = authService.login(request);
// try {
return ResponseEntity.ok(authService.login(request)); return ResponseEntity.ok(new LoginResponse(true, login.getUser().getEmail(),
// } catch (RuntimeException e) { login.getToken(),login.getExpiresAt().toString()));
// return ResponseEntity.badRequest();
// }
} }
} }
@@ -1,12 +1,18 @@
package ru.soune.no_copy.dto; package ru.soune.no_copy.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@Data @Data
@AllArgsConstructor
public class LoginRequest { public class LoginRequest {
@NotBlank(message = "error.not.blank") @Email(message = "error.not.email") @Size(max = 128)
private String email; private String email;
@NotBlank(message = "error.not.blank") @Size(min = 8)
private String password; private String password;
} }
@@ -1,13 +1,14 @@
package ru.soune.no_copy.dto; package ru.soune.no_copy.dto;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import ru.soune.no_copy.entity.User; import ru.soune.no_copy.entity.User;
@Data @Data
@AllArgsConstructor
public class LoginResponse { public class LoginResponse {
private boolean success; private boolean success;
private String message; private String email;
private User user;
private String token; private String token;
private String expiresAt; private String expiresAt;
} }
@@ -8,6 +8,6 @@ public record RegisterRequest(
@NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String firstName, @NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String firstName,
@NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String secondName, @NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String secondName,
@NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String lastName, @NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String lastName,
@NotBlank(message = "error.not.blank") @Email(message = "error.not.email") @Size(max = 128)String email, @NotBlank(message = "error.not.blank") @Email(message = "error.not.email") @Size(max = 128) String email,
@NotBlank(message = "error.not.blank") @Size(min = 8) String password @NotBlank(message = "error.not.blank") @Size(min = 8) String password
) {} ) {}
@@ -0,0 +1,7 @@
package ru.soune.no_copy.exception;
public class NotValidationPasswordException extends RuntimeException {
public NotValidationPasswordException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package ru.soune.no_copy.exception;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
@@ -8,7 +8,9 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import ru.soune.no_copy.exception.NotValidationPasswordException;
import ru.soune.no_copy.exception.UserAlreadyExistsException; import ru.soune.no_copy.exception.UserAlreadyExistsException;
import ru.soune.no_copy.exception.UserNotFoundException;
import java.util.Map; import java.util.Map;
@@ -39,4 +41,26 @@ public class GlobalExceptionHandler {
"message" ,ex.getMessage() "message" ,ex.getMessage()
)); ));
} }
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<?> handleUserNotFoundException(UserNotFoundException ex) {
return ResponseEntity.
badRequest()
.body(Map.of(
"success", false,
"message" ,ex.getMessage()
));
}
@ExceptionHandler(NotValidationPasswordException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ResponseEntity<?> handleNotValidationPasswordException(NotValidationPasswordException ex) {
return ResponseEntity.
badRequest()
.body(Map.of(
"success", false,
"message" ,ex.getMessage()
));
}
} }
@@ -6,17 +6,17 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.no_copy.dto.LoginRequest; import ru.soune.no_copy.dto.LoginRequest;
import ru.soune.no_copy.dto.LoginResponse;
import ru.soune.no_copy.dto.RegisterRequest; import ru.soune.no_copy.dto.RegisterRequest;
import ru.soune.no_copy.entity.AuthToken; import ru.soune.no_copy.entity.AuthToken;
import ru.soune.no_copy.entity.User; import ru.soune.no_copy.entity.User;
import ru.soune.no_copy.exception.NotValidationPasswordException;
import ru.soune.no_copy.exception.UserAlreadyExistsException; import ru.soune.no_copy.exception.UserAlreadyExistsException;
import ru.soune.no_copy.exception.UserNotFoundException;
import ru.soune.no_copy.repository.AuthTokenRepository; import ru.soune.no_copy.repository.AuthTokenRepository;
import ru.soune.no_copy.repository.UserRepository; import ru.soune.no_copy.repository.UserRepository;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.time.LocalDateTime; import java.util.Base64;
import java.util.HexFormat;
import java.util.Locale; import java.util.Locale;
import java.util.Optional; import java.util.Optional;
@@ -32,6 +32,8 @@ public class AuthService {
private final MessageSource messageSource; private final MessageSource messageSource;
private final SecureRandom secureRandom = new SecureRandom();
@Transactional @Transactional
public AuthToken register(RegisterRequest registerRequest) { public AuthToken register(RegisterRequest registerRequest) {
if (userRepository.existsByEmail(registerRequest.email())) { if (userRepository.existsByEmail(registerRequest.email())) {
@@ -49,48 +51,37 @@ public class AuthService {
User savedUser = userRepository.save(user); User savedUser = userRepository.save(user);
AuthToken authToken = new AuthToken(); AuthToken authToken = new AuthToken();
authToken.setToken(generateToken()); authToken.setToken(generateAuthToken());
authToken.setUser(savedUser); authToken.setUser(savedUser);
return authTokenRepository.save(authToken); return authTokenRepository.save(authToken);
} }
public LoginResponse login(LoginRequest request) { @Transactional
public AuthToken login(LoginRequest request) {
Optional<User> userOpt = userRepository.findByEmail(request.getEmail()); Optional<User> userOpt = userRepository.findByEmail(request.getEmail());
if (userOpt.isEmpty()) { if (userOpt.isEmpty()) {
//ADD EXCEPTION throw new UserNotFoundException("User with email " + request.getEmail() + " not found");
} }
User user = userOpt.get(); User user = userOpt.get();
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
//ADD EXCEPTION throw new NotValidationPasswordException("Invalid password");
} }
String token = generateToken();
LocalDateTime expiresAt = LocalDateTime.now().plusDays(30);
AuthToken authToken = new AuthToken(); AuthToken authToken = new AuthToken();
authToken.setToken(generateToken()); authToken.setToken(generateAuthToken());
authToken.setUser(user); authToken.setUser(user);
authTokenRepository.save(authToken); return authTokenRepository.save(authToken);
userRepository.save(user);
LoginResponse response = new LoginResponse();
response.setSuccess(true);
// response.setMessage(""); add message
response.setUser(user);
response.setToken(token);
response.setExpiresAt(expiresAt.toString());
return response;
} }
//TODO change logic generate token
private String generateToken() {
private String generateAuthToken() {
byte[] bytes = new byte[32]; byte[] bytes = new byte[32];
new SecureRandom().nextBytes(bytes); secureRandom.nextBytes(bytes);
return HexFormat.of().formatHex(bytes); return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
} }
} }
@@ -2,23 +2,29 @@ package ru.soune.no_copy.service;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import ru.soune.no_copy.dto.LoginRequest;
import ru.soune.no_copy.dto.RegisterRequest; import ru.soune.no_copy.dto.RegisterRequest;
import ru.soune.no_copy.entity.AuthToken; import ru.soune.no_copy.entity.AuthToken;
import ru.soune.no_copy.entity.User; import ru.soune.no_copy.entity.User;
import ru.soune.no_copy.exception.NotValidationPasswordException;
import ru.soune.no_copy.exception.UserAlreadyExistsException; import ru.soune.no_copy.exception.UserAlreadyExistsException;
import ru.soune.no_copy.exception.UserNotFoundException;
import ru.soune.no_copy.repository.AuthTokenRepository; import ru.soune.no_copy.repository.AuthTokenRepository;
import ru.soune.no_copy.repository.UserRepository; import ru.soune.no_copy.repository.UserRepository;
import java.util.Locale; import java.util.Locale;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class AuthServiceTest { public class AuthServiceTest {
@@ -45,7 +51,8 @@ public class AuthServiceTest {
@Test @Test
void registerSuccess() { void registerSuccess() {
RegisterRequest request = new RegisterRequest("John","A.","Doe","john@example.com","password123"); RegisterRequest request = new RegisterRequest("John","A.","Doe",
"john@example.com", "password123");
when(userRepository.existsByEmail(request.email())).thenReturn(false); when(userRepository.existsByEmail(request.email())).thenReturn(false);
when(passwordEncoder.encode(request.password())).thenReturn("hashed"); when(passwordEncoder.encode(request.password())).thenReturn("hashed");
@@ -65,14 +72,71 @@ public class AuthServiceTest {
@Test @Test
void registerExistingEmailThrows() { void registerExistingEmailThrows() {
RegisterRequest request = new RegisterRequest("John","A.","Doe","john@example.com","123"); RegisterRequest request = new RegisterRequest("John","A.","Doe",
"john@example.com", "password123");
when(userRepository.existsByEmail(request.email())).thenReturn(true); when(userRepository.existsByEmail(request.email()))
when(messageSource.getMessage(anyString(), any(), any(Locale.class))).thenReturn("User exists"); .thenReturn(true);
when(messageSource.getMessage(anyString(), any(), any(Locale.class)))
.thenReturn("User exists");
UserAlreadyExistsException ex = assertThrows(UserAlreadyExistsException.class, UserAlreadyExistsException ex = assertThrows(UserAlreadyExistsException.class,
() -> authService.register(request)); () -> authService.register(request));
assertEquals("User exists", ex.getMessage()); assertEquals("User exists", ex.getMessage());
} }
@Test
void login_success() {
LoginRequest request = new LoginRequest("test@mail.com", "password");
User user = new User();
user.setUserId(1L);
user.setEmail("test@mail.com");
user.setPassword("encoded_pass");
when(userRepository.findByEmail("test@mail.com"))
.thenReturn(Optional.of(user));
when(passwordEncoder.matches("password", "encoded_pass"))
.thenReturn(true);
when(authTokenRepository.save(any(AuthToken.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
AuthToken token = authService.login(request);
assertNotNull(token);
assertNotNull(token.getToken());
assertEquals(user, token.getUser());
ArgumentCaptor<AuthToken> captor = ArgumentCaptor.forClass(AuthToken.class);
verify(authTokenRepository).save(captor.capture());
assertEquals(user, captor.getValue().getUser());
assertNotNull(captor.getValue().getToken());
}
@Test
void login_userNotFound() {
LoginRequest request = new LoginRequest("missing@mail.com", "pwd");
when(userRepository.findByEmail("missing@mail.com")).thenReturn(Optional.empty());
assertThrows(UserNotFoundException.class, () -> authService.login(request));
}
@Test
void login_invalidPassword() {
LoginRequest request = new LoginRequest("test@mail.com", "wrong");
User user = new User();
user.setEmail("test@mail.com");
user.setPassword("encoded_pass");
when(userRepository.findByEmail("test@mail.com")).thenReturn(Optional.of(user));
when(passwordEncoder.matches("wrong", "encoded_pass")).thenReturn(false);
assertThrows(NotValidationPasswordException.class, () -> authService.login(request));
}
} }