Sign up controller, logic, and tests.
This commit is contained in:
parent
b17dddfca9
commit
e205db813a
@ -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 {}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
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;
|
||||
|
||||
public sealed interface User extends UserDetails permits UserEntity {
|
||||
public interface User extends UserDetails {
|
||||
|
||||
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> {
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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