[+] Finish backend
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.hydev.clock_api
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
open class ClockApiApplication
|
||||
|
||||
fun main(args: Array<String>)
|
||||
{
|
||||
runApplication<ClockApiApplication>(*args)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.hydev.clock_api
|
||||
|
||||
import org.hydev.clock_api.entity.User
|
||||
import org.hydev.clock_api.repository.UserRepository
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.util.DigestUtils
|
||||
import org.springframework.web.bind.annotation.RequestHeader
|
||||
|
||||
|
||||
|
||||
typealias H = RequestHeader
|
||||
typealias Str = String
|
||||
typealias Bool = Boolean
|
||||
|
||||
fun <T> T.http(code: Int): ResponseEntity<T> = ResponseEntity.status(code).body<T>(this)
|
||||
fun pinHash(fid: Long, pin: Str) = DigestUtils.md5DigestAsHex("$fid-$pin".toByteArray()).toLowerCase()
|
||||
fun <T> List<T>.csv() = joinToString(";")
|
||||
fun Str.csv() = split(";").toMutableList().apply { removeIf { it.isEmpty() } }
|
||||
|
||||
/**
|
||||
* Create salted hash for user's password
|
||||
* Format: "$username + $password".toLowerMd5();
|
||||
*
|
||||
* @param username Unique username used as a salt
|
||||
* @param password Password initial hash
|
||||
* @return Salted hash
|
||||
*/
|
||||
fun userToSaltedMd5(username: String?, password: String?): String
|
||||
{
|
||||
val beforeMd5 = String.format("%s + %s", username, password)
|
||||
return DigestUtils.md5DigestAsHex(beforeMd5.toByteArray()).toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Login and do callback
|
||||
*
|
||||
* @param username
|
||||
* @param password
|
||||
* @param callback
|
||||
* @return Execution result
|
||||
*/
|
||||
fun UserRepository.login(username: Str, password: Str, callback: (User) -> Any?): Any
|
||||
{
|
||||
// Verify login
|
||||
val user: User = queryByUsername(username) ?: return "Username not found".http(404)
|
||||
if (user.passwordMd5 != userToSaltedMd5(username, password)) return "Login invalid".http(401)
|
||||
|
||||
return callback(user) ?: "Success"
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package org.hydev.clock_api.controller
|
||||
|
||||
import org.hydev.clock_api.*
|
||||
import org.hydev.clock_api.entity.Family
|
||||
import org.hydev.clock_api.entity.FamilyRepo
|
||||
import org.hydev.clock_api.entity.User
|
||||
import org.hydev.clock_api.repository.UserRepository
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/family")
|
||||
open class FamilyController(private val userRepo: UserRepository, private val familyRepo: FamilyRepo)
|
||||
{
|
||||
/**
|
||||
* Login, check family, and callback
|
||||
*
|
||||
* @param username
|
||||
* @param password
|
||||
* @param fid Family ID
|
||||
* @param pin Family Pin
|
||||
* @param checkInFamily Verify the user is in the family or not
|
||||
* @param callback
|
||||
* @return Execution result
|
||||
*/
|
||||
private fun family(username: Str, password: Str, fid: Long, pin: Str, checkInFamily: Bool = true, callback: (User, Family) -> Any?): Any
|
||||
{
|
||||
return userRepo.login(username, password) { user ->
|
||||
|
||||
val family = familyRepo.findByIdOrNull(fid) ?: return@login "Family not found".http(404)
|
||||
|
||||
// Verify
|
||||
if (checkInFamily && user.family != fid) return@login "You're not in this family.".http(400)
|
||||
if (family.pinHash != pinHash(fid, pin)) return@login "Family pin incorrect.".http(401)
|
||||
|
||||
// Done
|
||||
callback(user, family) ?: "Success"
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/get")
|
||||
fun get(@H username: Str, @H password: Str): Any
|
||||
{
|
||||
return userRepo.login(username, password)
|
||||
{
|
||||
if (it.family == null) "You didn't join any family".http(404)
|
||||
else familyRepo.findByIdOrNull(it.family)!!
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/create")
|
||||
fun create(@H username: Str, @H password: Str, @H name: Str, @H pin: Str): Any
|
||||
{
|
||||
return userRepo.login(username, password) { user ->
|
||||
|
||||
// Verify that the user isn't already in a family
|
||||
if (user.family != null) return@login "You already have a family.".http(400)
|
||||
|
||||
// Create family and save once to get ID
|
||||
var family = Family(name = name, pinHash = "", members = username)
|
||||
family = familyRepo.save(family)
|
||||
|
||||
// Create pin hash using id and save again
|
||||
family.pinHash = pinHash(family.fid, pin)
|
||||
familyRepo.save(family)
|
||||
|
||||
// Set user's family
|
||||
user.family = family.fid
|
||||
userRepo.save(user)
|
||||
|
||||
// Done
|
||||
family
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/update_pin")
|
||||
fun updatePin(@H username: Str, @H password: Str, @H fid: Long, @H pin: Str, @H newPin: Str): Any
|
||||
{
|
||||
return family(username, password, fid, pin) { _, family ->
|
||||
|
||||
// Change pin
|
||||
family.pinHash = pinHash(family.fid, newPin)
|
||||
familyRepo.save(family)
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/action")
|
||||
fun action(@H username: Str, @H password: Str, @H fid: Long, @H pin: Str, @H action: Str): Any
|
||||
{
|
||||
return family(username, password, fid, pin, checkInFamily = action.toLowerCase() != "join") { user, family ->
|
||||
|
||||
// Do action
|
||||
when (action.toLowerCase())
|
||||
{
|
||||
"join" ->
|
||||
{
|
||||
if (user.family != null) return@family "You're already in a family.".http(400)
|
||||
user.family = fid
|
||||
userRepo.save(user)
|
||||
family.members = family.members.csv().apply { add(username) }.csv()
|
||||
familyRepo.save(family)
|
||||
}
|
||||
"delete" ->
|
||||
{
|
||||
// TODO: Test this
|
||||
family.members.csv().forEach {
|
||||
val member = userRepo.queryByUsername(it) ?: return@forEach
|
||||
member.family = null
|
||||
userRepo.save(member)
|
||||
}
|
||||
familyRepo.delete(family)
|
||||
family
|
||||
}
|
||||
"leave" ->
|
||||
{
|
||||
// TODO: Test this
|
||||
user.family = null
|
||||
userRepo.save(user)
|
||||
family.members = family.members.csv().apply { remove(username) }.csv()
|
||||
familyRepo.save(family)
|
||||
}
|
||||
else -> "".http(404)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/add_alarm")
|
||||
fun addAlarm(@H username: Str, @H password: Str, @H fid: Long, @H pin: Str, @H to: Str, @H alarm: Str): Any
|
||||
{
|
||||
return family(username, password, fid, pin) { _, _ ->
|
||||
|
||||
val toUser = userRepo.queryByUsername(to) ?: return@family "User not found in database".http(404)
|
||||
if (toUser.family != fid) return@family "User not in your family".http(401)
|
||||
|
||||
toUser.notifications = toUser.notifications.csv().apply { add(alarm) }.csv()
|
||||
userRepo.save(toUser)
|
||||
"Success"
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/get_alarm_updates")
|
||||
fun getUpdates(@H username: Str, @H password: Str): Any
|
||||
{
|
||||
return userRepo.login(username, password) { user ->
|
||||
|
||||
// TODO: test this
|
||||
val response = user.notifications
|
||||
user.notifications = ""
|
||||
userRepo.save(user)
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
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.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
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.Pattern;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
public class UserController {
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
public UserController(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create salted hash for user's password
|
||||
* Format: "$username + $password".toLowerMd5();
|
||||
*
|
||||
* @param username Unique username used as a salt
|
||||
* @param password Password initial hash
|
||||
* @return Salted hash
|
||||
*/
|
||||
private static String userToSaltedMd5(String username, String password) {
|
||||
String beforeMd5 = String.format("%s + %s", username, password);
|
||||
return DigestUtils.md5DigestAsHex(beforeMd5.getBytes()).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a user to the database.
|
||||
* <p>
|
||||
* https://www.baeldung.com/spring-rest-http-headers
|
||||
* This method should be synchronized to avoid race condition.
|
||||
* Also, this method should not be private, or else cannot use userRepository.
|
||||
* <p>
|
||||
* TODO: 2021/1/22 Need a better design!
|
||||
* Controller Return error code list as List<String>, or return uuid as String.
|
||||
*
|
||||
* @param username Unique username (Not empty, and should match the regex {@code User.RE_USERNAME})
|
||||
* @param password Password initial hash (Not empty)
|
||||
* @return Success or error
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
@SuppressWarnings("rawtypes")
|
||||
public synchronized ResponseEntity register(
|
||||
// [!] @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)
|
||||
@RequestHeader String username,
|
||||
@Pattern(regexp = User.RE_PASSWORD, message = ErrorCode.USER_PASSWORD_NOT_MATCH_REGEX)
|
||||
@RequestHeader String password
|
||||
) {
|
||||
// First, spring will check args. If not pass there regex, raise ConstraintViolationException.
|
||||
// Then, we check if username not exists. If username already exists, return ErrorCode with 409 conflict.
|
||||
if (userRepository.existsByUsername(username))
|
||||
// TODO: 2021/1/22 Using library instead of hand make.
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT).body(List.of(ErrorCode.USER_NAME_ALREADY_EXISTS));
|
||||
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setPasswordMd5(userToSaltedMd5(username, password));
|
||||
|
||||
// After save and flush, uuid field will be generated automatically.
|
||||
userRepository.saveAndFlush(user);
|
||||
return ResponseEntity.ok(user.getUuid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check username & password.
|
||||
* - User doesn't exist -> http 404
|
||||
* - Password doesn't match -> http 401
|
||||
* - All match -> Execute operation and return the resulting String.
|
||||
*
|
||||
* @param username Unique username
|
||||
* @param password Password initial hash
|
||||
* @param operation Callback on success
|
||||
* @return Callback result or the error response
|
||||
*/
|
||||
private ResponseEntity<String> checkPasswordAndDo(String username, String password,
|
||||
Function<User, String> operation) {
|
||||
User user = userRepository.queryByUsername(username);
|
||||
if (user == null) return ResponseEntity.notFound().build();
|
||||
|
||||
if (!user.getPasswordMd5().equals(userToSaltedMd5(username, password)))
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||
|
||||
String result = operation.apply(user);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
public synchronized ResponseEntity<String> delete(@RequestHeader String username, @RequestHeader String password) {
|
||||
return checkPasswordAndDo(username, password, user -> {
|
||||
userRepository.delete(user);
|
||||
return "";
|
||||
});
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public synchronized ResponseEntity<String> login(@RequestHeader String username, @RequestHeader String password) {
|
||||
return checkPasswordAndDo(username, password, user -> userRepository.queryByUsername(username).getUuid());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.hydev.clock_api.controller
|
||||
|
||||
import org.hydev.clock_api.*
|
||||
import org.hydev.clock_api.entity.FamilyRepo
|
||||
import org.hydev.clock_api.entity.User
|
||||
import org.hydev.clock_api.error.ErrorCode
|
||||
import org.hydev.clock_api.repository.UserRepository
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import javax.validation.constraints.Pattern
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
open class UserController(private val userRepo: UserRepository, private val familyRepo: FamilyRepo)
|
||||
{
|
||||
/**
|
||||
* Register a user to the database.
|
||||
*
|
||||
*
|
||||
* https://www.baeldung.com/spring-rest-http-headers
|
||||
* This method should be synchronized to avoid race condition.
|
||||
* Also, this method should not be private, or else cannot use userRepository.
|
||||
*
|
||||
*
|
||||
* TODO: 2021/1/22 Need a better design!
|
||||
* Controller Return error code list as List<String>, or return uuid as String.
|
||||
*
|
||||
* @param username Unique username (Not empty, and should match the regex `User.RE_USERNAME`)
|
||||
* @param password Password initial hash (Not empty)
|
||||
* @return Success or error
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
@Synchronized
|
||||
fun register(
|
||||
// [!] @H(required = false) makes no need make another error handler.
|
||||
// [!] And also, ExceptionHandler of MissingHException cannot deal with all missing fields.
|
||||
@H username: @Pattern(regexp = User.RE_USERNAME, message = ErrorCode.USER_NAME_NOT_MATCH_REGEX) Str,
|
||||
@H password: @Pattern(regexp = User.RE_PASSWORD, message = ErrorCode.USER_PASSWORD_NOT_MATCH_REGEX) Str): Any
|
||||
{
|
||||
// First, spring will check args. If not pass there regex, raise ConstraintViolationException.
|
||||
// Then, we check if username not exists. If username already exists, return ErrorCode with 409 conflict.
|
||||
// TODO: 2021/1/22 Using library instead of hand make.
|
||||
if (userRepo.existsByUsername(username)) return listOf(ErrorCode.USER_NAME_ALREADY_EXISTS).http(409)
|
||||
|
||||
val user = User().apply {
|
||||
this.username = username
|
||||
passwordMd5 = userToSaltedMd5(username, password)
|
||||
}
|
||||
|
||||
// After save and flush, uuid field will be generated automatically.
|
||||
userRepo.saveAndFlush(user)
|
||||
return user.uuid
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Synchronized
|
||||
fun delete(@H username: Str, @H password: Str): Any
|
||||
{
|
||||
return userRepo.login(username, password) { user ->
|
||||
|
||||
// Remove user from family
|
||||
if (user.family != null)
|
||||
{
|
||||
val family = familyRepo.findByIdOrNull(user.family)
|
||||
if (family != null)
|
||||
{
|
||||
family.members = family.members.csv().apply { remove(username) }.csv()
|
||||
familyRepo.save(family)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove user
|
||||
userRepo.delete(user)
|
||||
|
||||
// Done
|
||||
"Success"
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
@Synchronized
|
||||
fun login(@H username: Str, @H password: Str): Any
|
||||
{
|
||||
return userRepo.login(username, password) { it.uuid }
|
||||
}
|
||||
|
||||
@PostMapping("/backup/upload")
|
||||
fun backupUpload(@H username: Str, @H password: Str, @H config: Str): Any
|
||||
{
|
||||
return userRepo.login(username, password) { user ->
|
||||
|
||||
user.backup = config
|
||||
userRepo.save(user)
|
||||
"Success"
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/backup/download")
|
||||
fun backupDownload(@H username: Str, @H password: Str): Any
|
||||
{
|
||||
return userRepo.login(username, password) { user -> user.backup }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.hydev.clock_api.entity
|
||||
|
||||
import com.sun.istack.NotNull
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.GeneratedValue
|
||||
import javax.persistence.Id
|
||||
|
||||
/**
|
||||
* TODO: Write a description for this class!
|
||||
*
|
||||
* @author HyDEV Team (https://github.com/HyDevelop)
|
||||
* @author Hykilpikonna (https://github.com/hykilpikonna)
|
||||
* @author Vanilla (https://github.com/VergeDX)
|
||||
* @since 2021-01-24 10:18
|
||||
*/
|
||||
@Entity(name = "family")
|
||||
data class Family(
|
||||
@Id
|
||||
@GeneratedValue
|
||||
var fid: Long = 0,
|
||||
|
||||
@NotNull
|
||||
var name: String = "",
|
||||
|
||||
@NotNull
|
||||
var members: String = "",
|
||||
|
||||
@NotNull
|
||||
var pinHash: String = "",
|
||||
)
|
||||
|
||||
interface FamilyRepo: JpaRepository<Family, Long>
|
||||
@@ -4,13 +4,14 @@ import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
import org.hydev.clock_api.error.ErrorCode;
|
||||
import org.w3c.dom.stylesheets.LinkStyle;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Entity(name = "users")
|
||||
@@ -18,7 +19,7 @@ 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}$";
|
||||
public static final String RE_USERNAME = "^[A-Za-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,}$";
|
||||
|
||||
@@ -33,17 +34,27 @@ public class User {
|
||||
// https://thorben-janssen.com/generate-uuids-primary-keys-hibernate/
|
||||
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
|
||||
@GeneratedValue(generator = "UUID")
|
||||
private String uuid;
|
||||
public String uuid;
|
||||
|
||||
@NotNull(message = ErrorCode.INNER_USERNAME_IS_NULL)
|
||||
@Pattern(regexp = RE_USERNAME, message = ErrorCode.INNER_USERNAME_NOT_MATCH_REGEX)
|
||||
private String username;
|
||||
public 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;
|
||||
public String passwordMd5;
|
||||
|
||||
// https://stackoverflow.com/questions/8202154/how-to-create-an-auto-generated-date-timestamp-field-in-a-play-jpa/8207652
|
||||
@CreationTimestamp
|
||||
private LocalDateTime joinDate;
|
||||
public LocalDateTime joinDate;
|
||||
|
||||
/** The id of the family that the user joined */
|
||||
public Long family;
|
||||
|
||||
@NotNull
|
||||
public String notifications = "";
|
||||
|
||||
@NotNull
|
||||
@Column(length = 5000)
|
||||
public String backup = "";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.hydev.clock_api
|
||||
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.Matchers.matchesPattern
|
||||
import org.hydev.clock_api.entity.User
|
||||
import org.hydev.clock_api.error.ErrorCode.*
|
||||
@@ -122,7 +121,7 @@ class UserRegisterDeleteNodeTest {
|
||||
fun testWhenUserAlreadyExists() {
|
||||
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)))
|
||||
.andExpect(content().string(matchesPattern(R_UUID)))
|
||||
.andDo { result: MvcResult ->
|
||||
// Notice: inserted user should be delete.
|
||||
val tempUuid = result.response.contentAsString
|
||||
@@ -179,7 +178,7 @@ class UserRegisterDeleteNodeTest {
|
||||
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)))
|
||||
.andExpect(content().string(matchesPattern(R_UUID)))
|
||||
.andDo {
|
||||
// Missing headers, 400 bad request.
|
||||
pTDWHsAEHS(mapOf(), HttpStatus.BAD_REQUEST)
|
||||
|
||||
Reference in New Issue
Block a user