# 簡介

這一篇文章主要是練習從零開始創建用戶,使用 JPA+寫 Service+ 重點使用 Junit 寫 Unit Test。
唯一要實現的簡單功能是 注冊用戶

# 創建 Spring Boot Project 並且加入依賴

pom.xml 主要加入 spring web, spring data jpa, mysql driver 的依賴

  • 這次使用 MySql 數據庫
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
		<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

# 修改 application.properties

application.propeties 裡定義一下 MySQL 的配置

spring.datasource.url=jdbc:mysql://localhost:3306/junit
spring.datasource.username=root
spring.datasource.password=123456
spring.jpa.database=mysql 
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect

# 數據庫

User table:

-- auto-generated definition  
create table user  
(  
    user_id       int auto_increment comment 'user id'  
        primary key,  
    username      varchar(256)                       null comment 'user name',  
    user_account  varchar(256)                       not null comment 'user account number  
',  
    user_password varchar(500)                       null comment 'user password',  
    user_email    varchar(1000)                      null comment 'user email',  
    create_time   datetime default CURRENT_TIMESTAMP null comment 'user  created time',  
    update_time   datetime default CURRENT_TIMESTAMP null comment 'user info modified/ update time',  
    user_status   int                                null comment 'user status 0-active 1-inactive',  
    is_deleted    int                                null comment '1 - user is delete'  
)  
    comment 'user information';

# 代碼

Domain 裡面的 > User class

package com.example.junit_restapi.domain;
import javax.persistence.*;
import java.sql.Timestamp;
@Entity
@Data
@AllArgsConstructor  
@NoArgsConstructor
public class User {
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    @Column(name = "user_id")
    private int userId;
    @Basic
    @Column(name = "username")
    private String username;
    @Basic
    @Column(name = "user_account")
    private String userAccount;
    @Basic
    @Column(name = "user_password")
    private String userPassword;
    @Basic
    @Column(name = "user_email")
    private String userEmail;
    @Basic
    @Column(name = "create_time")
    private Timestamp createTime;
    @Basic
    @Column(name = "update_time")
    private Timestamp updateTime;
    @Basic
    @Column(name = "user_status")
    private Integer userStatus;
    @Basic
    @Column(name = "is_deleted")
    private Integer isDeleted;
    @Override
    public boolean equals(Object o) {
      	// 省略 
	   // 都是自己生成的(Generate persistence mapping)
    }
    @Override
    public int hashCode() {
	   // 省略 
	   // 都是自己生成的(Generate persistence mapping)
}

# 命名 bug

之前創建表的時候,每個 field 命名的時候都是用 camel case,比如 userAccount 而沒有用 _ 的格式寫 user_account
然後在 user entity 裡面 寫的也都是用 camel case,按道理來說應該是對 Map 上數據的,但最後出現這個 bug:

java.sql.SQLSyntaxErrorException: Unknown column "create_time" in 'field list'

這裡寫 unknow column "create_time" , 原本應該是 createTime 的。
結果是我忘了,hibernate 會把 camel 的字轉成 underscores,但我並沒有在 user entity class 裡面用 @Column() 去表示是對應哪一列。
實際上應該要寫成這樣的:

@Column(name = "user_account")
private String userAccount;
@Column(name = "create_time")  
private Timestamp createTime;

所以後來我都直接把數據庫裡的 field 的名稱都改成 underscore 去寫了,然後直接 generate persistence mapping,懶得自己再寫一遍 User class 了。

# 注冊用戶功能

在這裡就很簡單的實現注冊用戶這一個功能,實現的邏輯:

