From 2ee68700814a52b7c79ca39eaa0242e1fa37c1ac Mon Sep 17 00:00:00 2001 From: Matthew Johnson Date: Wed, 13 Jul 2016 15:18:40 +1000 Subject: [PATCH 01/10] Implemented creation of new User accounts. --- pom.xml | 32 ++++--- .../spring/challenge/SampleController.java | 14 --- .../co/redeye/spring/challenge/Utils.java | 16 ++++ .../controllers/AuthenticationController.java | 39 ++++++++ .../ExceptionHandlerController.java | 30 +++++++ .../co/redeye/spring/challenge/db/User.java | 71 +++++++++++++++ .../spring/challenge/db/UserRepository.java | 12 +++ .../services/AuthenticationException.java | 11 +++ .../services/AuthenticatorService.java | 90 +++++++++++++++++++ .../spring/challenge/views/ErrorResponse.java | 23 +++++ .../spring/challenge/views/LoginRequest.java | 27 ++++++ .../spring/challenge/views/LoginResponse.java | 16 ++++ 12 files changed, 356 insertions(+), 25 deletions(-) delete mode 100644 src/main/java/co/redeye/spring/challenge/SampleController.java create mode 100644 src/main/java/co/redeye/spring/challenge/Utils.java create mode 100644 src/main/java/co/redeye/spring/challenge/controllers/AuthenticationController.java create mode 100644 src/main/java/co/redeye/spring/challenge/controllers/ExceptionHandlerController.java create mode 100644 src/main/java/co/redeye/spring/challenge/db/User.java create mode 100644 src/main/java/co/redeye/spring/challenge/db/UserRepository.java create mode 100644 src/main/java/co/redeye/spring/challenge/services/AuthenticationException.java create mode 100644 src/main/java/co/redeye/spring/challenge/services/AuthenticatorService.java create mode 100644 src/main/java/co/redeye/spring/challenge/views/ErrorResponse.java create mode 100644 src/main/java/co/redeye/spring/challenge/views/LoginRequest.java create mode 100644 src/main/java/co/redeye/spring/challenge/views/LoginResponse.java diff --git a/pom.xml b/pom.xml index c2854e1..9c77d24 100644 --- a/pom.xml +++ b/pom.xml @@ -21,17 +21,27 @@ co.redeye.spring.challenge.Application - - org.springframework.boot - spring-boot-starter-parent - 1.3.5.RELEASE - - - - org.springframework.boot - spring-boot-starter-web - - + + org.springframework.boot + spring-boot-starter-parent + 1.3.5.RELEASE + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + 1.4.192 + + diff --git a/src/main/java/co/redeye/spring/challenge/SampleController.java b/src/main/java/co/redeye/spring/challenge/SampleController.java deleted file mode 100644 index 95d15a6..0000000 --- a/src/main/java/co/redeye/spring/challenge/SampleController.java +++ /dev/null @@ -1,14 +0,0 @@ -package co.redeye.spring.challenge; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class SampleController { - @RequestMapping("/") - @ResponseBody - public String home() { - return "Hello World!"; - } -} diff --git a/src/main/java/co/redeye/spring/challenge/Utils.java b/src/main/java/co/redeye/spring/challenge/Utils.java new file mode 100644 index 0000000..8e179d2 --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/Utils.java @@ -0,0 +1,16 @@ +package co.redeye.spring.challenge; + +/** + * Helper functions. + */ +public class Utils { + /** + * Checks that a string value is non-null and non-empty + * + * @param string The String to check. + * @return True if String is non-null and has a length >= 1 + */ + public static boolean stringPresent(String string) { + return string != null && !string.isEmpty(); + } +} diff --git a/src/main/java/co/redeye/spring/challenge/controllers/AuthenticationController.java b/src/main/java/co/redeye/spring/challenge/controllers/AuthenticationController.java new file mode 100644 index 0000000..b60a700 --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/controllers/AuthenticationController.java @@ -0,0 +1,39 @@ +package co.redeye.spring.challenge.controllers; + +import co.redeye.spring.challenge.Utils; +import co.redeye.spring.challenge.services.AuthenticationException; +import co.redeye.spring.challenge.services.AuthenticatorService; +import co.redeye.spring.challenge.views.LoginRequest; +import co.redeye.spring.challenge.views.LoginResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +public class AuthenticationController { + @Autowired + AuthenticatorService authenticator; + + @RequestMapping(value = "/account/register", method = RequestMethod.POST) + @ResponseBody + public LoginResponse home(@RequestBody LoginRequest registerRequest) throws AuthenticationException { + validate(registerRequest); + + String token = authenticator.register(registerRequest.getUsername(), registerRequest.getPassword()); + LoginResponse response = new LoginResponse(); + response.setToken(token); + return response; + } + + private void validate(LoginRequest registerRequest) throws AuthenticationException { + if (!Utils.stringPresent(registerRequest.getUsername())) { + throw new AuthenticationException("Invalid username."); + } + if (!Utils.stringPresent(registerRequest.getPassword())) { + throw new AuthenticationException("Invalid password."); + } + + } + + +} diff --git a/src/main/java/co/redeye/spring/challenge/controllers/ExceptionHandlerController.java b/src/main/java/co/redeye/spring/challenge/controllers/ExceptionHandlerController.java new file mode 100644 index 0000000..daaa15a --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/controllers/ExceptionHandlerController.java @@ -0,0 +1,30 @@ +package co.redeye.spring.challenge.controllers; + +import co.redeye.spring.challenge.services.AuthenticationException; +import co.redeye.spring.challenge.views.ErrorResponse; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Global Handler for uncaught Exceptions. + */ +@ControllerAdvice +public class ExceptionHandlerController { + @ExceptionHandler (value = AuthenticationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ErrorResponse authenticationException(AuthenticationException e) { + return new ErrorResponse(e.getMessage()); + } + + @ExceptionHandler (value = Throwable.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ResponseBody + public ErrorResponse catchAll(Throwable e) { + e.printStackTrace(); + return new ErrorResponse("An unexpected error has occurred."); + } +} diff --git a/src/main/java/co/redeye/spring/challenge/db/User.java b/src/main/java/co/redeye/spring/challenge/db/User.java new file mode 100644 index 0000000..7b443ba --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/db/User.java @@ -0,0 +1,71 @@ +package co.redeye.spring.challenge.db; + +import javax.persistence.*; +import java.util.List; + +/** + * Persistence ORM for registered users. + *

+ * This is my first time using automatically generated data definition. + */ +@Entity +@Table(name = "users", + indexes = {@Index(name = "username_index", columnList = "username", unique = true), + @Index(name = "token_index", columnList = "token", unique = true)}) +public class User { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private int id; + + @Column(nullable = false, unique = true) + private String username; + + @Column(nullable = false) + private String password; + + @Column(nullable = false) + private String salt; + + @Column(nullable = false, unique = true) + private String token; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getSalt() { + return salt; + } + + public void setSalt(String salt) { + this.salt = salt; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/src/main/java/co/redeye/spring/challenge/db/UserRepository.java b/src/main/java/co/redeye/spring/challenge/db/UserRepository.java new file mode 100644 index 0000000..60a022d --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/db/UserRepository.java @@ -0,0 +1,12 @@ +package co.redeye.spring.challenge.db; + +import org.springframework.data.repository.CrudRepository; + +/** + * Created by Matthew Johnson on 13/07/2016. + */ +public interface UserRepository extends CrudRepository { + User findByUsername(String username); + + User findByToken(String token); +} diff --git a/src/main/java/co/redeye/spring/challenge/services/AuthenticationException.java b/src/main/java/co/redeye/spring/challenge/services/AuthenticationException.java new file mode 100644 index 0000000..6cf9317 --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/services/AuthenticationException.java @@ -0,0 +1,11 @@ +package co.redeye.spring.challenge.services; + +/** + * Exception to be used for problems with authentication. + * The message will be delivered to the user. + */ +public class AuthenticationException extends Exception { + public AuthenticationException(String message) { + super(message); + } +} diff --git a/src/main/java/co/redeye/spring/challenge/services/AuthenticatorService.java b/src/main/java/co/redeye/spring/challenge/services/AuthenticatorService.java new file mode 100644 index 0000000..0692f02 --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/services/AuthenticatorService.java @@ -0,0 +1,90 @@ +package co.redeye.spring.challenge.services; + +import co.redeye.spring.challenge.db.User; +import co.redeye.spring.challenge.db.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Random; + +/** + * Handles all tasks related to user authentication. + */ +@Service +public class AuthenticatorService { + private static final int SALT_LENGTH = 64; + private static final int TOKEN_LENGTH = 32; + + // Constants for random String generation + private static final int UNIQUE_CHARACTERS = 10 + 26 + 26; //Numerals, lowercase and uppercase letters + private static final int NUMERAL_CUTOFF = 10; + private static final int LOWERCASE_CUTOFF = NUMERAL_CUTOFF + 26; + + @Autowired + private UserRepository userRepository; + + private final Random random; + private final MessageDigest hashEncoder; + + public AuthenticatorService() { + random = new Random(); + try { + hashEncoder = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException ignored) { + //Impossible + throw new RuntimeException(ignored); + } + } + + + /** + * Registers a new user. + * + * @param username The user's desired username. + * @param password The user's desired password. + * @return The user's authentication token. + */ + @Transactional + public String register(String username, String password) throws AuthenticationException { + User existingUser = userRepository.findByUsername(username); + if (existingUser != null) { + throw new AuthenticationException("This username has already been taken."); + } + + String salt = randomString(SALT_LENGTH); + String saltedPassword = new String(hashEncoder.digest((salt + password).getBytes())); + String token = randomString(TOKEN_LENGTH); + + User newUser = new User(); + newUser.setUsername(username); + newUser.setPassword(saltedPassword); + newUser.setSalt(salt); + newUser.setToken(token); + + newUser = userRepository.save(newUser); + return token; + } + + /** + * Generates a random string of alphanumberic characters + * + * @return The generated string. + */ + private String randomString(int length) { + StringBuilder salt = new StringBuilder(length); + for (int i = 0; i < length; ++i) { + int current = random.nextInt(10 + 26 + 26); //Numbers, lowercase and uppercase letters + if (current < NUMERAL_CUTOFF) { + salt.append((char) ('0' + current)); + } else if (current < LOWERCASE_CUTOFF) { + salt.append((char) ('a' + current - NUMERAL_CUTOFF)); + } else { + salt.append((char) ('A' + current - LOWERCASE_CUTOFF)); + } + } + return salt.toString(); + } +} diff --git a/src/main/java/co/redeye/spring/challenge/views/ErrorResponse.java b/src/main/java/co/redeye/spring/challenge/views/ErrorResponse.java new file mode 100644 index 0000000..3d30c02 --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/views/ErrorResponse.java @@ -0,0 +1,23 @@ +package co.redeye.spring.challenge.views; + +/** + * Standard response for when an error has occurred. + */ +public class ErrorResponse { + String message; + + public ErrorResponse() { + } + + public ErrorResponse(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/co/redeye/spring/challenge/views/LoginRequest.java b/src/main/java/co/redeye/spring/challenge/views/LoginRequest.java new file mode 100644 index 0000000..c89843f --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/views/LoginRequest.java @@ -0,0 +1,27 @@ +package co.redeye.spring.challenge.views; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a request to register/login + */ +public class LoginRequest { + private String username; + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/src/main/java/co/redeye/spring/challenge/views/LoginResponse.java b/src/main/java/co/redeye/spring/challenge/views/LoginResponse.java new file mode 100644 index 0000000..e798df1 --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/views/LoginResponse.java @@ -0,0 +1,16 @@ +package co.redeye.spring.challenge.views; + +/** + * Standard response object for login/registering + */ +public class LoginResponse { + private String token; + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} From ac14cf7ba6aff8ff443691476f0fba9dd2f5694a Mon Sep 17 00:00:00 2001 From: Matthew Johnson Date: Wed, 13 Jul 2016 16:43:49 +1000 Subject: [PATCH 02/10] Implemented authentication of existing users. --- .../controllers/AuthenticationController.java | 19 +++++---- .../services/AuthenticatorService.java | 41 ++++++++++++++++++- .../spring/challenge/views/LoginResponse.java | 4 ++ 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/main/java/co/redeye/spring/challenge/controllers/AuthenticationController.java b/src/main/java/co/redeye/spring/challenge/controllers/AuthenticationController.java index b60a700..cf98227 100644 --- a/src/main/java/co/redeye/spring/challenge/controllers/AuthenticationController.java +++ b/src/main/java/co/redeye/spring/challenge/controllers/AuthenticationController.java @@ -6,7 +6,6 @@ import co.redeye.spring.challenge.views.LoginRequest; import co.redeye.spring.challenge.views.LoginResponse; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @RestController @@ -16,13 +15,20 @@ public class AuthenticationController { @RequestMapping(value = "/account/register", method = RequestMethod.POST) @ResponseBody - public LoginResponse home(@RequestBody LoginRequest registerRequest) throws AuthenticationException { + public LoginResponse register(@RequestBody LoginRequest registerRequest) throws AuthenticationException { validate(registerRequest); String token = authenticator.register(registerRequest.getUsername(), registerRequest.getPassword()); - LoginResponse response = new LoginResponse(); - response.setToken(token); - return response; + return new LoginResponse(token); + } + + @RequestMapping(value = "/account/login", method = RequestMethod.POST) + @ResponseBody + public LoginResponse login(@RequestBody LoginRequest loginRequest) throws AuthenticationException { + validate(loginRequest); + + String token = authenticator.login(loginRequest.getUsername(), loginRequest.getPassword()); + return new LoginResponse(token); } private void validate(LoginRequest registerRequest) throws AuthenticationException { @@ -32,8 +38,5 @@ private void validate(LoginRequest registerRequest) throws AuthenticationExcepti if (!Utils.stringPresent(registerRequest.getPassword())) { throw new AuthenticationException("Invalid password."); } - } - - } diff --git a/src/main/java/co/redeye/spring/challenge/services/AuthenticatorService.java b/src/main/java/co/redeye/spring/challenge/services/AuthenticatorService.java index 0692f02..95e4f0c 100644 --- a/src/main/java/co/redeye/spring/challenge/services/AuthenticatorService.java +++ b/src/main/java/co/redeye/spring/challenge/services/AuthenticatorService.java @@ -46,6 +46,7 @@ public AuthenticatorService() { * @param username The user's desired username. * @param password The user's desired password. * @return The user's authentication token. + * @throws AuthenticationException When the specified username is taken. */ @Transactional public String register(String username, String password) throws AuthenticationException { @@ -55,7 +56,7 @@ public String register(String username, String password) throws AuthenticationEx } String salt = randomString(SALT_LENGTH); - String saltedPassword = new String(hashEncoder.digest((salt + password).getBytes())); + String saltedPassword = generateSaltedPassword(salt, password); String token = randomString(TOKEN_LENGTH); User newUser = new User(); @@ -68,6 +69,42 @@ public String register(String username, String password) throws AuthenticationEx return token; } + /** + * Authenticates a user and returns a token. + * + * @param username The user's username. + * @param password The user's password. + * @return The user's authentication token. + * @throws AuthenticationException When either credential is invalid. + */ + public String login(String username, String password) throws AuthenticationException { + User user = userRepository.findByUsername(username); + if (user == null) { + throw new AuthenticationException("Authentication error"); + } + + String saltedPassword = generateSaltedPassword(user.getSalt(), password); + if (!saltedPassword.equals(user.getPassword())) { + throw new AuthenticationException("Authentication error"); + } + + String token = randomString(TOKEN_LENGTH); + user.setToken(token); + userRepository.save(user); + return token; + } + + /** + * Generates the salted password hash. + * + * @param salt The salt to use on the password. + * @param password The user's password. + * @return The salted password hash. + */ + private String generateSaltedPassword(String salt, String password) { + return new String(hashEncoder.digest((salt + password).getBytes())); + } + /** * Generates a random string of alphanumberic characters * @@ -76,7 +113,7 @@ public String register(String username, String password) throws AuthenticationEx private String randomString(int length) { StringBuilder salt = new StringBuilder(length); for (int i = 0; i < length; ++i) { - int current = random.nextInt(10 + 26 + 26); //Numbers, lowercase and uppercase letters + int current = random.nextInt(UNIQUE_CHARACTERS); if (current < NUMERAL_CUTOFF) { salt.append((char) ('0' + current)); } else if (current < LOWERCASE_CUTOFF) { diff --git a/src/main/java/co/redeye/spring/challenge/views/LoginResponse.java b/src/main/java/co/redeye/spring/challenge/views/LoginResponse.java index e798df1..5a857cf 100644 --- a/src/main/java/co/redeye/spring/challenge/views/LoginResponse.java +++ b/src/main/java/co/redeye/spring/challenge/views/LoginResponse.java @@ -6,6 +6,10 @@ public class LoginResponse { private String token; + public LoginResponse(String token) { + this.token = token; + } + public String getToken() { return token; } From 38198e0320f69347f65ac182080be14be21289d6 Mon Sep 17 00:00:00 2001 From: Matthew Johnson Date: Thu, 14 Jul 2016 18:39:00 +1000 Subject: [PATCH 03/10] Implemented adding new list items and retrieving the entire list. --- pom.xml | 8 +++ .../controllers/AuthenticationController.java | 20 ++----- .../ExceptionHandlerController.java | 7 ++- .../controllers/TodoListController.java | 50 +++++++++++++++++ .../co/redeye/spring/challenge/db/Item.java | 56 +++++++++++++++++++ .../spring/challenge/db/ItemRepository.java | 12 ++++ .../co/redeye/spring/challenge/db/User.java | 33 ++++++++++- .../spring/challenge/db/UserRepository.java | 16 +++++- .../AuthenticationException.java | 4 +- .../exceptions/InvalidItemException.java | 10 ++++ .../challenge/exceptions/UserException.java | 11 ++++ .../services/AuthenticatorService.java | 39 ++++++++++++- .../spring/challenge/views/LoginRequest.java | 11 ++++ .../spring/challenge/views/TodoItem.java | 37 ++++++++++++ 14 files changed, 287 insertions(+), 27 deletions(-) create mode 100644 src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java create mode 100644 src/main/java/co/redeye/spring/challenge/db/Item.java create mode 100644 src/main/java/co/redeye/spring/challenge/db/ItemRepository.java rename src/main/java/co/redeye/spring/challenge/{services => exceptions}/AuthenticationException.java (64%) create mode 100644 src/main/java/co/redeye/spring/challenge/exceptions/InvalidItemException.java create mode 100644 src/main/java/co/redeye/spring/challenge/exceptions/UserException.java create mode 100644 src/main/java/co/redeye/spring/challenge/views/TodoItem.java diff --git a/pom.xml b/pom.xml index 9c77d24..214f655 100644 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,14 @@ ${start-class} + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + \ No newline at end of file diff --git a/src/main/java/co/redeye/spring/challenge/controllers/AuthenticationController.java b/src/main/java/co/redeye/spring/challenge/controllers/AuthenticationController.java index cf98227..299796c 100644 --- a/src/main/java/co/redeye/spring/challenge/controllers/AuthenticationController.java +++ b/src/main/java/co/redeye/spring/challenge/controllers/AuthenticationController.java @@ -1,7 +1,7 @@ package co.redeye.spring.challenge.controllers; import co.redeye.spring.challenge.Utils; -import co.redeye.spring.challenge.services.AuthenticationException; +import co.redeye.spring.challenge.exceptions.AuthenticationException; import co.redeye.spring.challenge.services.AuthenticatorService; import co.redeye.spring.challenge.views.LoginRequest; import co.redeye.spring.challenge.views.LoginResponse; @@ -9,34 +9,26 @@ import org.springframework.web.bind.annotation.*; @RestController +@RequestMapping("/account") public class AuthenticationController { @Autowired AuthenticatorService authenticator; - @RequestMapping(value = "/account/register", method = RequestMethod.POST) + @RequestMapping(value = "/register", method = RequestMethod.POST) @ResponseBody public LoginResponse register(@RequestBody LoginRequest registerRequest) throws AuthenticationException { - validate(registerRequest); + registerRequest.validate(); String token = authenticator.register(registerRequest.getUsername(), registerRequest.getPassword()); return new LoginResponse(token); } - @RequestMapping(value = "/account/login", method = RequestMethod.POST) + @RequestMapping(value = "/login", method = RequestMethod.POST) @ResponseBody public LoginResponse login(@RequestBody LoginRequest loginRequest) throws AuthenticationException { - validate(loginRequest); + loginRequest.validate(); String token = authenticator.login(loginRequest.getUsername(), loginRequest.getPassword()); return new LoginResponse(token); } - - private void validate(LoginRequest registerRequest) throws AuthenticationException { - if (!Utils.stringPresent(registerRequest.getUsername())) { - throw new AuthenticationException("Invalid username."); - } - if (!Utils.stringPresent(registerRequest.getPassword())) { - throw new AuthenticationException("Invalid password."); - } - } } diff --git a/src/main/java/co/redeye/spring/challenge/controllers/ExceptionHandlerController.java b/src/main/java/co/redeye/spring/challenge/controllers/ExceptionHandlerController.java index daaa15a..85393b6 100644 --- a/src/main/java/co/redeye/spring/challenge/controllers/ExceptionHandlerController.java +++ b/src/main/java/co/redeye/spring/challenge/controllers/ExceptionHandlerController.java @@ -1,6 +1,7 @@ package co.redeye.spring.challenge.controllers; -import co.redeye.spring.challenge.services.AuthenticationException; +import co.redeye.spring.challenge.exceptions.AuthenticationException; +import co.redeye.spring.challenge.exceptions.UserException; import co.redeye.spring.challenge.views.ErrorResponse; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -13,10 +14,10 @@ */ @ControllerAdvice public class ExceptionHandlerController { - @ExceptionHandler (value = AuthenticationException.class) + @ExceptionHandler (value = UserException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseBody - public ErrorResponse authenticationException(AuthenticationException e) { + public ErrorResponse authenticationException(UserException e) { return new ErrorResponse(e.getMessage()); } diff --git a/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java b/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java new file mode 100644 index 0000000..9e4107f --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java @@ -0,0 +1,50 @@ +package co.redeye.spring.challenge.controllers; + +import co.redeye.spring.challenge.db.User; +import co.redeye.spring.challenge.exceptions.AuthenticationException; +import co.redeye.spring.challenge.exceptions.UserException; +import co.redeye.spring.challenge.services.AuthenticatorService; +import co.redeye.spring.challenge.services.TodoListService; +import co.redeye.spring.challenge.views.TodoItem; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * Controller for handling all access of the user's to do list. + */ +@Controller +@RequestMapping("/todo") +public class TodoListController { + @Autowired + private TodoListService todoListService; + + /** + * Adds a new item to the authenticated user's to do list. + * + * @param newItem The item being added. + * @param authToken The user's authentication token + * @throws AuthenticationException If the authentication token is missing or invalid. + */ + @RequestMapping(value = "/", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.OK) + void newTask(@RequestBody TodoItem newItem, @RequestHeader("Authorization") String authToken) throws UserException { + newItem.validate(); + todoListService.addItem(authToken, newItem.getText(), newItem.isDone()); + } + + /** + * Retrieves the user's entire to do list + * + * @param authToken The user's authentication token + * @throws AuthenticationException If the authentication token is missing or invalid. + */ + @RequestMapping(value = "/", method = RequestMethod.GET) + @ResponseBody + List newTask(@RequestHeader("Authorization") String authToken) throws UserException { + return todoListService.getItems(authToken); + } +} diff --git a/src/main/java/co/redeye/spring/challenge/db/Item.java b/src/main/java/co/redeye/spring/challenge/db/Item.java new file mode 100644 index 0000000..649ae9d --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/db/Item.java @@ -0,0 +1,56 @@ +package co.redeye.spring.challenge.db; + +import javax.persistence.*; + +/** + * Represents a single to do list item for a user. + */ +@Entity +@Table(name = "items") +public class Item { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; + + @ManyToOne(targetEntity = User.class) + @JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false) + private User user; + + @Column(nullable = false) + private boolean done; + + @Column(nullable = false) + private String description; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public boolean isDone() { + return done; + } + + public void setDone(boolean done) { + this.done = done; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/src/main/java/co/redeye/spring/challenge/db/ItemRepository.java b/src/main/java/co/redeye/spring/challenge/db/ItemRepository.java new file mode 100644 index 0000000..f300c6f --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/db/ItemRepository.java @@ -0,0 +1,12 @@ +package co.redeye.spring.challenge.db; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +/** + * Provides custom database access methods we require. + */ +@Repository +public interface ItemRepository extends CrudRepository { + +} diff --git a/src/main/java/co/redeye/spring/challenge/db/User.java b/src/main/java/co/redeye/spring/challenge/db/User.java index 7b443ba..bbe2a05 100644 --- a/src/main/java/co/redeye/spring/challenge/db/User.java +++ b/src/main/java/co/redeye/spring/challenge/db/User.java @@ -15,7 +15,7 @@ public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) - private int id; + private long id; @Column(nullable = false, unique = true) private String username; @@ -29,11 +29,14 @@ public class User { @Column(nullable = false, unique = true) private String token; - public int getId() { + @OneToMany(mappedBy = "user", targetEntity = Item.class, cascade = CascadeType.REMOVE) + private List items; + + public long getId() { return id; } - public void setId(int id) { + public void setId(long id) { this.id = id; } @@ -68,4 +71,28 @@ public String getToken() { public void setToken(String token) { this.token = token; } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + User user = (User) o; + + return id == user.id; + + } + + @Override + public int hashCode() { + return (int) (id ^ (id >>> 32)); + } } diff --git a/src/main/java/co/redeye/spring/challenge/db/UserRepository.java b/src/main/java/co/redeye/spring/challenge/db/UserRepository.java index 60a022d..8e194a5 100644 --- a/src/main/java/co/redeye/spring/challenge/db/UserRepository.java +++ b/src/main/java/co/redeye/spring/challenge/db/UserRepository.java @@ -1,12 +1,24 @@ package co.redeye.spring.challenge.db; import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; /** - * Created by Matthew Johnson on 13/07/2016. + * Provides custom database access methods we require. Methods declared below are defined by Spring Framework magic. */ -public interface UserRepository extends CrudRepository { +@Repository +public interface UserRepository extends CrudRepository { + /** + * Retrieves the user with the given username. + * @param username The user to retrieve. + * @return The user or null if they do not exist. + */ User findByUsername(String username); + /** + * Retrieves the user currently using the given authentication token. + * @param token The user provided token. + * @return The user or null if no user is using this token. + */ User findByToken(String token); } diff --git a/src/main/java/co/redeye/spring/challenge/services/AuthenticationException.java b/src/main/java/co/redeye/spring/challenge/exceptions/AuthenticationException.java similarity index 64% rename from src/main/java/co/redeye/spring/challenge/services/AuthenticationException.java rename to src/main/java/co/redeye/spring/challenge/exceptions/AuthenticationException.java index 6cf9317..f7c2109 100644 --- a/src/main/java/co/redeye/spring/challenge/services/AuthenticationException.java +++ b/src/main/java/co/redeye/spring/challenge/exceptions/AuthenticationException.java @@ -1,10 +1,10 @@ -package co.redeye.spring.challenge.services; +package co.redeye.spring.challenge.exceptions; /** * Exception to be used for problems with authentication. * The message will be delivered to the user. */ -public class AuthenticationException extends Exception { +public class AuthenticationException extends UserException { public AuthenticationException(String message) { super(message); } diff --git a/src/main/java/co/redeye/spring/challenge/exceptions/InvalidItemException.java b/src/main/java/co/redeye/spring/challenge/exceptions/InvalidItemException.java new file mode 100644 index 0000000..7bdf192 --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/exceptions/InvalidItemException.java @@ -0,0 +1,10 @@ +package co.redeye.spring.challenge.exceptions; + +/** + * For when a user submits an invalid to do list item. + */ +public class InvalidItemException extends UserException { + public InvalidItemException(String message) { + super(message); + } +} diff --git a/src/main/java/co/redeye/spring/challenge/exceptions/UserException.java b/src/main/java/co/redeye/spring/challenge/exceptions/UserException.java new file mode 100644 index 0000000..78da5a5 --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/exceptions/UserException.java @@ -0,0 +1,11 @@ +package co.redeye.spring.challenge.exceptions; + +/** + * Base checked exception class for all exceptions caused by bad user input. + * The message attached to this must be appropriate to send to the end user. + */ +public class UserException extends Exception { + public UserException(String message) { + super(message); + } +} diff --git a/src/main/java/co/redeye/spring/challenge/services/AuthenticatorService.java b/src/main/java/co/redeye/spring/challenge/services/AuthenticatorService.java index 95e4f0c..e757b21 100644 --- a/src/main/java/co/redeye/spring/challenge/services/AuthenticatorService.java +++ b/src/main/java/co/redeye/spring/challenge/services/AuthenticatorService.java @@ -2,21 +2,26 @@ import co.redeye.spring.challenge.db.User; import co.redeye.spring.challenge.db.UserRepository; +import co.redeye.spring.challenge.exceptions.AuthenticationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import javax.transaction.Transactional; +import org.springframework.transaction.annotation.Transactional; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Handles all tasks related to user authentication. */ @Service public class AuthenticatorService { + private static final String BAD_AUTH_MESSAGE = "Authentication error"; private static final int SALT_LENGTH = 64; private static final int TOKEN_LENGTH = 32; + private static final Pattern TOKEN_VALIDATOR = Pattern.compile("Bearer ([a-zA-Z0-9]{" + TOKEN_LENGTH +"})"); // Constants for random String generation private static final int UNIQUE_CHARACTERS = 10 + 26 + 26; //Numerals, lowercase and uppercase letters @@ -77,15 +82,16 @@ public String register(String username, String password) throws AuthenticationEx * @return The user's authentication token. * @throws AuthenticationException When either credential is invalid. */ + @Transactional public String login(String username, String password) throws AuthenticationException { User user = userRepository.findByUsername(username); if (user == null) { - throw new AuthenticationException("Authentication error"); + throw new AuthenticationException(BAD_AUTH_MESSAGE); } String saltedPassword = generateSaltedPassword(user.getSalt(), password); if (!saltedPassword.equals(user.getPassword())) { - throw new AuthenticationException("Authentication error"); + throw new AuthenticationException(BAD_AUTH_MESSAGE); } String token = randomString(TOKEN_LENGTH); @@ -94,6 +100,33 @@ public String login(String username, String password) throws AuthenticationExcep return token; } + /** + * Validates a user login token and returns the user. + * + * @param authToken The user's authentication toekn. + * @return The user's account object if the token is valid. + * @throws AuthenticationException If the token is invalid. + */ + @Transactional(readOnly = true) + public User fromToken(String authToken) throws AuthenticationException { + if (authToken == null) { + throw new AuthenticationException(BAD_AUTH_MESSAGE); + } + + Matcher matcher = TOKEN_VALIDATOR.matcher(authToken); + if (!matcher.matches()) { + throw new AuthenticationException(BAD_AUTH_MESSAGE); + } + + String cleanedToken = matcher.group(1); + User user = userRepository.findByToken(cleanedToken); + if (user == null) { + throw new AuthenticationException(BAD_AUTH_MESSAGE); + } + + return user; + } + /** * Generates the salted password hash. * diff --git a/src/main/java/co/redeye/spring/challenge/views/LoginRequest.java b/src/main/java/co/redeye/spring/challenge/views/LoginRequest.java index c89843f..7554eb3 100644 --- a/src/main/java/co/redeye/spring/challenge/views/LoginRequest.java +++ b/src/main/java/co/redeye/spring/challenge/views/LoginRequest.java @@ -1,5 +1,7 @@ package co.redeye.spring.challenge.views; +import co.redeye.spring.challenge.Utils; +import co.redeye.spring.challenge.exceptions.AuthenticationException; import com.fasterxml.jackson.annotation.JsonProperty; /** @@ -24,4 +26,13 @@ public String getPassword() { public void setPassword(String password) { this.password = password; } + + public void validate() throws AuthenticationException { + if (!Utils.stringPresent(username)) { + throw new AuthenticationException("Invalid username."); + } + if (!Utils.stringPresent(password)) { + throw new AuthenticationException("Invalid password."); + } + } } diff --git a/src/main/java/co/redeye/spring/challenge/views/TodoItem.java b/src/main/java/co/redeye/spring/challenge/views/TodoItem.java new file mode 100644 index 0000000..795785d --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/views/TodoItem.java @@ -0,0 +1,37 @@ +package co.redeye.spring.challenge.views; + +import co.redeye.spring.challenge.Utils; +import co.redeye.spring.challenge.exceptions.InvalidItemException; + +/** + * Represents a single to do list item. + */ +public class TodoItem { + Boolean done; + String text; + + public boolean isDone() { + return done; + } + + public void setDone(boolean done) { + this.done = done; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public void validate() throws InvalidItemException { + if (!Utils.stringPresent(text)) { + throw new InvalidItemException("Missing field 'text'"); + } + if (done == null) { + throw new InvalidItemException("Missing field 'done' (true|false)"); + } + } +} From 1991fbdadd6172a487404d4da320ecef0c3920fb Mon Sep 17 00:00:00 2001 From: Matthew Johnson Date: Thu, 14 Jul 2016 21:08:13 +1000 Subject: [PATCH 04/10] Implemented editing of to do list items --- .../ExceptionHandlerController.java | 8 ++ .../controllers/TodoListController.java | 26 ++++-- .../co/redeye/spring/challenge/db/User.java | 16 ---- .../exceptions/IllegalItemException.java | 11 +++ .../challenge/services/ToDoListService.java | 86 +++++++++++++++++++ .../spring/challenge/views/TodoItem.java | 19 ++++ 6 files changed, 142 insertions(+), 24 deletions(-) create mode 100644 src/main/java/co/redeye/spring/challenge/exceptions/IllegalItemException.java create mode 100644 src/main/java/co/redeye/spring/challenge/services/ToDoListService.java diff --git a/src/main/java/co/redeye/spring/challenge/controllers/ExceptionHandlerController.java b/src/main/java/co/redeye/spring/challenge/controllers/ExceptionHandlerController.java index 85393b6..aaa96aa 100644 --- a/src/main/java/co/redeye/spring/challenge/controllers/ExceptionHandlerController.java +++ b/src/main/java/co/redeye/spring/challenge/controllers/ExceptionHandlerController.java @@ -1,6 +1,7 @@ package co.redeye.spring.challenge.controllers; import co.redeye.spring.challenge.exceptions.AuthenticationException; +import co.redeye.spring.challenge.exceptions.IllegalItemException; import co.redeye.spring.challenge.exceptions.UserException; import co.redeye.spring.challenge.views.ErrorResponse; import org.springframework.http.HttpStatus; @@ -14,6 +15,13 @@ */ @ControllerAdvice public class ExceptionHandlerController { + @ExceptionHandler (value = IllegalItemException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + @ResponseBody + public ErrorResponse illegalAccessException(IllegalItemException e) { + return new ErrorResponse(e.getMessage()); + } + @ExceptionHandler (value = UserException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseBody diff --git a/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java b/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java index 9e4107f..474bcdc 100644 --- a/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java +++ b/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java @@ -22,6 +22,18 @@ public class TodoListController { @Autowired private TodoListService todoListService; + /** + * Retrieves the user's entire to do list + * + * @param authToken The user's authentication token + * @throws AuthenticationException If the authentication token is missing or invalid. + */ + @RequestMapping(value = "/", method = RequestMethod.GET) + @ResponseBody + List newTask(@RequestHeader("Authorization") String authToken) throws UserException { + return todoListService.getItems(authToken); + } + /** * Adds a new item to the authenticated user's to do list. * @@ -37,14 +49,12 @@ void newTask(@RequestBody TodoItem newItem, @RequestHeader("Authorization") Stri } /** - * Retrieves the user's entire to do list - * - * @param authToken The user's authentication token - * @throws AuthenticationException If the authentication token is missing or invalid. + * Modifies an existing task. */ - @RequestMapping(value = "/", method = RequestMethod.GET) - @ResponseBody - List newTask(@RequestHeader("Authorization") String authToken) throws UserException { - return todoListService.getItems(authToken); + @RequestMapping(value = "/{id}", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.OK) + void editTask(@RequestBody TodoItem item, @RequestHeader("Authorization") String authToken, @PathVariable("id") long taskId) throws UserException { + item.validate(); + todoListService.editItem(authToken, taskId, item.getText(), item.isDone()); } } diff --git a/src/main/java/co/redeye/spring/challenge/db/User.java b/src/main/java/co/redeye/spring/challenge/db/User.java index bbe2a05..b945a6f 100644 --- a/src/main/java/co/redeye/spring/challenge/db/User.java +++ b/src/main/java/co/redeye/spring/challenge/db/User.java @@ -79,20 +79,4 @@ public List getItems() { public void setItems(List items) { this.items = items; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - User user = (User) o; - - return id == user.id; - - } - - @Override - public int hashCode() { - return (int) (id ^ (id >>> 32)); - } } diff --git a/src/main/java/co/redeye/spring/challenge/exceptions/IllegalItemException.java b/src/main/java/co/redeye/spring/challenge/exceptions/IllegalItemException.java new file mode 100644 index 0000000..38ecc94 --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/exceptions/IllegalItemException.java @@ -0,0 +1,11 @@ +package co.redeye.spring.challenge.exceptions; + +/** + * Exception class for users attempting to manipulate to do list items which they do not own. + * Results in a Forbidden Status. + */ +public class IllegalItemException extends UserException { + public IllegalItemException(String message) { + super(message); + } +} diff --git a/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java b/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java new file mode 100644 index 0000000..9cf6a17 --- /dev/null +++ b/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java @@ -0,0 +1,86 @@ +package co.redeye.spring.challenge.services; + +import co.redeye.spring.challenge.db.Item; +import co.redeye.spring.challenge.db.ItemRepository; +import co.redeye.spring.challenge.db.User; +import co.redeye.spring.challenge.exceptions.AuthenticationException; +import co.redeye.spring.challenge.exceptions.IllegalItemException; +import co.redeye.spring.challenge.exceptions.UserException; +import co.redeye.spring.challenge.views.TodoItem; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * All functionality related to the manipulation of to do lists. + */ +@Service +public class TodoListService { + @Autowired + private AuthenticatorService authenticatorService; + + @Autowired + private ItemRepository itemRepository; + + /** + * Gets all the items a user has in their to do list, complete and incomplete. + * + * @param token The user's authentication token + * @return A list of this user's to do tasks. + * @throws AuthenticationException + */ + @Transactional(readOnly = true) + public List getItems(String token) throws AuthenticationException { + User user = authenticatorService.fromToken(token); + + return user.getItems().stream() + .map(TodoItem::new) + .collect(Collectors.toList()); + } + + /** + * Adds a new list item for the current user + * + * @param token The user's authentication token + * @param desc The new task's description + * @param complete Whether the user has completed this task. + * @ + */ + @Transactional + public void addItem(String token, String desc, boolean complete) throws AuthenticationException { + User user = authenticatorService.fromToken(token); + + Item newItem = new Item(); + newItem.setUser(user); + newItem.setDescription(desc); + newItem.setDone(complete); + + itemRepository.save(newItem); + } + + /** + * Edits an existing item on a user's to do list. + * + * @param token The user's authentication token. + * @param taskId The id of the task. + * @param text The new text for the list item. + * @param done The new status for the item. + * @throws AuthenticationException If the user's token is invalid. + * @throws IllegalItemException If the specified task does not belong to the user. + */ + public void editItem(String token, long taskId, String text, boolean done) throws UserException { + User user = authenticatorService.fromToken(token); + Item item = itemRepository.findOne(taskId); + + if (!user.equals(item.getUser())) { + throw new IllegalItemException("This item does not belong to you."); + } + + item.setDone(done); + item.setDescription(text); + itemRepository.save(item); + } +} diff --git a/src/main/java/co/redeye/spring/challenge/views/TodoItem.java b/src/main/java/co/redeye/spring/challenge/views/TodoItem.java index 795785d..19b1969 100644 --- a/src/main/java/co/redeye/spring/challenge/views/TodoItem.java +++ b/src/main/java/co/redeye/spring/challenge/views/TodoItem.java @@ -1,15 +1,34 @@ package co.redeye.spring.challenge.views; import co.redeye.spring.challenge.Utils; +import co.redeye.spring.challenge.db.Item; import co.redeye.spring.challenge.exceptions.InvalidItemException; /** * Represents a single to do list item. */ public class TodoItem { + long id; Boolean done; String text; + public TodoItem() { + } + + public TodoItem(Item item) { + this.id = item.getId(); + this.done = item.isDone(); + this.text = item.getDescription(); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + public boolean isDone() { return done; } From 5c77dfa4aa6981253023df599457055da57535f2 Mon Sep 17 00:00:00 2001 From: Matthew Johnson Date: Thu, 14 Jul 2016 21:10:46 +1000 Subject: [PATCH 05/10] Added test suite --- Postman_test_suite | 202 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 Postman_test_suite diff --git a/Postman_test_suite b/Postman_test_suite new file mode 100644 index 0000000..40a3689 --- /dev/null +++ b/Postman_test_suite @@ -0,0 +1,202 @@ +{ + "id": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "name": "Redeye", + "description": "", + "order": [ + "28c8fdfc-2cef-b371-f52b-f23365432f02", + "6d5f6d6c-d935-3a08-3974-91fb4335fd2f", + "761ece2c-0fce-776e-048b-6dc50fdf7ad9", + "f1547a60-95af-f687-34c1-3a54923b12f3", + "40e6353e-da45-8e5e-95d2-b9790b7cada1", + "74e10085-91c2-f7fd-7c79-c11cc7297abc", + "3ae6234e-7b70-0fcd-6de5-e0c2bb1126da", + "e7797da5-2739-e224-4228-ebf033238ea9", + "b59e6244-f297-86dc-9e2f-ecfab73abb91" + ], + "folders": [], + "timestamp": 1468385917501, + "owner": "", + "remoteLink": "", + "public": false, + "published": false, + "requests": [ + { + "id": "28c8fdfc-2cef-b371-f52b-f23365432f02", + "headers": "Content-Type: application/json\n", + "url": "localhost:8080/account/register", + "preRequestScript": null, + "pathVariables": {}, + "method": "POST", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "tests[\"Status code is 200\"] = responseCode.code === 200;\nvar jsonData = JSON.parse(responseBody);\npostman.setGlobalVariable(\"authTokenRegister\", jsonData.token);\n", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468491701902, + "name": "register", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n\"username\": \"Steve\",\n\"password\": \"password\"\n}" + }, + { + "id": "3ae6234e-7b70-0fcd-6de5-e0c2bb1126da", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{authTokenRegister}}\n", + "url": "localhost:8080/todo/", + "preRequestScript": null, + "pathVariables": {}, + "method": "GET", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "var jsonData = JSON.parse(responseBody);\npostman.setGlobalVariable(\"itemId\", jsonData[0].id);\ntests[\"Body contains one element\"] = jsonData.length === 1;\ntests[\"Correct done value\"] = jsonData[0].done === false;\ntests[\"Correct text value\"] = jsonData[0].text === \"Buy bread\";", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468494372926, + "name": "Todo list contains item", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n\"text\": \"Build a teleporter\",\n\"done\": \"false\"\n}" + }, + { + "id": "40e6353e-da45-8e5e-95d2-b9790b7cada1", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{authTokenRegister}}\n", + "url": "localhost:8080/todo/", + "preRequestScript": null, + "pathVariables": {}, + "method": "GET", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "tests[\"Body is empty array\"] = responseBody === \"[]\";", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468493239702, + "name": "Empty todo list", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n\"text\": \"Build a teleporter\",\n\"done\": \"false\"\n}" + }, + { + "id": "6d5f6d6c-d935-3a08-3974-91fb4335fd2f", + "headers": "Content-Type: application/json\n", + "url": "localhost:8080/account/register", + "preRequestScript": null, + "pathVariables": {}, + "method": "POST", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "tests[\"Status code is 400\"] = responseCode.code === 400;", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468489162232, + "name": "register username taken", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n\"username\": \"Steve\",\n\"password\": \"password\"\n}" + }, + { + "id": "74e10085-91c2-f7fd-7c79-c11cc7297abc", + "headers": "Authorization: Bearer {{authTokenRegister}}\nContent-Type: application/json\n", + "url": "localhost:8080/todo/", + "preRequestScript": null, + "pathVariables": {}, + "method": "POST", + "data": [], + "dataMode": "raw", + "tests": "tests[\"Status code is 200\"] = responseCode.code === 200;", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468494040745, + "name": "Add to list", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n \"done\": false,\n \"text\": \"Buy bread\"\n}" + }, + { + "id": "761ece2c-0fce-776e-048b-6dc50fdf7ad9", + "headers": "Content-Type: application/json\n", + "url": "localhost:8080/account/register", + "preRequestScript": null, + "pathVariables": {}, + "method": "POST", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "tests[\"Status code is 400\"] = responseCode.code === 400;", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468490114816, + "name": "register missing username", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n \"password\": \"password\"\n}" + }, + { + "id": "b59e6244-f297-86dc-9e2f-ecfab73abb91", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{authTokenRegister}}\n", + "url": "localhost:8080/todo/", + "preRequestScript": null, + "pathVariables": {}, + "method": "GET", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "var jsonData = JSON.parse(responseBody);\npostman.setGlobalVariable(\"itemId\", jsonData[0].id);\ntests[\"Body contains one element\"] = jsonData.length === 1;\ntests[\"Correct done value\"] = jsonData[0].done === true;\ntests[\"Correct text value\"] = jsonData[0].text === \"Buy Bread\";", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468494394468, + "name": "Check edit succeeded", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n\"text\": \"Build a teleporter\",\n\"done\": \"false\"\n}" + }, + { + "id": "e7797da5-2739-e224-4228-ebf033238ea9", + "headers": "Authorization: Bearer {{authTokenRegister}}\nContent-Type: application/json\n", + "url": "localhost:8080/todo/{{itemId}}", + "preRequestScript": null, + "pathVariables": {}, + "method": "POST", + "data": [], + "dataMode": "raw", + "tests": "tests[\"Status code is 200\"] = responseCode.code === 200;", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468494185691, + "name": "Edit list item", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n \"done\": true,\n \"text\": \"Buy Bread\"\n}" + }, + { + "id": "f1547a60-95af-f687-34c1-3a54923b12f3", + "headers": "Content-Type: application/json\n", + "url": "localhost:8080/account/register", + "preRequestScript": null, + "pathVariables": {}, + "method": "POST", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "tests[\"Status code is 400\"] = responseCode.code === 400;", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468490118078, + "name": "register missing password", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n \"username\": \"Steve\"\n}" + } + ] +} \ No newline at end of file From 9df74c34f8f352d0c4f56cc3ef2b50d9333b6b39 Mon Sep 17 00:00:00 2001 From: Matthew Johnson Date: Fri, 15 Jul 2016 13:05:48 +1000 Subject: [PATCH 06/10] Implemented deletion of to do list items --- Postman_test_suite | 174 +++++++++++++++++- .../controllers/TodoListController.java | 28 ++- .../challenge/services/ToDoListService.java | 29 +++ 3 files changed, 220 insertions(+), 11 deletions(-) diff --git a/Postman_test_suite b/Postman_test_suite index 40a3689..c4e3110 100644 --- a/Postman_test_suite +++ b/Postman_test_suite @@ -10,8 +10,16 @@ "40e6353e-da45-8e5e-95d2-b9790b7cada1", "74e10085-91c2-f7fd-7c79-c11cc7297abc", "3ae6234e-7b70-0fcd-6de5-e0c2bb1126da", + "b8964b96-10c0-9b84-8c40-c1768455ef98", "e7797da5-2739-e224-4228-ebf033238ea9", - "b59e6244-f297-86dc-9e2f-ecfab73abb91" + "b59e6244-f297-86dc-9e2f-ecfab73abb91", + "18b7439e-36c0-7897-a897-f7fad7e77a4c", + "319f5cf7-d147-1b00-0097-c47ca1f5ca09", + "2b18d8b5-484e-1737-4d81-da25488451f0", + "79e0105c-47a1-44fd-2e3c-cc4fea2966da", + "4d9857fa-6a04-2897-eab9-42e14baa2387", + "c63e1e0e-b9d3-10ea-aca7-3bed45a16e9c", + "35407456-75f7-6572-475d-8bbe8f95742d" ], "folders": [], "timestamp": 1468385917501, @@ -20,6 +28,26 @@ "public": false, "published": false, "requests": [ + { + "id": "18b7439e-36c0-7897-a897-f7fad7e77a4c", + "headers": "Content-Type: application/json\n", + "url": "localhost:8080/account/register", + "preRequestScript": null, + "pathVariables": {}, + "method": "POST", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "tests[\"Status code is 200\"] = responseCode.code === 200;\nvar jsonData = JSON.parse(responseBody);\npostman.setGlobalVariable(\"authTokenRegister2\", jsonData.token);\n", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468545605620, + "name": "register second account", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n\"username\": \"Bill\",\n\"password\": \"password\"\n}" + }, { "id": "28c8fdfc-2cef-b371-f52b-f23365432f02", "headers": "Content-Type: application/json\n", @@ -40,6 +68,64 @@ "responses": [], "rawModeData": "{\n\"username\": \"Steve\",\n\"password\": \"password\"\n}" }, + { + "id": "2b18d8b5-484e-1737-4d81-da25488451f0", + "headers": "Authorization: Bearer {{authTokenRegister2}}\nContent-Type: application/json\n", + "url": "localhost:8080/todo/{{itemId}}", + "preRequestScript": null, + "pathVariables": {}, + "method": "DELETE", + "data": [], + "dataMode": "raw", + "tests": "tests[\"Status code is 403\"] = responseCode.code === 403;", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468548161431, + "name": "Delete another user's item", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "" + }, + { + "id": "319f5cf7-d147-1b00-0097-c47ca1f5ca09", + "headers": "Authorization: Bearer {{authTokenRegister2}}\nContent-Type: application/json\n", + "url": "localhost:8080/todo/{{itemId}}", + "preRequestScript": null, + "pathVariables": {}, + "method": "POST", + "data": [], + "dataMode": "raw", + "tests": "tests[\"Status code is 403\"] = responseCode.code === 403;", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468547232076, + "name": "Edit another user's item", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n \"done\": true,\n \"text\": \"Buy Bread\"\n}" + }, + { + "id": "35407456-75f7-6572-475d-8bbe8f95742d", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{authTokenRegister}}\n", + "url": "localhost:8080/todo/", + "preRequestScript": null, + "pathVariables": {}, + "method": "GET", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "var jsonData = JSON.parse(responseBody);\ntests[\"Body contains one element\"] = jsonData.length === 1;\ntests[\"Correct done value\"] = jsonData[0].done === true;\ntests[\"Correct text value\"] = jsonData[0].text === \"Buy Bread\";", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468548365022, + "name": "Check todo list contains 1 item", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n\"text\": \"Build a teleporter\",\n\"done\": \"false\"\n}" + }, { "id": "3ae6234e-7b70-0fcd-6de5-e0c2bb1126da", "headers": "Content-Type: application/json\nAuthorization: Bearer {{authTokenRegister}}\n", @@ -50,10 +136,10 @@ "data": [], "dataMode": "raw", "version": 2, - "tests": "var jsonData = JSON.parse(responseBody);\npostman.setGlobalVariable(\"itemId\", jsonData[0].id);\ntests[\"Body contains one element\"] = jsonData.length === 1;\ntests[\"Correct done value\"] = jsonData[0].done === false;\ntests[\"Correct text value\"] = jsonData[0].text === \"Buy bread\";", + "tests": "var jsonData = JSON.parse(responseBody);\ntests[\"Body contains one element\"] = jsonData.length === 1;\npostman.setGlobalVariable(\"itemId\", jsonData[0].id);\ntests[\"Correct done value\"] = jsonData[0].done === false;\ntests[\"Correct text value\"] = jsonData[0].text === \"Buy bread\";", "currentHelper": "normal", "helperAttributes": {}, - "time": 1468494372926, + "time": 1468549277302, "name": "Todo list contains item", "description": "", "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", @@ -80,6 +166,26 @@ "responses": [], "rawModeData": "{\n\"text\": \"Build a teleporter\",\n\"done\": \"false\"\n}" }, + { + "id": "4d9857fa-6a04-2897-eab9-42e14baa2387", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{authTokenRegister}}\n", + "url": "localhost:8080/todo/", + "preRequestScript": null, + "pathVariables": {}, + "method": "GET", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "var jsonData = JSON.parse(responseBody);\ntests[\"Body contains one element\"] = jsonData.length === 2;\ntests[\"Correct done value\"] = jsonData[0].done === true;\ntests[\"Correct text value\"] = jsonData[0].text === \"Buy Bread\";\npostman.setGlobalVariable(\"itemId2\", jsonData[1].id);\ntests[\"Correct done value2\"] = jsonData[1].done === false;\ntests[\"Correct text value2\"] = jsonData[1].text === \"Buy milk\";", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468548312891, + "name": "Check todo list contains 2 items", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n\"text\": \"Build a teleporter\",\n\"done\": \"false\"\n}" + }, { "id": "6d5f6d6c-d935-3a08-3974-91fb4335fd2f", "headers": "Content-Type: application/json\n", @@ -139,6 +245,26 @@ "responses": [], "rawModeData": "{\n \"password\": \"password\"\n}" }, + { + "id": "79e0105c-47a1-44fd-2e3c-cc4fea2966da", + "headers": "Content-Type: application/json\n", + "url": "localhost:8080/account/login", + "preRequestScript": null, + "pathVariables": {}, + "method": "POST", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "tests[\"Status code is 200\"] = responseCode.code === 200;\nvar jsonData = JSON.parse(responseBody);\npostman.setGlobalVariable(\"authTokenRegister\", jsonData.token);\n", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468548172863, + "name": "login as first user", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n\"username\": \"Steve\",\n\"password\": \"password\"\n}" + }, { "id": "b59e6244-f297-86dc-9e2f-ecfab73abb91", "headers": "Content-Type: application/json\nAuthorization: Bearer {{authTokenRegister}}\n", @@ -149,16 +275,54 @@ "data": [], "dataMode": "raw", "version": 2, - "tests": "var jsonData = JSON.parse(responseBody);\npostman.setGlobalVariable(\"itemId\", jsonData[0].id);\ntests[\"Body contains one element\"] = jsonData.length === 1;\ntests[\"Correct done value\"] = jsonData[0].done === true;\ntests[\"Correct text value\"] = jsonData[0].text === \"Buy Bread\";", + "tests": "var jsonData = JSON.parse(responseBody);\npostman.setGlobalVariable(\"itemId\", jsonData[0].id);\ntests[\"Body contains two elements\"] = jsonData.length === 2;\ntests[\"Correct done value\"] = jsonData[0].done === true;\ntests[\"Correct text value\"] = jsonData[0].text === \"Buy Bread\";", "currentHelper": "normal", "helperAttributes": {}, - "time": 1468494394468, + "time": 1468551537121, "name": "Check edit succeeded", "description": "", "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", "responses": [], "rawModeData": "{\n\"text\": \"Build a teleporter\",\n\"done\": \"false\"\n}" }, + { + "id": "b8964b96-10c0-9b84-8c40-c1768455ef98", + "headers": "Authorization: Bearer {{authTokenRegister}}\nContent-Type: application/json\n", + "url": "localhost:8080/todo/", + "preRequestScript": null, + "pathVariables": {}, + "method": "POST", + "data": [], + "dataMode": "raw", + "tests": "tests[\"Status code is 200\"] = responseCode.code === 200;", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468547282599, + "name": "Add second item to list", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n \"done\": false,\n \"text\": \"Buy milk\"\n}" + }, + { + "id": "c63e1e0e-b9d3-10ea-aca7-3bed45a16e9c", + "headers": "Authorization: Bearer {{authTokenRegister}}\nContent-Type: application/json\n", + "url": "localhost:8080/todo/{{itemId2}}", + "preRequestScript": null, + "pathVariables": {}, + "method": "DELETE", + "data": [], + "dataMode": "raw", + "tests": "tests[\"Status code is 200\"] = responseCode.code === 200;", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468548334514, + "name": "Delete item", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "" + }, { "id": "e7797da5-2739-e224-4228-ebf033238ea9", "headers": "Authorization: Bearer {{authTokenRegister}}\nContent-Type: application/json\n", diff --git a/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java b/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java index 474bcdc..67f7da0 100644 --- a/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java +++ b/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java @@ -1,9 +1,7 @@ package co.redeye.spring.challenge.controllers; -import co.redeye.spring.challenge.db.User; import co.redeye.spring.challenge.exceptions.AuthenticationException; import co.redeye.spring.challenge.exceptions.UserException; -import co.redeye.spring.challenge.services.AuthenticatorService; import co.redeye.spring.challenge.services.TodoListService; import co.redeye.spring.challenge.views.TodoItem; import org.springframework.beans.factory.annotation.Autowired; @@ -30,31 +28,49 @@ public class TodoListController { */ @RequestMapping(value = "/", method = RequestMethod.GET) @ResponseBody - List newTask(@RequestHeader("Authorization") String authToken) throws UserException { + public List newTask(@RequestHeader("Authorization") String authToken) throws UserException { return todoListService.getItems(authToken); } /** * Adds a new item to the authenticated user's to do list. * - * @param newItem The item being added. + * @param newItem The item being added. * @param authToken The user's authentication token * @throws AuthenticationException If the authentication token is missing or invalid. */ @RequestMapping(value = "/", method = RequestMethod.POST) @ResponseStatus(HttpStatus.OK) - void newTask(@RequestBody TodoItem newItem, @RequestHeader("Authorization") String authToken) throws UserException { + public void newTask(@RequestBody TodoItem newItem, @RequestHeader("Authorization") String authToken) throws UserException { newItem.validate(); todoListService.addItem(authToken, newItem.getText(), newItem.isDone()); } /** * Modifies an existing task. + * + * @param item The new values for the item. + * @param authToken The user's authentication token. + * @param taskId The id of the task being modified. + * @throws UserException If there is any problem with the request. */ @RequestMapping(value = "/{id}", method = RequestMethod.POST) @ResponseStatus(HttpStatus.OK) - void editTask(@RequestBody TodoItem item, @RequestHeader("Authorization") String authToken, @PathVariable("id") long taskId) throws UserException { + public void editTask(@RequestBody TodoItem item, @RequestHeader("Authorization") String authToken, @PathVariable("id") long taskId) throws UserException { item.validate(); todoListService.editItem(authToken, taskId, item.getText(), item.isDone()); } + + /** + * Completely removes an item from the user's to do list. + * + * @param authToken The user's authentication token. + * @param taskId The id of the task to be removed + * @throws UserException If there is an authentication issue or the task does not belong to the user. + */ + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + public void deleteTask(@RequestHeader("Authorization") String authToken, @PathVariable("id") long taskId) throws UserException { + todoListService.deleteItem(authToken, taskId); + } } diff --git a/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java b/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java index 9cf6a17..c98fa1d 100644 --- a/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java +++ b/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java @@ -5,6 +5,7 @@ import co.redeye.spring.challenge.db.User; import co.redeye.spring.challenge.exceptions.AuthenticationException; import co.redeye.spring.challenge.exceptions.IllegalItemException; +import co.redeye.spring.challenge.exceptions.InvalidItemException; import co.redeye.spring.challenge.exceptions.UserException; import co.redeye.spring.challenge.views.TodoItem; import org.springframework.beans.factory.annotation.Autowired; @@ -71,10 +72,15 @@ public void addItem(String token, String desc, boolean complete) throws Authenti * @throws AuthenticationException If the user's token is invalid. * @throws IllegalItemException If the specified task does not belong to the user. */ + @Transactional public void editItem(String token, long taskId, String text, boolean done) throws UserException { User user = authenticatorService.fromToken(token); Item item = itemRepository.findOne(taskId); + if (item == null) { + throw new InvalidItemException("The specified to do list item does not exist."); + } + if (!user.equals(item.getUser())) { throw new IllegalItemException("This item does not belong to you."); } @@ -83,4 +89,27 @@ public void editItem(String token, long taskId, String text, boolean done) throw item.setDescription(text); itemRepository.save(item); } + + /** + * Deletes a specified item from the current user's to do list. + * + * @param token The user's authentication token. + * @param taskId The id of the task to be removed. + * @throws UserException If there is an authentication issue or the + */ + @Transactional + public void deleteItem(String token, long taskId) throws UserException { + User user = authenticatorService.fromToken(token); + Item item = itemRepository.findOne(taskId); + + if (item == null) { + throw new InvalidItemException("The specified to do list item does not exist."); + } + + if (!user.equals(item.getUser())) { + throw new IllegalItemException("This item does not belong to you."); + } + + itemRepository.delete(item); + } } From c0783a9e21e22f0481da1566f6b2628aebc4b5cd Mon Sep 17 00:00:00 2001 From: Matthew Johnson Date: Fri, 15 Jul 2016 14:01:38 +1000 Subject: [PATCH 07/10] Implemented specificially getting active and inactive tasks. --- Postman_test_suite | 42 +++++++++++++++++++ .../controllers/TodoListController.java | 28 ++++++++++++- .../spring/challenge/db/ItemRepository.java | 11 ++++- .../challenge/services/ToDoListService.java | 42 ++++++++++++++++++- 4 files changed, 120 insertions(+), 3 deletions(-) diff --git a/Postman_test_suite b/Postman_test_suite index c4e3110..bf219f5 100644 --- a/Postman_test_suite +++ b/Postman_test_suite @@ -13,6 +13,8 @@ "b8964b96-10c0-9b84-8c40-c1768455ef98", "e7797da5-2739-e224-4228-ebf033238ea9", "b59e6244-f297-86dc-9e2f-ecfab73abb91", + "60d622bd-999b-7b51-4950-1e2f22d7bab0", + "3d389515-7454-c059-133a-f7b3252750e1", "18b7439e-36c0-7897-a897-f7fad7e77a4c", "319f5cf7-d147-1b00-0097-c47ca1f5ca09", "2b18d8b5-484e-1737-4d81-da25488451f0", @@ -146,6 +148,26 @@ "responses": [], "rawModeData": "{\n\"text\": \"Build a teleporter\",\n\"done\": \"false\"\n}" }, + { + "id": "3d389515-7454-c059-133a-f7b3252750e1", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{authTokenRegister}}\n", + "url": "localhost:8080/todo/inactive", + "preRequestScript": null, + "pathVariables": {}, + "method": "GET", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "tests[\"Status code is 200\"] = responseCode.code === 200;\nvar jsonData = JSON.parse(responseBody);\ntests[\"Body contains one element\"] = jsonData.length === 1;\nif (jsonData.length > 0) {\n tests[\"Correct done value\"] = jsonData[0].done === true;\n tests[\"Correct text value\"] = jsonData[0].text === \"Buy Bread\";\n}", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468555169686, + "name": "Test get inactive tasks", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n\"text\": \"Build a teleporter\",\n\"done\": \"false\"\n}" + }, { "id": "40e6353e-da45-8e5e-95d2-b9790b7cada1", "headers": "Content-Type: application/json\nAuthorization: Bearer {{authTokenRegister}}\n", @@ -186,6 +208,26 @@ "responses": [], "rawModeData": "{\n\"text\": \"Build a teleporter\",\n\"done\": \"false\"\n}" }, + { + "id": "60d622bd-999b-7b51-4950-1e2f22d7bab0", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{authTokenRegister}}\n", + "url": "localhost:8080/todo/active", + "preRequestScript": null, + "pathVariables": {}, + "method": "GET", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "tests[\"Status code is 200\"] = responseCode.code === 200;\nvar jsonData = JSON.parse(responseBody);\ntests[\"Body contains one element\"] = jsonData.length === 1;\nif (jsonData.length > 0) {\n tests[\"Correct done value\"] = jsonData[0].done === false;\n tests[\"Correct text value\"] = jsonData[0].text === \"Buy milk\";\n}", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468555112154, + "name": "Test get active tasks", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n\"text\": \"Build a teleporter\",\n\"done\": \"false\"\n}" + }, { "id": "6d5f6d6c-d935-3a08-3974-91fb4335fd2f", "headers": "Content-Type: application/json\n", diff --git a/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java b/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java index 67f7da0..c95ac6e 100644 --- a/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java +++ b/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java @@ -28,7 +28,7 @@ public class TodoListController { */ @RequestMapping(value = "/", method = RequestMethod.GET) @ResponseBody - public List newTask(@RequestHeader("Authorization") String authToken) throws UserException { + public List getTasks(@RequestHeader("Authorization") String authToken) throws UserException { return todoListService.getItems(authToken); } @@ -46,6 +46,32 @@ public void newTask(@RequestBody TodoItem newItem, @RequestHeader("Authorization todoListService.addItem(authToken, newItem.getText(), newItem.isDone()); } + /** + * Gets the user's incomplete items. + * + * @param authToken The user's authentication token. + * @return All of the user's items which are not done. + * @throws AuthenticationException If the user's token is invalid. + */ + @RequestMapping(value = "/active", method = RequestMethod.GET) + @ResponseBody + public List getActiveItems(@RequestHeader("Authorization") String authToken) throws AuthenticationException { + return todoListService.getIncompleteItems(authToken); + } + + /** + * Gets the user's complete items. + * + * @param authToken The user's authentication token. + * @return All of the user's items which are done. + * @throws AuthenticationException If the user's token is invalid. + */ + @RequestMapping(value = "/inactive", method = RequestMethod.GET) + @ResponseBody + public List getInactiveItems(@RequestHeader("Authorization") String authToken) throws AuthenticationException { + return todoListService.getCompleteItems(authToken); + } + /** * Modifies an existing task. * diff --git a/src/main/java/co/redeye/spring/challenge/db/ItemRepository.java b/src/main/java/co/redeye/spring/challenge/db/ItemRepository.java index f300c6f..a732219 100644 --- a/src/main/java/co/redeye/spring/challenge/db/ItemRepository.java +++ b/src/main/java/co/redeye/spring/challenge/db/ItemRepository.java @@ -3,10 +3,19 @@ import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; +import java.util.List; + /** * Provides custom database access methods we require. */ @Repository public interface ItemRepository extends CrudRepository { - + /** + * Query to retrieve all to do list items belonging to a specific user with a given status. + * + * @param user The user. + * @param done The item's status + * @return The user's to do list items with the given status. + */ + List findByUserAndDone(User user, boolean done); } diff --git a/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java b/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java index c98fa1d..84a8b94 100644 --- a/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java +++ b/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java @@ -95,7 +95,7 @@ public void editItem(String token, long taskId, String text, boolean done) throw * * @param token The user's authentication token. * @param taskId The id of the task to be removed. - * @throws UserException If there is an authentication issue or the + * @throws UserException If there is an issue with authentication or the item. */ @Transactional public void deleteItem(String token, long taskId) throws UserException { @@ -112,4 +112,44 @@ public void deleteItem(String token, long taskId) throws UserException { itemRepository.delete(item); } + + /** + * Gets all of the user's incomplete to do list items. + * + * @param token The user's authentication token. + * @throws AuthenticationException If the user's token is invalid. + * @return The user's incomplete items. + */ + public List getIncompleteItems(String token) throws AuthenticationException{ + return getItemsWithDoneStatus(token, false); + } + + /** + * Gets all of the user's complete to do list items. + * + * @param token The user's authentication token. + * @throws AuthenticationException If the user's token is invalid. + * @return The user's complete items. + */ + public List getCompleteItems(String token) throws AuthenticationException { + return getItemsWithDoneStatus(token, true); + } + + /** + * Method for handling requests for a user's complete/incomplete items. + * + * @param token The user's authentication token. + * @param doneStatus The desired status of the TodoItems to return. + * @throws AuthenticationException If the user's token is invalid. + * @return The user's items with the desired status. + */ + @Transactional + private List getItemsWithDoneStatus(String token, boolean doneStatus) throws AuthenticationException { + User user = authenticatorService.fromToken(token); + + return itemRepository.findByUserAndDone(user, doneStatus).stream() + .filter(item -> item.isDone() == doneStatus) + .map(TodoItem::new) + .collect(Collectors.toList()); + } } From 96a7d550a9ecdbcaa59b7f0e49170ab310a6863c Mon Sep 17 00:00:00 2001 From: Matthew Johnson Date: Fri, 15 Jul 2016 14:07:35 +1000 Subject: [PATCH 08/10] Updated readme with some important details --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c7f104d..cc5f284 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,9 @@ To run the project through maven: `mvn exec:java` ### Testing ### -By default, the application will be running on port 8080. \ No newline at end of file +By default, the application will be running on port 8080. + +This application does use a H2 SQL database using the persistance API. However, the application is still configured to +remake the database upon start up so user accounts and to do lists will not be persisted between runs. + +The included file `Postman_test_suite.json` can be imported into the Postman extension for Chrome to be run as an integration test suite. From 9800e833f1ebf73f519b3794ebe0d645930e49c4 Mon Sep 17 00:00:00 2001 From: Matthew Johnson Date: Fri, 15 Jul 2016 14:31:21 +1000 Subject: [PATCH 09/10] Implemented clear request and added tests. --- Postman_test_suite | 45 ++++++++++++++++++- .../controllers/TodoListController.java | 12 +++++ .../challenge/services/ToDoListService.java | 22 ++++++--- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/Postman_test_suite b/Postman_test_suite index bf219f5..9b4f9b1 100644 --- a/Postman_test_suite +++ b/Postman_test_suite @@ -21,11 +21,13 @@ "79e0105c-47a1-44fd-2e3c-cc4fea2966da", "4d9857fa-6a04-2897-eab9-42e14baa2387", "c63e1e0e-b9d3-10ea-aca7-3bed45a16e9c", - "35407456-75f7-6572-475d-8bbe8f95742d" + "35407456-75f7-6572-475d-8bbe8f95742d", + "1b61851b-eab8-084e-38bc-f83f5d7a1a65", + "a2d53da8-d963-74ee-85fd-b1527731daee" ], "folders": [], "timestamp": 1468385917501, - "owner": "", + "owner": 0, "remoteLink": "", "public": false, "published": false, @@ -50,6 +52,25 @@ "responses": [], "rawModeData": "{\n\"username\": \"Bill\",\n\"password\": \"password\"\n}" }, + { + "id": "1b61851b-eab8-084e-38bc-f83f5d7a1a65", + "headers": "Authorization: Bearer {{authTokenRegister}}\nContent-Type: application/json\n", + "url": "localhost:8080/todo/clear", + "preRequestScript": null, + "pathVariables": {}, + "method": "DELETE", + "data": [], + "dataMode": "raw", + "tests": "tests[\"Status code is 200\"] = responseCode.code === 200;", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468556010590, + "name": "Clear entire list", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "" + }, { "id": "28c8fdfc-2cef-b371-f52b-f23365432f02", "headers": "Content-Type: application/json\n", @@ -307,6 +328,26 @@ "responses": [], "rawModeData": "{\n\"username\": \"Steve\",\n\"password\": \"password\"\n}" }, + { + "id": "a2d53da8-d963-74ee-85fd-b1527731daee", + "headers": "Content-Type: application/json\nAuthorization: Bearer {{authTokenRegister}}\n", + "url": "localhost:8080/todo/", + "preRequestScript": null, + "pathVariables": {}, + "method": "GET", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": "tests[\"Status code is 200\"] = responseCode.code === 200;\ntests[\"Body is empty array\"] = responseBody === \"[]\";", + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1468556063063, + "name": "Check todo list is empty", + "description": "", + "collectionId": "bb4070c0-5f32-bc49-8839-86571f6424b4", + "responses": [], + "rawModeData": "{\n\"text\": \"Build a teleporter\",\n\"done\": \"false\"\n}" + }, { "id": "b59e6244-f297-86dc-9e2f-ecfab73abb91", "headers": "Content-Type: application/json\nAuthorization: Bearer {{authTokenRegister}}\n", diff --git a/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java b/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java index c95ac6e..2abc9b8 100644 --- a/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java +++ b/src/main/java/co/redeye/spring/challenge/controllers/TodoListController.java @@ -99,4 +99,16 @@ public void editTask(@RequestBody TodoItem item, @RequestHeader("Authorization") public void deleteTask(@RequestHeader("Authorization") String authToken, @PathVariable("id") long taskId) throws UserException { todoListService.deleteItem(authToken, taskId); } + + /** + * Completely removes all items user's to do list. + * + * @param authToken The user's authentication token. + * @throws AuthenticationException If there is an authentication issue. + */ + @RequestMapping(value = "/clear", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + public void deleteAllTasks(@RequestHeader("Authorization") String authToken) throws AuthenticationException { + todoListService.deleteAllItems(authToken); + } } diff --git a/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java b/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java index 84a8b94..ba3a0d1 100644 --- a/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java +++ b/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java @@ -117,10 +117,10 @@ public void deleteItem(String token, long taskId) throws UserException { * Gets all of the user's incomplete to do list items. * * @param token The user's authentication token. - * @throws AuthenticationException If the user's token is invalid. * @return The user's incomplete items. + * @throws AuthenticationException If the user's token is invalid. */ - public List getIncompleteItems(String token) throws AuthenticationException{ + public List getIncompleteItems(String token) throws AuthenticationException { return getItemsWithDoneStatus(token, false); } @@ -128,8 +128,8 @@ public List getIncompleteItems(String token) throws AuthenticationExce * Gets all of the user's complete to do list items. * * @param token The user's authentication token. - * @throws AuthenticationException If the user's token is invalid. * @return The user's complete items. + * @throws AuthenticationException If the user's token is invalid. */ public List getCompleteItems(String token) throws AuthenticationException { return getItemsWithDoneStatus(token, true); @@ -138,10 +138,10 @@ public List getCompleteItems(String token) throws AuthenticationExcept /** * Method for handling requests for a user's complete/incomplete items. * - * @param token The user's authentication token. + * @param token The user's authentication token. * @param doneStatus The desired status of the TodoItems to return. - * @throws AuthenticationException If the user's token is invalid. * @return The user's items with the desired status. + * @throws AuthenticationException If the user's token is invalid. */ @Transactional private List getItemsWithDoneStatus(String token, boolean doneStatus) throws AuthenticationException { @@ -152,4 +152,16 @@ private List getItemsWithDoneStatus(String token, boolean doneStatus) .map(TodoItem::new) .collect(Collectors.toList()); } + + /** + * Deletes all of the user's to do list items. + * + * @param token The user's authentication token. + * @throws AuthenticationException If the user's token is invalid. + */ + @Transactional + public void deleteAllItems(String token) throws AuthenticationException { + User user = authenticatorService.fromToken(token); + itemRepository.delete(user.getItems()); + } } From c9bf958c0d6a8def17f6141320f911a558d1d768 Mon Sep 17 00:00:00 2001 From: Matthew Johnson Date: Fri, 15 Jul 2016 15:03:38 +1000 Subject: [PATCH 10/10] Fixed a bad filename --- README.md | 3 +++ .../services/{ToDoListService.java => TodoListService.java} | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) rename src/main/java/co/redeye/spring/challenge/services/{ToDoListService.java => TodoListService.java} (99%) diff --git a/README.md b/README.md index cc5f284..5e38a2c 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,6 @@ This application does use a H2 SQL database using the persistance API. However, remake the database upon start up so user accounts and to do lists will not be persisted between runs. The included file `Postman_test_suite.json` can be imported into the Postman extension for Chrome to be run as an integration test suite. + +Note: Most of the intermittent commits will not compile due to a file rename not being committed by IntelliJ. Manually +renaming 'ToDoListService.java' to 'TodoListService.java' will fix this. \ No newline at end of file diff --git a/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java b/src/main/java/co/redeye/spring/challenge/services/TodoListService.java similarity index 99% rename from src/main/java/co/redeye/spring/challenge/services/ToDoListService.java rename to src/main/java/co/redeye/spring/challenge/services/TodoListService.java index ba3a0d1..e2f8dde 100644 --- a/src/main/java/co/redeye/spring/challenge/services/ToDoListService.java +++ b/src/main/java/co/redeye/spring/challenge/services/TodoListService.java @@ -22,7 +22,6 @@ public class TodoListService { @Autowired private AuthenticatorService authenticatorService; - @Autowired private ItemRepository itemRepository;