Sign up controller, logic, and tests.
This commit is contained in:
parent
b17dddfca9
commit
e205db813a
@ -1,11 +1,5 @@
|
|||||||
package app.mealsmadeeasy.api.security;
|
package app.mealsmadeeasy.api.security;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.user.User;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
|
||||||
public interface JpaUserDetailsService extends UserDetailsService {
|
public interface JpaUserDetailsService extends UserDetailsService {}
|
||||||
User createUser(User user);
|
|
||||||
User updateUser(User user);
|
|
||||||
void deleteUser(String username);
|
|
||||||
void deleteUser(User user);
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package app.mealsmadeeasy.api.security;
|
package app.mealsmadeeasy.api.security;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.user.User;
|
|
||||||
import app.mealsmadeeasy.api.user.UserEntity;
|
|
||||||
import app.mealsmadeeasy.api.user.UserRepository;
|
import app.mealsmadeeasy.api.user.UserRepository;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
@ -16,28 +14,6 @@ public final class JpaUserDetailsServiceImpl implements JpaUserDetailsService {
|
|||||||
this.userRepository = userRepository;
|
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
|
@Override
|
||||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
return this.userRepository.findByUsername(username)
|
return this.userRepository.findByUsername(username)
|
||||||
|
@ -32,7 +32,7 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||||
return web -> web.ignoring().requestMatchers("/greeting", "/auth/**");
|
return web -> web.ignoring().requestMatchers("/greeting", "/auth/**", "/sign-up/**");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
33
src/main/java/app/mealsmadeeasy/api/signup/SignUpBody.java
Normal file
33
src/main/java/app/mealsmadeeasy/api/signup/SignUpBody.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -4,7 +4,7 @@ import org.springframework.security.core.userdetails.UserDetails;
|
|||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public sealed interface User extends UserDetails permits UserEntity {
|
public interface User extends UserDetails {
|
||||||
|
|
||||||
Long getId();
|
Long getId();
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -6,4 +6,7 @@ import java.util.Optional;
|
|||||||
|
|
||||||
public interface UserRepository extends JpaRepository<UserEntity, Long> {
|
public interface UserRepository extends JpaRepository<UserEntity, Long> {
|
||||||
Optional<UserEntity> findByUsername(String username);
|
Optional<UserEntity> findByUsername(String username);
|
||||||
|
boolean existsByUsername(String username);
|
||||||
|
boolean existsByEmail(String email);
|
||||||
|
void deleteByUsername(String username);
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,19 @@ package app.mealsmadeeasy.api.user;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public interface UserService {
|
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);
|
User updateUser(User user);
|
||||||
void deleteUser(User user);
|
void deleteUser(User user);
|
||||||
void deleteUser(String username);
|
void deleteUser(String username);
|
||||||
|
|
||||||
|
boolean isUsernameAvailable(String username);
|
||||||
|
boolean isEmailAvailable(String email);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package app.mealsmadeeasy.api.user;
|
package app.mealsmadeeasy.api.user;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.security.JpaUserDetailsService;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@ -9,12 +8,12 @@ import java.util.Set;
|
|||||||
@Service
|
@Service
|
||||||
public final class UserServiceImpl implements UserService {
|
public final class UserServiceImpl implements UserService {
|
||||||
|
|
||||||
private final JpaUserDetailsService jpaUserDetailsService;
|
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
public UserServiceImpl(JpaUserDetailsService jpaUserDetailsService, PasswordEncoder passwordEncoder) {
|
public UserServiceImpl(PasswordEncoder passwordEncoder, UserRepository userRepository) {
|
||||||
this.jpaUserDetailsService = jpaUserDetailsService;
|
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.userRepository = userRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -23,28 +22,47 @@ public final class UserServiceImpl implements UserService {
|
|||||||
String email,
|
String email,
|
||||||
String rawPassword,
|
String rawPassword,
|
||||||
Set<UserGrantedAuthority> authorities
|
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();
|
final UserEntity draft = UserEntity.getDefaultDraft();
|
||||||
draft.setUsername(username);
|
draft.setUsername(username);
|
||||||
draft.setEmail(email);
|
draft.setEmail(email);
|
||||||
draft.setPassword(this.passwordEncoder.encode(rawPassword));
|
draft.setPassword(this.passwordEncoder.encode(rawPassword));
|
||||||
draft.addAuthorities(authorities);
|
draft.addAuthorities(authorities);
|
||||||
return this.jpaUserDetailsService.createUser(draft);
|
return this.userRepository.save(draft);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public User updateUser(User user) {
|
public User updateUser(User user) {
|
||||||
return this.jpaUserDetailsService.updateUser(user);
|
return this.userRepository.save((UserEntity) user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteUser(User user) {
|
public void deleteUser(User user) {
|
||||||
this.jpaUserDetailsService.deleteUser(user);
|
this.userRepository.delete((UserEntity) user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteUser(String username) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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")));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user