From eb1e549b09912bd4f3be952a5ce7784d822055c9 Mon Sep 17 00:00:00 2001 From: VergeDX Date: Sun, 10 Jan 2021 16:56:07 +0800 Subject: [PATCH] [+] Repo init, with node /register & Tests. --- HELP.md | 24 +++ README.md | 27 ++++ build.gradle | 76 +++++++++ settings.gradle | 8 + .../hydev/clock_api/ClockApiApplication.java | 11 ++ .../configuration/AddHeaderArgsResolver.java | 27 ++++ .../controller/RegistryController.java | 54 +++++++ .../controller/RegistryControllerAspect.java | 32 ++++ .../controller/RegistryExceptionHandler.java | 25 +++ .../java/org/hydev/clock_api/entity/User.java | 43 ++++++ .../org/hydev/clock_api/error/ErrorCode.java | 23 +++ .../clock_api/repository/UserRepository.java | 11 ++ .../resources/application-test.properties | 5 + src/main/resources/application.properties | 6 + .../org/hydev/clock_api/RegisterNodeTest.kt | 146 ++++++++++++++++++ src/test/java/org/hydev/clock_api/test.kt | 16 ++ 16 files changed, 534 insertions(+) create mode 100644 HELP.md create mode 100644 README.md create mode 100644 build.gradle create mode 100644 settings.gradle create mode 100644 src/main/java/org/hydev/clock_api/ClockApiApplication.java create mode 100644 src/main/java/org/hydev/clock_api/configuration/AddHeaderArgsResolver.java create mode 100644 src/main/java/org/hydev/clock_api/controller/RegistryController.java create mode 100644 src/main/java/org/hydev/clock_api/controller/RegistryControllerAspect.java create mode 100644 src/main/java/org/hydev/clock_api/controller/RegistryExceptionHandler.java create mode 100644 src/main/java/org/hydev/clock_api/entity/User.java create mode 100644 src/main/java/org/hydev/clock_api/error/ErrorCode.java create mode 100644 src/main/java/org/hydev/clock_api/repository/UserRepository.java create mode 100644 src/main/resources/application-test.properties create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/org/hydev/clock_api/RegisterNodeTest.kt create mode 100644 src/test/java/org/hydev/clock_api/test.kt diff --git a/HELP.md b/HELP.md new file mode 100644 index 0000000..909cf51 --- /dev/null +++ b/HELP.md @@ -0,0 +1,24 @@ +# Getting Started + +### Reference Documentation + +For further reference, please consider the following sections: + +* [Official Gradle documentation](https://docs.gradle.org) +* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.4.1/gradle-plugin/reference/html/) +* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.4.1/gradle-plugin/reference/html/#build-image) +* [Spring Web](https://docs.spring.io/spring-boot/docs/2.4.1/reference/htmlsingle/#boot-features-developing-web-applications) + +### Guides + +The following guides illustrate how to use some features concretely: + +* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) +* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) +* [Building REST services with Spring](https://spring.io/guides/tutorials/bookmarks/) + +### Additional Links + +These additional references should also help you: + +* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ce0be8 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# clock_api + +## Node + +**GET** `/register` + +| Name | Type | In | Description | +| :--------: | :------: | :------: | :---------------------------------------------------------: | +| `username` | `string` | `header` | User's name, should match the regex `/^[a-z0-9_-]{3,16}$/`. | +| `password` | `string` | `header` | Only password's md5 will be save. | + +**Response** + +| Scenario | Http Status | Type | Value | +| :------: | :------------------: | :----------: | :----------------------: | +| Success | `200 OK` | `string` | User's uuid | +| Failure | `406 NOT ACCEPTABLE` | `json array` | JSON Array of Error Code | + +## Error Code + +| Code | Description | +| :-----: | :-----------------------------: | +| `A0101` | `username` is null. | +| `A0102` | `password` is null | +| `A0111` | `username` not match the regex. | +| `A0112` | `password` not match the regex. | + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..b2b1f1d --- /dev/null +++ b/build.gradle @@ -0,0 +1,76 @@ +plugins { + id 'org.springframework.boot' version '2.4.1' + id 'io.spring.dependency-management' version '1.0.10.RELEASE' + id 'java' + + // https://github.com/ben-manes/gradle-versions-plugin + id "com.github.ben-manes.versions" version "0.36.0" + + // Use Kotlin in Spring testing. + id 'org.jetbrains.kotlin.jvm' version '1.4.30-M1' +} + +group = 'org.hydev' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '11' + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() + maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } +} + +dependencies { + // Spring Web + implementation 'org.springframework.boot:spring-boot-starter-web' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // Lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + // https://my.oschina.net/mingyuelab/blog/3190313 + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation('org.xerial:sqlite-jdbc:3.34.0') + implementation('com.github.gwenn:sqlite-dialect:0.1.1') + + // https://www.baeldung.com/java-bean-validation-not-null-empty-blank + implementation('org.hibernate:hibernate-validator:7.0.0.Final') + + // If I need @Slf4J (Lombok) in testing. + // testCompileOnly('org.projectlombok:lombok') + // testAnnotationProcessor('org.projectlombok:lombok') + + // Try to use Jackson...? + // implementation "com.google.code.gson:gson:2.8.6" + + // Using Kotlin in testing. + testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + + // About the warning in Slf4J, Add jackson Kotlin support. + // implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + + // Spring Security. + // implementation 'org.springframework.boot:spring-boot-starter-security' +} + +test { + useJUnitPlatform() +} + +compileKotlin { + kotlinOptions { + jvmTarget = "11" + } +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = "11" + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..307aca4 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,8 @@ +pluginManagement { + repositories { + maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } + mavenCentral() + maven { url 'https://plugins.gradle.org/m2/' } + } +} +rootProject.name = 'clock_api' diff --git a/src/main/java/org/hydev/clock_api/ClockApiApplication.java b/src/main/java/org/hydev/clock_api/ClockApiApplication.java new file mode 100644 index 0000000..aea9c9b --- /dev/null +++ b/src/main/java/org/hydev/clock_api/ClockApiApplication.java @@ -0,0 +1,11 @@ +package org.hydev.clock_api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ClockApiApplication { + public static void main(String[] args) { + SpringApplication.run(ClockApiApplication.class, args); + } +} diff --git a/src/main/java/org/hydev/clock_api/configuration/AddHeaderArgsResolver.java b/src/main/java/org/hydev/clock_api/configuration/AddHeaderArgsResolver.java new file mode 100644 index 0000000..8054022 --- /dev/null +++ b/src/main/java/org/hydev/clock_api/configuration/AddHeaderArgsResolver.java @@ -0,0 +1,27 @@ +package org.hydev.clock_api.configuration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class AddHeaderArgsResolver implements WebMvcConfigurer { + private final ConfigurableListableBeanFactory configurableListableBeanFactory; + + @Autowired + public AddHeaderArgsResolver(ConfigurableListableBeanFactory configurableListableBeanFactory) { + this.configurableListableBeanFactory = configurableListableBeanFactory; + } + + @Override + // https://github.com/spring-projects/spring-framework/issues/23838 + // https://stackoverflow.com/questions/49305099/requestheader-not-binding-in-pojo-but-binding-only-in-variable + public void addArgumentResolvers(List resolvers) { + resolvers.add(new RequestHeaderMethodArgumentResolver(configurableListableBeanFactory)); + } +} diff --git a/src/main/java/org/hydev/clock_api/controller/RegistryController.java b/src/main/java/org/hydev/clock_api/controller/RegistryController.java new file mode 100644 index 0000000..071a8b6 --- /dev/null +++ b/src/main/java/org/hydev/clock_api/controller/RegistryController.java @@ -0,0 +1,54 @@ +package org.hydev.clock_api.controller; + +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.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.RestController; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +@Validated +@RestController +public class RegistryController { + private final UserRepository userRepository; + + @Autowired + public RegistryController(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @PostMapping("/register") + // https://www.baeldung.com/spring-rest-http-headers + // TODO: This method should be synchronized to avoid race condition. + // Also, this method should not be private, or else cannot use userRepository. + public synchronized ResponseEntity register( + // username & password shouldn't be null, and should match thr regex. + // [!] @RequestHeader(required = false) makes no need make another error handler. + // [!] And also, ExceptionHandler of MissingRequestHeaderException cannot deal with all missing fields. + @Pattern(regexp = User.RE_USERNAME, message = ErrorCode.USER_NAME_NOT_MATCH_REGEX) + @NotNull(message = ErrorCode.USER_NAME_IS_NULL) @RequestHeader(required = false) String username, + @Pattern(regexp = User.RE_PASSWORD, message = ErrorCode.USER_PASSWORD_NOT_MATCH_REGEX) + @NotNull(message = ErrorCode.USER_PASSWORD_IS_NULL) @RequestHeader(required = false) String password + ) { + // First, spring will check args. If not pass there regex, raise ConstraintViolationException. + // Then, the aspect will check username if already exists. + + User user = new User(); + user.setUsername(username); + + // TODO: Using Spring Security instead. + user.setPasswordMd5(DigestUtils.md5DigestAsHex(password.getBytes()).toLowerCase()); + + // After save and flush, uuid field will be generated automatically. + userRepository.saveAndFlush(user); + return ResponseEntity.ok(user.getUuid()); + } +} diff --git a/src/main/java/org/hydev/clock_api/controller/RegistryControllerAspect.java b/src/main/java/org/hydev/clock_api/controller/RegistryControllerAspect.java new file mode 100644 index 0000000..8037804 --- /dev/null +++ b/src/main/java/org/hydev/clock_api/controller/RegistryControllerAspect.java @@ -0,0 +1,32 @@ +package org.hydev.clock_api.controller; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.hydev.clock_api.error.ErrorCode; +import org.hydev.clock_api.repository.UserRepository; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Aspect +@Component +public class RegistryControllerAspect { + private final UserRepository userRepository; + + public RegistryControllerAspect(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Around(value = "execution(* RegistryController.register(String, String)) " + + "&& args(username, password)", argNames = "pjp, username, password") + // Even if I'm not use the password argument, I still should write it out! + private Object checkUsernameExists(ProceedingJoinPoint pjp, String username, String password) throws Throwable { + if (userRepository.existsByUsername(username)) + return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE) + .body(List.of(ErrorCode.USER_NAME_ALREADY_EXISTS)); + return pjp.proceed(); + } +} diff --git a/src/main/java/org/hydev/clock_api/controller/RegistryExceptionHandler.java b/src/main/java/org/hydev/clock_api/controller/RegistryExceptionHandler.java new file mode 100644 index 0000000..a731b4b --- /dev/null +++ b/src/main/java/org/hydev/clock_api/controller/RegistryExceptionHandler.java @@ -0,0 +1,25 @@ +package org.hydev.clock_api.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.util.List; +import java.util.stream.Collectors; + +@RestControllerAdvice +public class RegistryExceptionHandler { + @ExceptionHandler(ConstraintViolationException.class) + // ConstraintViolationException will be wrapped to TransactionSystemException! + // https://stackoverflow.com/questions/45070642/springboot-doesnt-handle-org-hibernate-exception-constraintviolationexception + private ResponseEntity> handleConstraintViolationException(ConstraintViolationException tse) { + List errorMessages = tse.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage).sorted() + .collect(Collectors.toList()); + + return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).body(errorMessages); + } +} diff --git a/src/main/java/org/hydev/clock_api/entity/User.java b/src/main/java/org/hydev/clock_api/entity/User.java new file mode 100644 index 0000000..ae62d2b --- /dev/null +++ b/src/main/java/org/hydev/clock_api/entity/User.java @@ -0,0 +1,43 @@ +package org.hydev.clock_api.entity; + +import lombok.Data; +import org.hibernate.annotations.GenericGenerator; +import org.hydev.clock_api.error.ErrorCode; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +@Data +@Entity(name = "users") +public class User { + // https://digitalfortress.tech/tricks/top-15-commonly-used-regex/ + + // https://www.regexpal.com/?fam=104030 + public static final String RE_USERNAME = "^[a-z0-9_-]{3,16}$"; + // https://www.regexpal.com/?fam=104029 + public static final String RE_PASSWORD = "(?=(.*[0-9]))((?=.*[A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z]))^.{8,}$"; + + // https://stackoverflow.com/questions/21517102/regex-to-match-md5-hashes + // https://stackoverflow.com/questions/45153520/are-md5-hashes-always-either-capital-or-lowercase + public static final String RE_LOWER_MD5 = "^[a-f0-9]{32}$"; + + // https://stackoverflow.com/questions/45635827/how-do-i-stop-spring-data-jpa-from-doing-a-select-before-a-save + @Id + + // Define a generator, and use it. + // https://thorben-janssen.com/generate-uuids-primary-keys-hibernate/ + @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator") + @GeneratedValue(generator = "UUID") + private String uuid; + + @NotNull(message = ErrorCode.INNER_USERNAME_IS_NULL) + @Pattern(regexp = RE_USERNAME, message = ErrorCode.INNER_USERNAME_NOT_MATCH_REGEX) + private String username; + + @NotNull(message = ErrorCode.INNER_PASSWORD_MD5_IS_NULL) + @Pattern(regexp = RE_LOWER_MD5, message = ErrorCode.INNER_PASSWORD_MD5_NOT_MATCH_REGEX) + private String passwordMd5; +} diff --git a/src/main/java/org/hydev/clock_api/error/ErrorCode.java b/src/main/java/org/hydev/clock_api/error/ErrorCode.java new file mode 100644 index 0000000..237d51f --- /dev/null +++ b/src/main/java/org/hydev/clock_api/error/ErrorCode.java @@ -0,0 +1,23 @@ +package org.hydev.clock_api.error; + + +public interface ErrorCode { + // Missing field in header. + String USER_NAME_IS_NULL = "A0101"; + String USER_PASSWORD_IS_NULL = "A0102"; + + // Field not match regex. + String USER_NAME_NOT_MATCH_REGEX = "A0111"; + String USER_PASSWORD_NOT_MATCH_REGEX = "A0112"; + + // Field already exists. + String USER_NAME_ALREADY_EXISTS = "A0121"; + + // Inner system field is null. + String INNER_USERNAME_IS_NULL = "B0101"; + String INNER_PASSWORD_MD5_IS_NULL = "B0102"; + + // Inner system field not match regex. + String INNER_USERNAME_NOT_MATCH_REGEX = "B0111"; + String INNER_PASSWORD_MD5_NOT_MATCH_REGEX = "B0112"; +} diff --git a/src/main/java/org/hydev/clock_api/repository/UserRepository.java b/src/main/java/org/hydev/clock_api/repository/UserRepository.java new file mode 100644 index 0000000..1a357d5 --- /dev/null +++ b/src/main/java/org/hydev/clock_api/repository/UserRepository.java @@ -0,0 +1,11 @@ +package org.hydev.clock_api.repository; + +import org.hydev.clock_api.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { + // https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods + boolean existsByUsername(String username); +} diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties new file mode 100644 index 0000000..b2e3186 --- /dev/null +++ b/src/main/resources/application-test.properties @@ -0,0 +1,5 @@ +spring.datasource.driver-class-name=org.sqlite.JDBC +spring.datasource.url=jdbc:sqlite:test.db +# For testing, table will be drop after test. +# https://blog.csdn.net/qq_36666651/article/details/80719259 +spring.jpa.hibernate.ddl-auto=create-drop diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..2612c7e --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,6 @@ +spring.datasource.driver-class-name=org.sqlite.JDBC +spring.datasource.url=jdbc:sqlite:clock-api.db +# https://blog.csdn.net/qq_36666651/article/details/80719259 +spring.jpa.hibernate.ddl-auto=update +# https://blog.csdn.net/qq_35981283/article/details/88994092 +spring.jpa.open-in-view=true diff --git a/src/test/java/org/hydev/clock_api/RegisterNodeTest.kt b/src/test/java/org/hydev/clock_api/RegisterNodeTest.kt new file mode 100644 index 0000000..0641f34 --- /dev/null +++ b/src/test/java/org/hydev/clock_api/RegisterNodeTest.kt @@ -0,0 +1,146 @@ +package org.hydev.clock_api + +import org.hamcrest.Matchers +import org.hydev.clock_api.entity.User +import org.hydev.clock_api.error.ErrorCode.* +import org.hydev.clock_api.repository.UserRepository +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.* +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.HttpEntity +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.MvcResult +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.util.DigestUtils +import org.springframework.util.LinkedMultiValueMap +import java.util.* +import javax.validation.ConstraintViolationException + +// https://stackoverflow.com/questions/59097035/springboottest-vs-webmvctest-datajpatest-service-unit-tests-what-is-the-b + +// https://stackoverflow.com/questions/45653753/how-to-tell-spring-boot-to-use-another-db-for-test +// Need RANDOM_PORT to inject TestRestTemplate. +// https://stackoverflow.com/questions/39213531/spring-boot-test-unable-to-inject-testresttemplate-and-mockmvc +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) + +// https://stackoverflow.com/questions/45653753/how-to-tell-spring-boot-to-use-another-db-for-test +@ActiveProfiles("test") + +// https://spring.io/guides/gs/testing-web/ +@AutoConfigureMockMvc +class RegisterNodeTest { + companion object { + private const val TEST_NODE = "/register" + + private const val H_USERNAME = "username" + private const val V_USERNAME = "vanilla" + + private const val H_PASSWORD = "password" + private const val V_PASSWORD = "P1ssW0rd" + private val V_PASSWORD_MD5 = DigestUtils.md5DigestAsHex(V_PASSWORD.toByteArray()) + + // https://stackoverflow.com/questions/37615731/java-regex-for-uuid + private const val R_UUID = "([a-f0-9]{8}(-[a-f0-9]{4}){4}[a-f0-9]{8})" + } + + @Autowired + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var userRepository: UserRepository + + @Autowired + private lateinit var restTemplate: TestRestTemplate + + // Post with headers, expect 406 and ErrorCodes. + // todo: Using List instead of Array. + private fun pWHsE406AECs(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) + + // Expect http status is 406 NOT ACCEPTABLE, and ErrorCode array are same. + assertEquals(HttpStatus.NOT_ACCEPTABLE, responseEntity.statusCode) + assertArrayEquals(expectedECList, responseEntity.body) + } + + @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)) + } + + @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( + mapOf(H_USERNAME to "", H_PASSWORD to ""), + arrayOf(USER_NAME_NOT_MATCH_REGEX, USER_PASSWORD_NOT_MATCH_REGEX) + ) + } + + @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)) + .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)) + .andExpect(status().isNotAcceptable) + .andExpect(content().json(String.format("[\"%s\"]", USER_NAME_ALREADY_EXISTS))) + userRepository.deleteById(tempUuid) + } + } + + // -- Begin inner system test. + // Get ConstraintViolationException contains error messages set. + private fun ConstraintViolationException.getCveMsgSet(): Set { + return this.constraintViolations.map { it.message }.toSet() + } + + // Insert custom User, and assert CVE with ErrorCodes. + private fun iUACVEWECs(user: User, cveSet: Set) { + // Expect throw error, and message set = given cve set. + val cve = assertThrows(ConstraintViolationException::class.java) { userRepository.saveAndFlush(user) } + assertEquals(cveSet, cve.getCveMsgSet()) + } + + @Test + // [B0101, B0102, B0101 + B0102] M1 * 2 + M2. + fun innerTestWhenFieldIsNull() { + iUACVEWECs(User().apply { passwordMd5 = V_PASSWORD_MD5 }, setOf(INNER_USERNAME_IS_NULL)) + iUACVEWECs(User().apply { username = V_USERNAME }, setOf(INNER_PASSWORD_MD5_IS_NULL)) + iUACVEWECs(User(), setOf(INNER_USERNAME_IS_NULL, INNER_PASSWORD_MD5_IS_NULL)) + } + + @Test + // [B0111, B0112, B0111 + B0112] W1 * 2 + W2. + fun innerTestWhenFieldNotMatchRegex() { + iUACVEWECs(User().apply { username = ""; passwordMd5 = V_PASSWORD_MD5 }, setOf(INNER_USERNAME_NOT_MATCH_REGEX)) + iUACVEWECs(User().apply { username = V_USERNAME; passwordMd5 = "" }, setOf(INNER_PASSWORD_MD5_NOT_MATCH_REGEX)) + iUACVEWECs( + User().apply { username = ""; passwordMd5 = "" }, + setOf(INNER_USERNAME_NOT_MATCH_REGEX, INNER_PASSWORD_MD5_NOT_MATCH_REGEX) + ) + } +} diff --git a/src/test/java/org/hydev/clock_api/test.kt b/src/test/java/org/hydev/clock_api/test.kt new file mode 100644 index 0000000..363695d --- /dev/null +++ b/src/test/java/org/hydev/clock_api/test.kt @@ -0,0 +1,16 @@ +package org.hydev.clock_api + +import com.fasterxml.jackson.databind.ObjectMapper +import java.util.* + +fun main() { + println(listOf("A0101", "A0102")) + println() + + val testJsonArray = "[\"A1\", \"A2\"]" + val objectMapper = ObjectMapper() + val stringArray = objectMapper.readValue(testJsonArray, Array::class.java) + println(Arrays.toString(stringArray)) + + println() +}