包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
555 lines
16 KiB
Go
555 lines
16 KiB
Go
package users
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gothinkster/golang-gin-realworld-example-app/common"
|
|
"github.com/stretchr/testify/assert"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var image_url = "https://golang.org/doc/gopher/frontpage.png"
|
|
var test_db *gorm.DB
|
|
|
|
func newUserModel() UserModel {
|
|
return UserModel{
|
|
ID: 2,
|
|
Username: "asd123!@#ASD",
|
|
Email: "wzt@g.cn",
|
|
Bio: "heheda",
|
|
Image: &image_url,
|
|
PasswordHash: "",
|
|
}
|
|
}
|
|
|
|
func userModelMocker(n int) []UserModel {
|
|
var offset int64
|
|
test_db.Model(&UserModel{}).Count(&offset)
|
|
var ret []UserModel
|
|
for i := int(offset) + 1; i <= int(offset)+n; i++ {
|
|
image := fmt.Sprintf("http://image/%v.jpg", i)
|
|
userModel := UserModel{
|
|
Username: fmt.Sprintf("user%v", i),
|
|
Email: fmt.Sprintf("user%v@linkedin.com", i),
|
|
Bio: fmt.Sprintf("bio%v", i),
|
|
Image: &image,
|
|
}
|
|
userModel.setPassword("password123")
|
|
test_db.Create(&userModel)
|
|
ret = append(ret, userModel)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func TestUserModel(t *testing.T) {
|
|
asserts := assert.New(t)
|
|
|
|
//Testing UserModel's password feature
|
|
userModel := newUserModel()
|
|
err := userModel.checkPassword("")
|
|
asserts.Error(err, "empty password should return err")
|
|
|
|
userModel = newUserModel()
|
|
err = userModel.setPassword("")
|
|
asserts.Error(err, "empty password can not be set null")
|
|
|
|
userModel = newUserModel()
|
|
err = userModel.setPassword("asd123!@#ASD")
|
|
asserts.NoError(err, "password should be set successful")
|
|
asserts.Len(userModel.PasswordHash, 60, "password hash length should be 60")
|
|
|
|
err = userModel.checkPassword("sd123!@#ASD")
|
|
asserts.Error(err, "password should be checked and not validated")
|
|
|
|
err = userModel.checkPassword("asd123!@#ASD")
|
|
asserts.NoError(err, "password should be checked and validated")
|
|
|
|
//Testing the following relationship between users
|
|
users := userModelMocker(3)
|
|
a := users[0]
|
|
b := users[1]
|
|
c := users[2]
|
|
asserts.Equal(0, len(a.GetFollowings()), "GetFollowings should be right before following")
|
|
asserts.Equal(false, a.isFollowing(b), "isFollowing relationship should be right at init")
|
|
a.following(b)
|
|
asserts.Equal(1, len(a.GetFollowings()), "GetFollowings should be right after a following b")
|
|
asserts.Equal(true, a.isFollowing(b), "isFollowing should be right after a following b")
|
|
a.following(c)
|
|
asserts.Equal(2, len(a.GetFollowings()), "GetFollowings be right after a following c")
|
|
asserts.EqualValues(b, a.GetFollowings()[0], "GetFollowings should be right")
|
|
asserts.EqualValues(c, a.GetFollowings()[1], "GetFollowings should be right")
|
|
a.unFollowing(b)
|
|
asserts.Equal(1, len(a.GetFollowings()), "GetFollowings should be right after a unFollowing b")
|
|
asserts.EqualValues(c, a.GetFollowings()[0], "GetFollowings should be right after a unFollowing b")
|
|
asserts.Equal(false, a.isFollowing(b), "isFollowing should be right after a unFollowing b")
|
|
}
|
|
|
|
// Reset test DB and create new one with mock data
|
|
func resetDBWithMock() {
|
|
common.TestDBFree(test_db)
|
|
test_db = common.TestDBInit()
|
|
AutoMigrate()
|
|
userModelMocker(3)
|
|
}
|
|
|
|
// You could write the init logic like reset database code here
|
|
var unauthRequestTests = []struct {
|
|
init func(*http.Request)
|
|
url string
|
|
method string
|
|
bodyData string
|
|
expectedCode int
|
|
responseRegexg string
|
|
msg string
|
|
}{
|
|
//Testing will run one by one, so you can combine it to a user story till another init().
|
|
//And you can modified the header or body in the func(req *http.Request) {}
|
|
|
|
//--------------------- Testing for user register ---------------------
|
|
{
|
|
func(req *http.Request) {
|
|
resetDBWithMock()
|
|
},
|
|
"/users/",
|
|
"POST",
|
|
`{"user":{"username": "wangzitian0","email": "wzt@gg.cn","password": "jakejxke"}}`,
|
|
http.StatusCreated,
|
|
`{"user":{"username":"wangzitian0","email":"wzt@gg.cn","bio":"","image":"","token":"([a-zA-Z0-9-_.]{115})"}}`,
|
|
"valid data and should return StatusCreated",
|
|
},
|
|
{
|
|
func(req *http.Request) {},
|
|
"/users/",
|
|
"POST",
|
|
`{"user":{"username": "wangzitian0","email": "wzt@gg.cn","password": "jakejxke"}}`,
|
|
http.StatusUnprocessableEntity,
|
|
`{"errors":{"database":"UNIQUE constraint failed: user_models.email"}}`,
|
|
"duplicated data and should return StatusUnprocessableEntity",
|
|
},
|
|
{
|
|
func(req *http.Request) {},
|
|
"/users/",
|
|
"POST",
|
|
`{"user":{"username": "u","email": "wzt@gg.cn","password": "jakejxke"}}`,
|
|
http.StatusUnprocessableEntity,
|
|
`{"errors":{"Username":"{min: 4}"}}`,
|
|
"too short username should return error",
|
|
},
|
|
{
|
|
func(req *http.Request) {},
|
|
"/users/",
|
|
"POST",
|
|
`{"user":{"username": "wangzitian0","email": "wzt@gg.cn","password": "j"}}`,
|
|
http.StatusUnprocessableEntity,
|
|
`{"errors":{"Password":"{min: 8}"}}`,
|
|
"too short password should return error",
|
|
},
|
|
{
|
|
func(req *http.Request) {},
|
|
"/users/",
|
|
"POST",
|
|
`{"user":{"username": "wangzitian0","email": "wztgg.cn","password": "jakejxke"}}`,
|
|
http.StatusUnprocessableEntity,
|
|
`{"errors":{"Email":"{key: email}"}}`,
|
|
"email invalid should return error",
|
|
},
|
|
|
|
//--------------------- Testing for user login ---------------------
|
|
{
|
|
func(req *http.Request) {
|
|
resetDBWithMock()
|
|
},
|
|
"/users/login",
|
|
"POST",
|
|
`{"user":{"email": "user1@linkedin.com","password": "password123"}}`,
|
|
http.StatusOK,
|
|
`{"user":{"username":"user1","email":"user1@linkedin.com","bio":"bio1","image":"http://image/1.jpg","token":"([a-zA-Z0-9-_.]{115})"}}`,
|
|
"right info login should return user",
|
|
},
|
|
{
|
|
func(req *http.Request) {},
|
|
"/users/login",
|
|
"POST",
|
|
`{"user":{"email": "user112312312@linkedin.com","password": "password123"}}`,
|
|
http.StatusUnauthorized,
|
|
`{"errors":{"login":"Not Registered email or invalid password"}}`,
|
|
"email not exist should return error info",
|
|
},
|
|
{
|
|
func(req *http.Request) {},
|
|
"/users/login",
|
|
"POST",
|
|
`{"user":{"email": "user1@linkedin.com","password": "password126"}}`,
|
|
http.StatusUnauthorized,
|
|
`{"errors":{"login":"Not Registered email or invalid password"}}`,
|
|
"password error should return error info",
|
|
},
|
|
{
|
|
func(req *http.Request) {},
|
|
"/users/login",
|
|
"POST",
|
|
`{"user":{"email": "user1@linkedin.com","password": "passw"}}`,
|
|
http.StatusUnprocessableEntity,
|
|
`{"errors":{"Password":"{min: 8}"}}`,
|
|
"password too short should return error info",
|
|
},
|
|
{
|
|
func(req *http.Request) {},
|
|
"/users/login",
|
|
"POST",
|
|
`{"user":{"email": "user1@linkedin.com","password": "passw"}}`,
|
|
http.StatusUnprocessableEntity,
|
|
`{"errors":{"Password":"{min: 8}"}}`,
|
|
"password too short should return error info",
|
|
},
|
|
|
|
//--------------------- Testing for self info get & auth module ---------------------
|
|
{
|
|
func(req *http.Request) {
|
|
resetDBWithMock()
|
|
},
|
|
"/user/",
|
|
"GET",
|
|
``,
|
|
http.StatusUnauthorized,
|
|
``,
|
|
"request should return 401 without token",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
req.Header.Set("Authorization", fmt.Sprintf("Tokee %v", common.GenToken(1)))
|
|
},
|
|
"/user/",
|
|
"GET",
|
|
``,
|
|
http.StatusUnauthorized,
|
|
``,
|
|
"wrong token should return 401",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
common.HeaderTokenMock(req, 1)
|
|
},
|
|
"/user/",
|
|
"GET",
|
|
``,
|
|
http.StatusOK,
|
|
`{"user":{"username":"user1","email":"user1@linkedin.com","bio":"bio1","image":"http://image/1.jpg","token":"([a-zA-Z0-9-_.]{115})"}}`,
|
|
"request should return current user with token",
|
|
},
|
|
|
|
//--------------------- Testing for users' profile get ---------------------
|
|
{
|
|
func(req *http.Request) {
|
|
resetDBWithMock()
|
|
},
|
|
"/profiles/user1",
|
|
"GET",
|
|
``,
|
|
http.StatusOK,
|
|
`{"profile":{"username":"user1","bio":"bio1","image":"http://image/1.jpg","following":false}}`,
|
|
"anonymous request should return profile with following=false",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
resetDBWithMock()
|
|
common.HeaderTokenMock(req, 1)
|
|
},
|
|
"/profiles/user1",
|
|
"GET",
|
|
``,
|
|
http.StatusOK,
|
|
`{"profile":{"username":"user1","bio":"bio1","image":"http://image/1.jpg","following":false}}`,
|
|
"request should return self profile",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
common.HeaderTokenMock(req, 2)
|
|
},
|
|
"/profiles/user1",
|
|
"GET",
|
|
``,
|
|
http.StatusOK,
|
|
`{"profile":{"username":"user1","bio":"bio1","image":"http://image/1.jpg","following":false}}`,
|
|
"request should return correct other's profile",
|
|
},
|
|
|
|
//--------------------- Testing for users' profile update ---------------------
|
|
{
|
|
func(req *http.Request) {
|
|
resetDBWithMock()
|
|
common.HeaderTokenMock(req, 1)
|
|
},
|
|
"/profiles/user123",
|
|
"GET",
|
|
``,
|
|
http.StatusNotFound,
|
|
``,
|
|
"user should not exist profile before changed",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
common.HeaderTokenMock(req, 1)
|
|
},
|
|
"/user/",
|
|
"PUT",
|
|
`{"user":{"username":"user123","password": "password126","email":"user123@linkedin.com","bio":"bio123","image":"http://hehe/123.jpg"}}`,
|
|
http.StatusOK,
|
|
`{"user":{"username":"user123","email":"user123@linkedin.com","bio":"bio123","image":"http://hehe/123.jpg","token":"([a-zA-Z0-9-_.]{115})"}}`,
|
|
"current user profile should be changed",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
common.HeaderTokenMock(req, 1)
|
|
},
|
|
"/profiles/user123",
|
|
"GET",
|
|
``,
|
|
http.StatusOK,
|
|
`{"profile":{"username":"user123","bio":"bio123","image":"http://hehe/123.jpg","following":false}}`,
|
|
"request should return self profile after changed",
|
|
},
|
|
{
|
|
func(req *http.Request) {},
|
|
"/users/login",
|
|
"POST",
|
|
`{"user":{"email": "user123@linkedin.com","password": "password126"}}`,
|
|
http.StatusOK,
|
|
`{"user":{"username":"user123","email":"user123@linkedin.com","bio":"bio123","image":"http://hehe/123.jpg","token":"([a-zA-Z0-9-_.]{115})"}}`,
|
|
"user should login using new password after changed",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
common.HeaderTokenMock(req, 2)
|
|
},
|
|
"/user/",
|
|
"PUT",
|
|
`{"user":{"password": "pas"}}`,
|
|
http.StatusUnprocessableEntity,
|
|
`{"errors":{"Password":"{min: 8}"}}`,
|
|
"current user profile should not be changed with error user info",
|
|
},
|
|
|
|
//--------------------- Testing for db errors ---------------------
|
|
{
|
|
func(req *http.Request) {
|
|
resetDBWithMock()
|
|
common.HeaderTokenMock(req, 4)
|
|
},
|
|
"/user/",
|
|
"PUT",
|
|
`{"password": "password321"}}`,
|
|
http.StatusUnprocessableEntity,
|
|
`{"errors":{"Email":"{key: required}","Username":"{key: required}"}}`,
|
|
"test database pk error for user update",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
common.HeaderTokenMock(req, 0)
|
|
},
|
|
"/user/",
|
|
"PUT",
|
|
`{"user":{"username": "wangzitian0","email": "wzt@gg.cn","password": "jakejxke"}}`,
|
|
http.StatusUnprocessableEntity,
|
|
`{"errors":{"database":"WHERE conditions required"}}`,
|
|
"cheat validator and test database connecting error for user update",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
common.TestDBFree(test_db)
|
|
test_db = common.TestDBInit()
|
|
|
|
test_db.AutoMigrate(&UserModel{})
|
|
userModelMocker(3)
|
|
common.HeaderTokenMock(req, 2)
|
|
},
|
|
"/profiles/user1/follow",
|
|
"POST",
|
|
``,
|
|
http.StatusUnprocessableEntity,
|
|
`{"errors":{"database":"no such table: follow_models"}}`,
|
|
"test database error for following",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
common.HeaderTokenMock(req, 2)
|
|
},
|
|
"/profiles/user1/follow",
|
|
"DELETE",
|
|
``,
|
|
http.StatusUnprocessableEntity,
|
|
`{"errors":{"database":"no such table: follow_models"}}`,
|
|
"test database error for canceling following",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
resetDBWithMock()
|
|
common.HeaderTokenMock(req, 2)
|
|
},
|
|
"/profiles/user666/follow",
|
|
"POST",
|
|
``,
|
|
http.StatusNotFound,
|
|
`{"errors":{"profile":"Invalid username"}}`,
|
|
"following wrong user name should return errors",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
common.HeaderTokenMock(req, 2)
|
|
},
|
|
"/profiles/user666/follow",
|
|
"DELETE",
|
|
``,
|
|
http.StatusNotFound,
|
|
`{"errors":{"profile":"Invalid username"}}`,
|
|
"cancel following wrong user name should return errors",
|
|
},
|
|
|
|
//--------------------- Testing for user following ---------------------
|
|
{
|
|
func(req *http.Request) {
|
|
resetDBWithMock()
|
|
common.HeaderTokenMock(req, 2)
|
|
},
|
|
"/profiles/user1/follow",
|
|
"POST",
|
|
``,
|
|
http.StatusOK,
|
|
`{"profile":{"username":"user1","bio":"bio1","image":"http://image/1.jpg","following":true}}`,
|
|
"user follow another should work",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
common.HeaderTokenMock(req, 2)
|
|
},
|
|
"/profiles/user1",
|
|
"GET",
|
|
``,
|
|
http.StatusOK,
|
|
`{"profile":{"username":"user1","bio":"bio1","image":"http://image/1.jpg","following":true}}`,
|
|
"user follow another should make sure database changed",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
common.HeaderTokenMock(req, 2)
|
|
},
|
|
"/profiles/user1/follow",
|
|
"DELETE",
|
|
``,
|
|
http.StatusOK,
|
|
`{"profile":{"username":"user1","bio":"bio1","image":"http://image/1.jpg","following":false}}`,
|
|
"user cancel follow another should work",
|
|
},
|
|
{
|
|
func(req *http.Request) {
|
|
common.HeaderTokenMock(req, 2)
|
|
},
|
|
"/profiles/user1",
|
|
"GET",
|
|
``,
|
|
http.StatusOK,
|
|
`{"profile":{"username":"user1","bio":"bio1","image":"http://image/1.jpg","following":false}}`,
|
|
"user cancel follow another should make sure database changed",
|
|
},
|
|
}
|
|
|
|
func TestWithoutAuth(t *testing.T) {
|
|
asserts := assert.New(t)
|
|
//You could write the reset database code here if you want to create a database for this block
|
|
//resetDB()
|
|
|
|
r := gin.New()
|
|
UsersRegister(r.Group("/users"))
|
|
r.Use(AuthMiddleware(false))
|
|
ProfileRetrieveRegister(r.Group("/profiles"))
|
|
r.Use(AuthMiddleware(true))
|
|
UserRegister(r.Group("/user"))
|
|
ProfileRegister(r.Group("/profiles"))
|
|
for _, testData := range unauthRequestTests {
|
|
bodyData := testData.bodyData
|
|
req, err := http.NewRequest(testData.method, testData.url, bytes.NewBufferString(bodyData))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
asserts.NoError(err)
|
|
|
|
testData.init(req)
|
|
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
asserts.Equal(testData.expectedCode, w.Code, "Response Status - "+testData.msg)
|
|
asserts.Regexp(testData.responseRegexg, w.Body.String(), "Response Content - "+testData.msg)
|
|
}
|
|
}
|
|
|
|
func TestExtractTokenFromQueryParameter(t *testing.T) {
|
|
asserts := assert.New(t)
|
|
|
|
r := gin.New()
|
|
r.Use(AuthMiddleware(false))
|
|
r.GET("/test", func(c *gin.Context) {
|
|
userID := c.MustGet("my_user_id").(uint)
|
|
c.JSON(http.StatusOK, gin.H{"user_id": userID})
|
|
})
|
|
|
|
resetDBWithMock()
|
|
|
|
// Test with access_token query parameter
|
|
token := common.GenToken(1)
|
|
req, _ := http.NewRequest("GET", "/test?access_token="+token, nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
asserts.Equal(http.StatusOK, w.Code, "Request with query token should succeed")
|
|
asserts.Contains(w.Body.String(), `"user_id":1`, "User ID should be 1")
|
|
}
|
|
|
|
func TestAuthMiddlewareInvalidToken(t *testing.T) {
|
|
asserts := assert.New(t)
|
|
|
|
r := gin.New()
|
|
r.Use(AuthMiddleware(true))
|
|
r.GET("/test", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
})
|
|
|
|
// Test with invalid JWT token
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("Authorization", "Token invalid.jwt.token")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
asserts.Equal(http.StatusUnauthorized, w.Code, "Invalid token should return 401")
|
|
}
|
|
|
|
func TestAuthMiddlewareNoToken(t *testing.T) {
|
|
asserts := assert.New(t)
|
|
|
|
r := gin.New()
|
|
r.Use(AuthMiddleware(false))
|
|
r.GET("/test", func(c *gin.Context) {
|
|
userID := c.MustGet("my_user_id").(uint)
|
|
c.JSON(http.StatusOK, gin.H{"user_id": userID})
|
|
})
|
|
|
|
// Test with no token (auto401=false should still proceed)
|
|
req, _ := http.NewRequest("GET", "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
asserts.Equal(http.StatusOK, w.Code, "No token with auto401=false should proceed")
|
|
asserts.Contains(w.Body.String(), `"user_id":0`, "User ID should be 0")
|
|
}
|
|
|
|
// This is a hack way to add test database for each case, as whole test will just share one database.
|
|
// You can read TestWithoutAuth's comment to know how to not share database each case.
|
|
func TestMain(m *testing.M) {
|
|
test_db = common.TestDBInit()
|
|
AutoMigrate()
|
|
exitVal := m.Run()
|
|
common.TestDBFree(test_db)
|
|
os.Exit(exitVal)
|
|
}
|