diff --git a/src/main/java/org/hydev/clock_api/controller/RegistryController.java b/src/main/java/org/hydev/clock_api/controller/RegistryController.java index 071a8b6..6d9849a 100644 --- a/src/main/java/org/hydev/clock_api/controller/RegistryController.java +++ b/src/main/java/org/hydev/clock_api/controller/RegistryController.java @@ -4,12 +4,13 @@ import org.hydev.clock_api.entity.User; import org.hydev.clock_api.error.ErrorCode; import org.hydev.clock_api.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.DigestUtils; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.constraints.NotNull; @@ -17,6 +18,7 @@ import javax.validation.constraints.Pattern; @Validated @RestController +@RequestMapping("/user") public class RegistryController { private final UserRepository userRepository; @@ -43,12 +45,29 @@ public class RegistryController { User user = new User(); user.setUsername(username); - - // TODO: Using Spring Security instead. - user.setPasswordMd5(DigestUtils.md5DigestAsHex(password.getBytes()).toLowerCase()); + user.setPasswordMd5(userToSaltedMd5(username, password)); // After save and flush, uuid field will be generated automatically. userRepository.saveAndFlush(user); return ResponseEntity.ok(user.getUuid()); } + + // Format: "$username + $password".toLowerMd5(); + private String userToSaltedMd5(String username, String password) { + String beforeMd5 = String.format("%s + %s", username, password); + return DigestUtils.md5DigestAsHex(beforeMd5.getBytes()).toLowerCase(); + } + + @PostMapping("/delete") + public synchronized ResponseEntity delete(@RequestHeader String username, @RequestHeader String password) { + User user = userRepository.queryByUsername(username); + if (user == null) return ResponseEntity.notFound().build(); + + if (user.getPasswordMd5().equals(userToSaltedMd5(username, password))) { + userRepository.delete(user); + return ResponseEntity.ok(""); + } + + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(""); + } } diff --git a/src/main/java/org/hydev/clock_api/repository/UserRepository.java b/src/main/java/org/hydev/clock_api/repository/UserRepository.java index 1a357d5..0f1ba82 100644 --- a/src/main/java/org/hydev/clock_api/repository/UserRepository.java +++ b/src/main/java/org/hydev/clock_api/repository/UserRepository.java @@ -8,4 +8,5 @@ import org.springframework.stereotype.Repository; public interface UserRepository extends JpaRepository { // https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods boolean existsByUsername(String username); + User queryByUsername(String username); } diff --git a/src/test/java/org/hydev/clock_api/RegisterNodeTest.kt b/src/test/java/org/hydev/clock_api/UserRegisterDeleteNodeTest.kt similarity index 66% rename from src/test/java/org/hydev/clock_api/RegisterNodeTest.kt rename to src/test/java/org/hydev/clock_api/UserRegisterDeleteNodeTest.kt index 0641f34..1f87c3c 100644 --- a/src/test/java/org/hydev/clock_api/RegisterNodeTest.kt +++ b/src/test/java/org/hydev/clock_api/UserRegisterDeleteNodeTest.kt @@ -37,9 +37,11 @@ import javax.validation.ConstraintViolationException // https://spring.io/guides/gs/testing-web/ @AutoConfigureMockMvc -class RegisterNodeTest { +class UserRegisterDeleteNodeTest { companion object { - private const val TEST_NODE = "/register" + private const val TEST_NODE = "/user" + private const val REGISTER_NODE = "${TEST_NODE}/register" + private const val DELETE_NODE = "${TEST_NODE}/delete" private const val H_USERNAME = "username" private const val V_USERNAME = "vanilla" @@ -61,16 +63,17 @@ class RegisterNodeTest { @Autowired private lateinit var restTemplate: TestRestTemplate - // Post with headers, expect 406 and ErrorCodes. + // Post to register node with headers, expect 406 and ErrorCodes. // todo: Using List instead of Array. - private fun pWHsE406AECs(headerMap: Map, expectedECList: Array) { + private fun pTRWHsE406AECs(headerMap: Map, expectedECList: Array) { val tempMultiValueMap = LinkedMultiValueMap() headerMap.forEach { tempMultiValueMap[it.key] = listOf(it.value) } val httpEntity = HttpEntity(tempMultiValueMap) // Using exchange to custom headers, etc. Args: (node, method, headers, forObject). // https://stackoverflow.com/questions/16781680/http-get-with-headers-using-resttemplate - val responseEntity = restTemplate.exchange(TEST_NODE, HttpMethod.POST, httpEntity, Array::class.java) + val responseEntity = + restTemplate.exchange(REGISTER_NODE, HttpMethod.POST, httpEntity, Array::class.java) // Expect http status is 406 NOT ACCEPTABLE, and ErrorCode array are same. assertEquals(HttpStatus.NOT_ACCEPTABLE, responseEntity.statusCode) @@ -80,17 +83,17 @@ class RegisterNodeTest { @Test // [A0101, A0102, A0101 + A0102] M1 * 2 + M2. fun testWhenMissingField() { - pWHsE406AECs(mapOf(H_PASSWORD to V_PASSWORD), arrayOf(USER_NAME_IS_NULL)) - pWHsE406AECs(mapOf(H_USERNAME to V_USERNAME), arrayOf(USER_PASSWORD_IS_NULL)) - pWHsE406AECs(mapOf(), arrayOf(USER_NAME_IS_NULL, USER_PASSWORD_IS_NULL)) + pTRWHsE406AECs(mapOf(H_PASSWORD to V_PASSWORD), arrayOf(USER_NAME_IS_NULL)) + pTRWHsE406AECs(mapOf(H_USERNAME to V_USERNAME), arrayOf(USER_PASSWORD_IS_NULL)) + pTRWHsE406AECs(mapOf(), arrayOf(USER_NAME_IS_NULL, USER_PASSWORD_IS_NULL)) } @Test // [A0111, A0112, A0111 + A0112] W1 * 2 + W2. fun testWhenNotMatchRegex() { - pWHsE406AECs(mapOf(H_USERNAME to "", H_PASSWORD to V_PASSWORD), arrayOf(USER_NAME_NOT_MATCH_REGEX)) - pWHsE406AECs(mapOf(H_USERNAME to V_USERNAME, H_PASSWORD to ""), arrayOf(USER_PASSWORD_NOT_MATCH_REGEX)) - pWHsE406AECs( + pTRWHsE406AECs(mapOf(H_USERNAME to "", H_PASSWORD to V_PASSWORD), arrayOf(USER_NAME_NOT_MATCH_REGEX)) + pTRWHsE406AECs(mapOf(H_USERNAME to V_USERNAME, H_PASSWORD to ""), arrayOf(USER_PASSWORD_NOT_MATCH_REGEX)) + pTRWHsE406AECs( mapOf(H_USERNAME to "", H_PASSWORD to ""), arrayOf(USER_NAME_NOT_MATCH_REGEX, USER_PASSWORD_NOT_MATCH_REGEX) ) @@ -99,13 +102,13 @@ class RegisterNodeTest { @Test // [A0121] Insert user, check it if already exists. fun testWhenUserAlreadyExists() { - mockMvc.perform(post(TEST_NODE).header(H_USERNAME, V_USERNAME).header(H_PASSWORD, V_PASSWORD)) + mockMvc.perform(post(REGISTER_NODE).header(H_USERNAME, V_USERNAME).header(H_PASSWORD, V_PASSWORD)) .andExpect(status().isOk) .andExpect(content().string(Matchers.matchesPattern(R_UUID))) .andDo { result: MvcResult -> // Notice: inserted user should be delete. val tempUuid = result.response.contentAsString - mockMvc.perform(post(TEST_NODE).header(H_USERNAME, V_USERNAME).header(H_PASSWORD, V_PASSWORD)) + mockMvc.perform(post(REGISTER_NODE).header(H_USERNAME, V_USERNAME).header(H_PASSWORD, V_PASSWORD)) .andExpect(status().isNotAcceptable) .andExpect(content().json(String.format("[\"%s\"]", USER_NAME_ALREADY_EXISTS))) userRepository.deleteById(tempUuid) @@ -143,4 +146,34 @@ class RegisterNodeTest { setOf(INNER_USERNAME_NOT_MATCH_REGEX, INNER_PASSWORD_MD5_NOT_MATCH_REGEX) ) } + + // Post to delete node with headers and expect HttpStatus. + private fun pTDWHsAEHS(headerMap: Map, expectedHttpStatus: HttpStatus) { + val tempMultiValueMap = LinkedMultiValueMap() + headerMap.forEach { tempMultiValueMap[it.key] = listOf(it.value) } + val httpEntity = HttpEntity(tempMultiValueMap) + + val responseEntity = restTemplate.exchange(DELETE_NODE, HttpMethod.POST, httpEntity, String::class.java) + assertEquals(expectedHttpStatus, responseEntity.statusCode) + } + + @Test + fun testDeleteUser() { + mockMvc.perform(post(REGISTER_NODE).header(H_USERNAME, V_USERNAME).header(H_PASSWORD, V_PASSWORD)) + .andExpect(status().isOk) + .andExpect(content().string(Matchers.matchesPattern(R_UUID))) + .andDo { + // Missing headers, 400 bad request. + pTDWHsAEHS(mapOf(H_USERNAME to ""), HttpStatus.BAD_REQUEST) + // Username not exist, 404 not found. + pTDWHsAEHS(mapOf(H_USERNAME to "", H_PASSWORD to ""), HttpStatus.NOT_FOUND) + // Username exist, but password not match. 401 unauthorized. + pTDWHsAEHS(mapOf(H_USERNAME to V_USERNAME, H_PASSWORD to ""), HttpStatus.UNAUTHORIZED) + // Username & password matched, delete user and 200 ok. + pTDWHsAEHS(mapOf(H_USERNAME to V_USERNAME, H_PASSWORD to V_PASSWORD), HttpStatus.OK) + + // And assert user is gone. + assertEquals(false, userRepository.existsByUsername(V_USERNAME)) + } + } }