  1. 前端輸入帳號、用戶名、和密碼
  2. 校驗輸入
  3. 把密碼加密
  4. 把數據存到數據庫裡
帳號密碼的期望規範

# 檢查帳號用戶名密碼

因為只是簡單寫寫,下面代碼都沒有優化的,也沒有寫 throw exceptions 就直接返回 -1 了。

//check if it's null value
        if (StringUtils.isAnyBlank(userAccount,username, userPassword)) {
            log.error("counldn't save user - null value");
            return -1;
        }
        //check the user account pattern and length
        String acct_pattern = "^[a-zA-Z0-9]([._-](?![._-])|[a-zA-Z0-9]){3,18}[a-zA-Z0-9]$";
        if (userAccount.length() < 4 || !userAccount.matches(acct_pattern)) {
            log.error("counldn't save user - acct name or password length problem");
            return -1;
        }
        // username length should > 4
        if (username.length() < 4 || !username.matches(acct_pattern)) {
            log.error("username length should be greater or equal to 4");
            return -1;
        }
        //Check that the password is in compliance 1. correct pattern 2. length < 8
        String password_pattern = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$";
        if (!userPassword.matches(password_pattern) || userPassword.length() < 8 ) {
            log.error("it didn't save user - password problem");
            return -1;
        }
        //check if the username is already exist
        List<User> user_result_list = userRepo.findUserByUsername(username);
        if(user_result_list.size() > 0) {
            log.error("counldn't save user - user_result_list");
            return -1;
        }

# 正則表達式

# 密碼:

^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$

^                 # start-of-string
(?=.*[0-9])       # a digit must occur at least once
(?=.*[a-z])       # a lower case letter must occur at least once
(?=.*[A-Z])       # an upper case letter must occur at least once
(?=.*[@#$%^&+=])  # a special character must occur at least once
(?=\S+$)          # no whitespace allowed in the entire string
.{8,}             # anything, at least eight places though
$                 # end-of-string

# user_account & username :

^[a-zA-Z0-9]([._-](?![._-])|[a-zA-Z0-9]){3,18}[a-zA-Z0-9]$

# StringUtils

項目裡面用到 StringUtils.isAnyBlank() ,如果要測下面這種代碼,用 StringUtils 看上去比較易懂點...

if(username == null || password == null || checkPassword == null)

在 google 裡搜 → Maven repository → apache commons utils (用最新最多 usage 的

https://mvnrepository.com/artifact/org.apache.commons/commons-lang3/3.12.0

然後把裡面的 dependency 加入到 pom.xml 配置

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

就可以用 StringUtils.isAnyBlank () 去測是否為 null ,並且代碼更加易懂

public long testUser(String username, String password, String checkPassword) {
        if(StringUtils.isAnyBlank(username, password, checkPassword)){
            return -1;
        }
        return 0;
    }

# 加密密碼

在這邊只是用最簡單的方法加密,沒有用 JWT 或其他方法密碼。

//encrypt password
        String salt = "tiff";
        String new_password = DigestUtils.md5DigestAsHex((salt + userPassword).getBytes(StandardCharsets.UTF_8));

# 創建 User 並存到數據庫

//insert user into database
        User user = new User();
        user.setUsername(username);
        user.setUserAccount(userAccount);
        user.setUserPassword(new_password);
        userRepo.save(user);

# 完整代碼

UserRepository

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @Query(value = "select u from User u where u.username = ?1")
    List<User> findUserByUsername(String username);
}

UserService

@Service
public interface UserService {
    long registerUser(String userAccount, String username, String userpassword);
}

UserServiceImpl

@Slf4j
@Service
public class UserServiceImpl implements UserService{
    private final UserRepository userRepo;
    @Autowired
    public UserServiceImpl(UserRepository userRepo) {
        this.userRepo = userRepo;
    }
    @Override
    public long registerUser(String userAccount, String username, String userPassword) {
        //check if it's null value
        if (StringUtils.isAnyBlank(userAccount,username, userPassword)) {
            log.error("counldn't save user - null value");
            return -1;
        }
        //check the user account pattern and length
        String acct_pattern = "^[a-zA-Z0-9]([._-](?![._-])|[a-zA-Z0-9]){3,18}[a-zA-Z0-9]$";
        if (userAccount.length() < 4 || !userAccount.matches(acct_pattern)) {
            log.error("counldn't save user - acct name or password length problem");
            return -1;
        }
        // username length should > 4
        if (username.length() < 4 || !username.matches(acct_pattern)) {
            log.error("username length should be greater or equal to 4");
            return -1;
        }
        //Check that the password is in compliance 1. correct pattern 2. length < 8
        String password_pattern = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$";
        if (!userPassword.matches(password_pattern) || userPassword.length() < 8 ) {
            log.error("it didn't save user - password problem");
            return -1;
        }
        //check if the username is already exist
        List<User> user_result_list = userRepo.findUserByUsername(username);
        if(user_result_list.size() > 0) {
            log.error("counldn't save user - user_result_list");
            return -1;
        }
        //encrypt password
        String salt = "tiff";
        String new_password = DigestUtils.md5DigestAsHex((salt + userPassword).getBytes(StandardCharsets.UTF_8));
        //insert user into database
        User user = new User();
        user.setUsername(username);
        user.setUserAccount(userAccount);
        user.setUserPassword(new_password);
        userRepo.save(user);
        log.info("========User has been  created-======");
        return user.getUserId();
    }
}

# 單元測試

@Test
    void registerUser() {
        User user = new User();
        user.setUsername("1");
        user.setUserAccount("testing123");
        user.setUserPassword("Ki_12352@932");
        user.setCreateTime(new Timestamp(new java.util.Date().getTime()));
        user.setUpdateTime(new Timestamp(new java.util.Date().getTime()));
        Assertions.assertAll("testing error",
                () -> { // username length should not <= 4
                    long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword());
                    Assertions.assertEquals(-1, result);
                },
                () -> { // username pattern check
                    user.setUsername("user4$@-^");
                    long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword());
                    Assertions.assertEquals(-1, result);
                },
                () -> { // test password length less than 4
                    user.setUsername("tiffany");
                    user.setUserPassword("11");
                    long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword());
                    Assertions.assertEquals(-1, result);
                },
                () -> { // test password doesn't include special char
                    user.setUserPassword("24359832759");
                    long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword());
                    Assertions.assertEquals(-1, result);
                },
                () -> { // test already have existed user in the db
                    user.setUserPassword("Jj@12345678");
                    user.setUsername("Alice");
                    long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword());
                    Assertions.assertEquals(-1, result);
                },
                () -> { // account has special char,
                    user.setUserAccount("test@^&2");
                    long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword());
                    Assertions.assertEquals(-1, result, "account shouldn't have special char!");
                },
                () -> { // account length check : < 4
                    user.setUserAccount("123");
                    long result = userService.registerUser(user.getUserAccount(), user.getUsername(), user.getUserPassword());
                    Assertions.assertEquals(-1, result, "account shouldn't have special char!");
                }
       );
        // test the successful user
        User successUser = new User();
        successUser.setUsername("junituser");
        successUser.setUserAccount("junituser001");
        successUser.setUserPassword("k_8Uj72882@5");
        successUser.setCreateTime(new Timestamp(new java.util.Date().getTime()));
        successUser.setUpdateTime(new Timestamp(new java.util.Date().getTime()));
        long success_result = userService.registerUser(successUser.getUserAccount(), successUser.getUsername(), successUser.getUserPassword());
        Assertions.assertTrue(success_result > 0);

# 測試完的數據庫長這樣

Alice 是本身就有了
經過測試,最後只加上 junituser 的那一 row

之後再練習下用 Mockito,這次先寫到這裡~