Skip to content

Commit

Permalink
Merge pull request #4 from raeperd/feature/#1-implement-user-registra…
Browse files Browse the repository at this point in the history
…tion

Feature/#1 implement user registration
  • Loading branch information
raeperd committed Apr 14, 2021
2 parents fb02331 + 034c440 commit 20b91ea
Show file tree
Hide file tree
Showing 13 changed files with 431 additions and 0 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id 'org.springframework.boot' version '2.4.4'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id 'jacoco'
id "io.freefair.lombok" version "5.3.3.3"
id "org.ec4j.editorconfig" version "0.0.3"
id "org.sonarqube" version "3.1.1"
Expand All @@ -23,6 +24,8 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

apply from: 'test.gradle'

test {
useJUnitPlatform()
}
Expand Down
2 changes: 2 additions & 0 deletions lombok.config
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# This file is generated by the 'io.freefair.lombok' Gradle plugin
config.stopBubbling = true
lombok.addLombokGeneratedAnnotation = true
lombok.extern.findbugs.addSuppressFBWarnings = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.github.raeperd.realworld.application;

import lombok.Getter;

@Getter
public class UserLoginRequestDTO {

private final String email;
private final String password;

public UserLoginRequestDTO(String email, String password) {
this.email = email;
this.password = password;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.github.raeperd.realworld.application;

import io.github.raeperd.realworld.domain.User;
import lombok.Getter;

@Getter
public class UserPostRequestDTO {

private final String username;
private final String email;
private final String password;

public UserPostRequestDTO(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}

public User toUser() {
return User.createNewUser(username, email, password);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.github.raeperd.realworld.application;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.github.raeperd.realworld.domain.User;
import lombok.Getter;

@JsonTypeName("user")
@JsonTypeInfo(include = As.WRAPPER_OBJECT, use = Id.NAME)
@Getter
public class UserResponseDTO {

private final String email;
private final String token;
private final String username;
private final String bio;
private final String image;

public static UserResponseDTO fromUser(User user) {
return new UserResponseDTO(user);
}

private UserResponseDTO(User user) {
this.email = user.getEmail();
this.username = user.getUsername();
this.bio = user.getBio();
this.image = user.getImage();
// TODO: Generate token from user somewhere else
this.token = "";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.github.raeperd.realworld.application;

import io.github.raeperd.realworld.domain.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.ResponseEntity.of;

@RequestMapping("/users")
@RestController
public class UserRestController {

private final UserService userService;

public UserRestController(UserService userService) {
this.userService = userService;
}

@PostMapping("/login")
public ResponseEntity<UserResponseDTO> login(@RequestBody UserLoginRequestDTO loginRequest) {
return of(userService.login(loginRequest.getEmail(), loginRequest.getPassword())
.map(UserResponseDTO::fromUser));
}

@ResponseStatus(CREATED)
@PostMapping
public UserResponseDTO postUser(@RequestBody UserPostRequestDTO postRequest) {
return UserResponseDTO.fromUser(
userService.createUser(postRequest.toUser()));
}

}
46 changes: 46 additions & 0 deletions src/main/java/io/github/raeperd/realworld/domain/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.github.raeperd.realworld.domain;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class User {

@Id
private long id;

private String email;
private String password;
private String username;
private String bio;
private String image;

public static User createNewUser(String username, String email, String password) {
return new User(username, email, password);
}

private User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}

protected User() {
}

public String getEmail() {
return email;
}

public String getUsername() {
return username;
}

public String getBio() {
return bio;
}

public String getImage() {
return image;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.raeperd.realworld.domain;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findFirstByEmailAndPassword(String email, String password);

boolean existsByEmail(String email);

}
27 changes: 27 additions & 0 deletions src/main/java/io/github/raeperd/realworld/domain/UserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.github.raeperd.realworld.domain;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
public class UserService {

private final UserRepository userRepository;

public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}

@Transactional(readOnly = true)
public Optional<User> login(String email, String password) {
return userRepository.findFirstByEmailAndPassword(email, password);
}

@Transactional
public User createUser(User user) {
return userRepository.save(user);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package io.github.raeperd.realworld.application;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.raeperd.realworld.domain.User;
import io.github.raeperd.realworld.domain.UserService;
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.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

import static io.github.raeperd.realworld.domain.User.createNewUser;
import static java.util.Optional.of;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import static org.springframework.http.MediaType.APPLICATION_JSON;
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;

@AutoConfigureMockMvc
@WebMvcTest(UserRestController.class)
class UserRestControllerTest {

@MockBean
private UserService userService;

@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;

@Test
void when_post_login_expect_userService_login_called() throws Exception {
final var loginDTO = new UserLoginRequestDTO("email", "password");
given(userService.login(loginDTO.getEmail(), loginDTO.getPassword()))
.willReturn(of(createNewUser("", loginDTO.getEmail(), loginDTO.getPassword())));

mockMvc.perform(post("/users/login")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginDTO)));

then(userService).should(times(1))
.login(loginDTO.getEmail(), loginDTO.getPassword());
}

@Test
void when_post_login_expect_valid_response() throws Exception {
when(userService.login(anyString(), anyString()))
.thenReturn(of(createNewUser("username", "email", "password")));

mockMvc.perform(post("/users/login")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new UserLoginRequestDTO("email", "password"))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.user").exists())
.andExpect(jsonPath("$.user.email").exists())
.andExpect(jsonPath("$.user.bio").hasJsonPath())
.andExpect(jsonPath("$.user.image").hasJsonPath())
.andExpect(jsonPath("$.user.token").hasJsonPath());
}

@Test
void when_post_users_expect_userService_createUser_called() throws Exception {
final var postRequestDTO = new UserPostRequestDTO("username", "email", "password");
given(userService.createUser(any(User.class))).willReturn(postRequestDTO.toUser());

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(postRequestDTO)));

then(userService).should(times(1)).createUser(any(User.class));
}

@Test
void when_post_users_expect_valid_response() throws Exception {
when(userService.createUser(any(User.class)))
.thenReturn(createNewUser("username", "email", "password"));

mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new UserLoginRequestDTO("email", "password"))))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.user").exists())
.andExpect(jsonPath("$.user.email").exists())
.andExpect(jsonPath("$.user.bio").hasJsonPath())
.andExpect(jsonPath("$.user.image").hasJsonPath())
.andExpect(jsonPath("$.user.token").hasJsonPath());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.github.raeperd.realworld.domain;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.times;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

private UserService userService;

@Mock
private UserRepository userRepository;

@BeforeEach
void initializeService() {
this.userService = new UserService(userRepository);
}

@Test
void when_login_expect_userRepository_findFirstByEmailAndPassword_called() {
final var email = "email";
final var password = "password";

userService.login(email, password);

then(userRepository).should(times(1))
.findFirstByEmailAndPassword(email, password);
}

@Test
void when_createUser_expect_userRepository_save_called(@Mock User user) {
userService.createUser(user);

then(userRepository).should(times(1)).save(user);
}

}
20 changes: 20 additions & 0 deletions src/test/java/io/github/raeperd/realworld/domain/UserTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.raeperd.realworld.domain;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class UserTest {

@Test
void expect_user_has_protected_no_args_constructor() {
class ChildUser extends User {
public ChildUser() {
super();
}
}
final var childUser = new ChildUser();

assertThat(childUser).isInstanceOf(User.class);
}
}
Loading

0 comments on commit 20b91ea

Please sign in to comment.