Sign up controller, logic, and tests.

This commit is contained in:
JesseBrault0709 2024-06-26 08:46:45 +02:00
parent b17dddfca9
commit e205db813a
11 changed files with 290 additions and 43 deletions

View File

@ -1,11 +1,5 @@
package app.mealsmadeeasy.api.security;
import app.mealsmadeeasy.api.user.User;
import org.springframework.security.core.userdetails.UserDetailsService;
public interface JpaUserDetailsService extends UserDetailsService {
User createUser(User user);
User updateUser(User user);
void deleteUser(String username);
void deleteUser(User user);
}
public interface JpaUserDetailsService extends UserDetailsService {}

View File

@ -1,7 +1,5 @@
package app.mealsmadeeasy.api.security;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserEntity;
import app.mealsmadeeasy.api.user.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@ -16,28 +14,6 @@ public final class JpaUserDetailsServiceImpl implements JpaUserDetailsService {
this.userRepository = userRepository;
}
@Override
public User createUser(User user) {
return this.userRepository.save((UserEntity) user);
}
@Override
public User updateUser(User user) {
return this.userRepository.save((UserEntity) user);
}
@Override
public void deleteUser(String username) {
final UserEntity user = this.userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("No such User with username: " + username));
this.userRepository.delete(user);
}
@Override
public void deleteUser(User user) {
this.userRepository.delete((UserEntity) user);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return this.userRepository.findByUsername(username)

View File

@ -32,7 +32,7 @@ public class SecurityConfiguration {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers("/greeting", "/auth/**");
return web -> web.ignoring().requestMatchers("/greeting", "/auth/**", "/sign-up/**");
}
@Bean

View File

@ -0,0 +1,33 @@
package app.mealsmadeeasy.api.signup;
public final class SignUpBody {
private String username;
private String email;
private String password;
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return this.email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@ -0,0 +1,53 @@
package app.mealsmadeeasy.api.signup;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserCreateException;
import app.mealsmadeeasy.api.user.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/sign-up")
public final class SignUpController {
private final UserService userService;
public SignUpController(UserService userService) {
this.userService = userService;
}
@GetMapping("/check-username")
public ResponseEntity<Map<String, Object>> checkUsername(@RequestBody Map<String, Object> body) {
final boolean usernameAvailable = this.userService.isUsernameAvailable((String) body.get("username"));
final Map<String, Object> result = Map.of("isAvailable", usernameAvailable);
return ResponseEntity.ok(result);
}
@GetMapping("/check-email")
public ResponseEntity<Map<String, Object>> checkEmail(@RequestBody Map<String, Object> body) {
final boolean emailAvailable = this.userService.isEmailAvailable((String) body.get("email"));
final Map<String, Object> result = Map.of("isAvailable", emailAvailable);
return ResponseEntity.ok(result);
}
@PostMapping
public ResponseEntity<Map<String, Object>> signUp(@RequestBody SignUpBody body) {
final User created;
try {
created = this.userService.createUser(body.getUsername(), body.getEmail(), body.getPassword());
} catch (UserCreateException userCreateException) {
return ResponseEntity.badRequest().body(Map.of(
"error", Map.of(
"type", userCreateException.getType().toString(),
"message", userCreateException.getMessage()
)
));
}
final Map<String, Object> view = Map.of("username", created.getUsername());
return ResponseEntity.status(HttpStatus.CREATED).body(view);
}
}

View File

@ -4,7 +4,7 @@ import org.springframework.security.core.userdetails.UserDetails;
import java.util.Set;
public sealed interface User extends UserDetails permits UserEntity {
public interface User extends UserDetails {
Long getId();

View File

@ -0,0 +1,25 @@
package app.mealsmadeeasy.api.user;
public class UserCreateException extends Exception {
public enum Type {
USERNAME_TAKEN, EMAIL_TAKEN, BAD_PASSWORD
}
private final Type type;
public UserCreateException(Type type, String message, Throwable cause) {
super(message, cause);
this.type = type;
}
public UserCreateException(Type type, String message) {
super(message);
this.type = type;
}
public Type getType() {
return this.type;
}
}

View File

@ -6,4 +6,7 @@ import java.util.Optional;
public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByUsername(String username);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
void deleteByUsername(String username);
}

View File

@ -3,8 +3,19 @@ package app.mealsmadeeasy.api.user;
import java.util.Set;
public interface UserService {
User createUser(String username, String email, String rawPassword, Set<UserGrantedAuthority> authorities);
User createUser(String username, String email, String rawPassword, Set<UserGrantedAuthority> authorities)
throws UserCreateException;
default User createUser(String username, String email, String rawPassword) throws UserCreateException {
return this.createUser(username, email, rawPassword, Set.of());
}
User updateUser(User user);
void deleteUser(User user);
void deleteUser(String username);
boolean isUsernameAvailable(String username);
boolean isEmailAvailable(String email);
}

View File

@ -1,6 +1,5 @@
package app.mealsmadeeasy.api.user;
import app.mealsmadeeasy.api.security.JpaUserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@ -9,12 +8,12 @@ import java.util.Set;
@Service
public final class UserServiceImpl implements UserService {
private final JpaUserDetailsService jpaUserDetailsService;
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
public UserServiceImpl(JpaUserDetailsService jpaUserDetailsService, PasswordEncoder passwordEncoder) {
this.jpaUserDetailsService = jpaUserDetailsService;
public UserServiceImpl(PasswordEncoder passwordEncoder, UserRepository userRepository) {
this.passwordEncoder = passwordEncoder;
this.userRepository = userRepository;
}
@Override
@ -23,28 +22,47 @@ public final class UserServiceImpl implements UserService {
String email,
String rawPassword,
Set<UserGrantedAuthority> authorities
) {
) throws UserCreateException {
if (this.userRepository.existsByUsername(username)) {
throw new UserCreateException(
UserCreateException.Type.USERNAME_TAKEN,
"Username " + username + " is taken."
);
}
if (this.userRepository.existsByEmail(email)) {
throw new UserCreateException(UserCreateException.Type.EMAIL_TAKEN, "Email " + email + " is taken.");
}
final UserEntity draft = UserEntity.getDefaultDraft();
draft.setUsername(username);
draft.setEmail(email);
draft.setPassword(this.passwordEncoder.encode(rawPassword));
draft.addAuthorities(authorities);
return this.jpaUserDetailsService.createUser(draft);
return this.userRepository.save(draft);
}
@Override
public User updateUser(User user) {
return this.jpaUserDetailsService.updateUser(user);
return this.userRepository.save((UserEntity) user);
}
@Override
public void deleteUser(User user) {
this.jpaUserDetailsService.deleteUser(user);
this.userRepository.delete((UserEntity) user);
}
@Override
public void deleteUser(String username) {
this.jpaUserDetailsService.deleteUser(username);
this.userRepository.deleteByUsername(username);
}
@Override
public boolean isUsernameAvailable(String username) {
return !this.userRepository.existsByUsername(username);
}
@Override
public boolean isEmailAvailable(String email) {
return !this.userRepository.existsByEmail(email);
}
}

View File

@ -0,0 +1,134 @@
package app.mealsmadeeasy.api.signup;
import app.mealsmadeeasy.api.user.UserCreateException.Type;
import app.mealsmadeeasy.api.user.UserService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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.DirtiesContext;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import java.util.Map;
import static org.hamcrest.Matchers.containsString;
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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
public class SignUpControllerTests {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private MockMvc mockMvc;
@Autowired
private UserService userService;
private MockHttpServletRequestBuilder getCheckUsernameRequest(String usernameToCheck)
throws JsonProcessingException {
final Map<String, Object> body = Map.of("username", usernameToCheck);
return get("/sign-up/check-username")
.content(this.objectMapper.writeValueAsString(body))
.contentType(MediaType.APPLICATION_JSON);
}
@Test
public void checkUsernameExpectAvailable() throws Exception {
this.mockMvc.perform(this.getCheckUsernameRequest("isAvailable"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.isAvailable").value(true));
}
@Test
@DirtiesContext
public void checkUsernameExpectNotAvailable() throws Exception {
this.userService.createUser("notAvailable", "not-available@notavailable.com", "test");
this.mockMvc.perform(this.getCheckUsernameRequest("notAvailable"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.isAvailable").value(false));
}
private MockHttpServletRequestBuilder getCheckEmailRequest(String emailToCheck) throws JsonProcessingException {
final Map<String, Object> body = Map.of("email", emailToCheck);
return get("/sign-up/check-email")
.content(this.objectMapper.writeValueAsString(body))
.contentType(MediaType.APPLICATION_JSON);
}
@Test
public void checkEmailExpectAvailable() throws Exception {
this.mockMvc.perform(this.getCheckEmailRequest("available@available.com"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.isAvailable").value(true));
}
@Test
@DirtiesContext
public void checkEmailExpectNotAvailable() throws Exception {
this.userService.createUser("notAvailable", "not-available@notavailable.com", "test");
this.mockMvc.perform(this.getCheckEmailRequest("not-available@notavailable.com"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.isAvailable").value(false));
}
@Test
@DirtiesContext
public void simpleSignUp() throws Exception {
final SignUpBody body = new SignUpBody();
body.setUsername("newUser");
body.setEmail("new@user.com");
body.setPassword("test");
final MockHttpServletRequestBuilder req = post("/sign-up")
.content(this.objectMapper.writeValueAsString(body))
.contentType(MediaType.APPLICATION_JSON);
this.mockMvc.perform(req)
.andExpect(status().isCreated())
.andExpect(jsonPath("$.username").value("newUser"));
}
@Test
@DirtiesContext
public void signUpBadRequestWhenUsernameTaken() throws Exception {
this.userService.createUser("taken", "taken@taken.com", "test");
final SignUpBody body = new SignUpBody();
body.setUsername("taken");
body.setEmail("not-taken@taken.com"); // n.b.
body.setPassword("test");
final MockHttpServletRequestBuilder req = post("/sign-up")
.content(this.objectMapper.writeValueAsString(body))
.contentType(MediaType.APPLICATION_JSON);
this.mockMvc.perform(req)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error.type").value(Type.USERNAME_TAKEN.toString()))
.andExpect(jsonPath("$.error.message").value(containsString("taken")));
}
@Test
@DirtiesContext
public void signUpBadRequestWhenEmailTaken() throws Exception {
this.userService.createUser("taken", "taken@taken.com", "test");
final SignUpBody body = new SignUpBody();
body.setUsername("notTaken"); // n.b.
body.setEmail("taken@taken.com");
body.setPassword("test");
final MockHttpServletRequestBuilder req = post("/sign-up")
.content(this.objectMapper.writeValueAsString(body))
.contentType(MediaType.APPLICATION_JSON);
this.mockMvc.perform(req)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error.type").value(Type.EMAIL_TAKEN.toString()))
.andExpect(jsonPath("$.error.message").value(containsString("taken@taken.com")));
}
}