init: 初始化 AssetX 项目仓库

包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
This commit is contained in:
2026-03-27 11:26:43 +00:00
commit 2ee4553b71
634 changed files with 988255 additions and 0 deletions

12
webapp-back/users/doc.go Normal file
View File

@@ -0,0 +1,12 @@
/*
The user module containing the user CRU operation.
model.go: definition of orm based data model
routers.go: router binding and core logic
serializers.go: definition the schema of return data
validators.go: definition the validator of form data
*/
package users

View File

@@ -0,0 +1,75 @@
package users
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/gothinkster/golang-gin-realworld-example-app/common"
)
// Extract token from Authorization header or query parameter
func extractToken(c *gin.Context) string {
// Check Authorization header first
bearerToken := c.GetHeader("Authorization")
if len(bearerToken) > 6 && strings.ToUpper(bearerToken[0:6]) == "TOKEN " {
return bearerToken[6:]
}
// Check query parameter
token := c.Query("access_token")
if token != "" {
return token
}
return ""
}
// A helper to write user_id and user_model to the context
func UpdateContextUserModel(c *gin.Context, my_user_id uint) {
var myUserModel UserModel
if my_user_id != 0 {
db := common.GetDB()
db.First(&myUserModel, my_user_id)
}
c.Set("my_user_id", my_user_id)
c.Set("my_user_model", myUserModel)
}
// You can custom middlewares yourself as the doc: https://github.com/gin-gonic/gin#custom-middleware
//
// r.Use(AuthMiddleware(true))
func AuthMiddleware(auto401 bool) gin.HandlerFunc {
return func(c *gin.Context) {
UpdateContextUserModel(c, 0)
tokenString := extractToken(c)
if tokenString == "" {
if auto401 {
c.AbortWithStatus(http.StatusUnauthorized)
}
return
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(common.JWTSecret), nil
})
if err != nil {
if auto401 {
c.AbortWithStatus(http.StatusUnauthorized)
}
return
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
my_user_id := uint(claims["id"].(float64))
UpdateContextUserModel(c, my_user_id)
}
}
}

154
webapp-back/users/models.go Normal file
View File

@@ -0,0 +1,154 @@
package users
import (
"errors"
"github.com/gothinkster/golang-gin-realworld-example-app/common"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// Models should only be concerned with database schema, more strict checking should be put in validator.
//
// More detail you can find here: http://jinzhu.me/gorm/models.html#model-definition
//
// HINT: If you want to split null and "", you should use *string instead of string.
type UserModel struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"column:username"`
Email string `gorm:"column:email;uniqueIndex"`
Bio string `gorm:"column:bio;size:1024"`
Image *string `gorm:"column:image"`
PasswordHash string `gorm:"column:password;not null"`
}
// A hack way to save ManyToMany relationship,
// gorm will build the alias as FollowingBy <-> FollowingByID <-> "following_by_id".
//
// DB schema looks like: id, created_at, updated_at, deleted_at, following_id, followed_by_id.
//
// Retrieve them by:
//
// db.Where(FollowModel{ FollowingID: v.ID, FollowedByID: u.ID, }).First(&follow)
// db.Where(FollowModel{ FollowedByID: u.ID, }).Find(&follows)
//
// More details about gorm.Model: http://jinzhu.me/gorm/models.html#conventions
type FollowModel struct {
gorm.Model
Following UserModel
FollowingID uint
FollowedBy UserModel
FollowedByID uint
}
// Migrate the schema of database if needed
func AutoMigrate() {
db := common.GetDB()
db.AutoMigrate(&UserModel{})
db.AutoMigrate(&FollowModel{})
}
// What's bcrypt? https://en.wikipedia.org/wiki/Bcrypt
// Golang bcrypt doc: https://godoc.org/golang.org/x/crypto/bcrypt
// You can change the value in bcrypt.DefaultCost to adjust the security index.
//
// err := userModel.setPassword("password0")
func (u *UserModel) setPassword(password string) error {
if len(password) == 0 {
return errors.New("password should not be empty!")
}
bytePassword := []byte(password)
// Make sure the second param `bcrypt generator cost` between [4, 32)
passwordHash, _ := bcrypt.GenerateFromPassword(bytePassword, bcrypt.DefaultCost)
u.PasswordHash = string(passwordHash)
return nil
}
// Database will only save the hashed string, you should check it by util function.
//
// if err := serModel.checkPassword("password0"); err != nil { password error }
func (u *UserModel) checkPassword(password string) error {
bytePassword := []byte(password)
byteHashedPassword := []byte(u.PasswordHash)
return bcrypt.CompareHashAndPassword(byteHashedPassword, bytePassword)
}
// You could input the conditions and it will return an UserModel in database with error info.
//
// userModel, err := FindOneUser(&UserModel{Username: "username0"})
func FindOneUser(condition interface{}) (UserModel, error) {
db := common.GetDB()
var model UserModel
err := db.Where(condition).First(&model).Error
return model, err
}
// You could input an UserModel which will be saved in database returning with error info
//
// if err := SaveOne(&userModel); err != nil { ... }
func SaveOne(data interface{}) error {
db := common.GetDB()
err := db.Save(data).Error
return err
}
// You could update properties of an UserModel to database returning with error info.
//
// err := db.Model(userModel).Updates(UserModel{Username: "wangzitian0"}).Error
func (model *UserModel) Update(data interface{}) error {
db := common.GetDB()
err := db.Model(model).Updates(data).Error
return err
}
// You could add a following relationship as userModel1 following userModel2
//
// err = userModel1.following(userModel2)
func (u UserModel) following(v UserModel) error {
db := common.GetDB()
var follow FollowModel
err := db.FirstOrCreate(&follow, &FollowModel{
FollowingID: v.ID,
FollowedByID: u.ID,
}).Error
return err
}
// You could check whether userModel1 following userModel2
//
// followingBool = myUserModel.isFollowing(self.UserModel)
func (u UserModel) isFollowing(v UserModel) bool {
db := common.GetDB()
var follow FollowModel
db.Where(FollowModel{
FollowingID: v.ID,
FollowedByID: u.ID,
}).First(&follow)
return follow.ID != 0
}
// You could delete a following relationship as userModel1 following userModel2
//
// err = userModel1.unFollowing(userModel2)
func (u UserModel) unFollowing(v UserModel) error {
db := common.GetDB()
err := db.Where("following_id = ? AND followed_by_id = ?", v.ID, u.ID).Delete(&FollowModel{}).Error
return err
}
// You could get a following list of userModel
//
// followings := userModel.GetFollowings()
func (u UserModel) GetFollowings() []UserModel {
db := common.GetDB()
var follows []FollowModel
var followings []UserModel
db.Preload("Following").Where(FollowModel{
FollowedByID: u.ID,
}).Find(&follows)
for _, follow := range follows {
followings = append(followings, follow.Following)
}
return followings
}

View File

@@ -0,0 +1,137 @@
package users
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/gothinkster/golang-gin-realworld-example-app/common"
"net/http"
)
func UsersRegister(router *gin.RouterGroup) {
router.POST("", UsersRegistration)
router.POST("/", UsersRegistration)
router.POST("/login", UsersLogin)
}
func UserRegister(router *gin.RouterGroup) {
router.GET("", UserRetrieve)
router.GET("/", UserRetrieve)
router.PUT("", UserUpdate)
router.PUT("/", UserUpdate)
}
func ProfileRetrieveRegister(router *gin.RouterGroup) {
router.GET("/:username", ProfileRetrieve)
}
func ProfileRegister(router *gin.RouterGroup) {
router.POST("/:username/follow", ProfileFollow)
router.DELETE("/:username/follow", ProfileUnfollow)
}
func ProfileRetrieve(c *gin.Context) {
username := c.Param("username")
userModel, err := FindOneUser(&UserModel{Username: username})
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("profile", errors.New("Invalid username")))
return
}
profileSerializer := ProfileSerializer{c, userModel}
c.JSON(http.StatusOK, gin.H{"profile": profileSerializer.Response()})
}
func ProfileFollow(c *gin.Context) {
username := c.Param("username")
userModel, err := FindOneUser(&UserModel{Username: username})
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("profile", errors.New("Invalid username")))
return
}
myUserModel := c.MustGet("my_user_model").(UserModel)
err = myUserModel.following(userModel)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
return
}
serializer := ProfileSerializer{c, userModel}
c.JSON(http.StatusOK, gin.H{"profile": serializer.Response()})
}
func ProfileUnfollow(c *gin.Context) {
username := c.Param("username")
userModel, err := FindOneUser(&UserModel{Username: username})
if err != nil {
c.JSON(http.StatusNotFound, common.NewError("profile", errors.New("Invalid username")))
return
}
myUserModel := c.MustGet("my_user_model").(UserModel)
err = myUserModel.unFollowing(userModel)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
return
}
serializer := ProfileSerializer{c, userModel}
c.JSON(http.StatusOK, gin.H{"profile": serializer.Response()})
}
func UsersRegistration(c *gin.Context) {
userModelValidator := NewUserModelValidator()
if err := userModelValidator.Bind(c); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
return
}
if err := SaveOne(&userModelValidator.userModel); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
return
}
c.Set("my_user_model", userModelValidator.userModel)
serializer := UserSerializer{c}
c.JSON(http.StatusCreated, gin.H{"user": serializer.Response()})
}
func UsersLogin(c *gin.Context) {
loginValidator := NewLoginValidator()
if err := loginValidator.Bind(c); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
return
}
userModel, err := FindOneUser(&UserModel{Email: loginValidator.userModel.Email})
if err != nil {
c.JSON(http.StatusUnauthorized, common.NewError("login", errors.New("Not Registered email or invalid password")))
return
}
if userModel.checkPassword(loginValidator.User.Password) != nil {
c.JSON(http.StatusUnauthorized, common.NewError("login", errors.New("Not Registered email or invalid password")))
return
}
UpdateContextUserModel(c, userModel.ID)
serializer := UserSerializer{c}
c.JSON(http.StatusOK, gin.H{"user": serializer.Response()})
}
func UserRetrieve(c *gin.Context) {
serializer := UserSerializer{c}
c.JSON(http.StatusOK, gin.H{"user": serializer.Response()})
}
func UserUpdate(c *gin.Context) {
myUserModel := c.MustGet("my_user_model").(UserModel)
userModelValidator := NewUserModelValidatorFillWith(myUserModel)
if err := userModelValidator.Bind(c); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
return
}
userModelValidator.userModel.ID = myUserModel.ID
if err := myUserModel.Update(userModelValidator.userModel); err != nil {
c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
return
}
UpdateContextUserModel(c, myUserModel.ID)
serializer := UserSerializer{c}
c.JSON(http.StatusOK, gin.H{"user": serializer.Response()})
}

View File

@@ -0,0 +1,66 @@
package users
import (
"github.com/gin-gonic/gin"
"github.com/gothinkster/golang-gin-realworld-example-app/common"
)
type ProfileSerializer struct {
C *gin.Context
UserModel
}
// Declare your response schema here
type ProfileResponse struct {
ID uint `json:"-"`
Username string `json:"username"`
Bio string `json:"bio"`
Image string `json:"image"`
Following bool `json:"following"`
}
// Put your response logic including wrap the userModel here.
func (self *ProfileSerializer) Response() ProfileResponse {
myUserModel := self.C.MustGet("my_user_model").(UserModel)
image := ""
if self.Image != nil {
image = *self.Image
}
profile := ProfileResponse{
ID: self.ID,
Username: self.Username,
Bio: self.Bio,
Image: image,
Following: myUserModel.isFollowing(self.UserModel),
}
return profile
}
type UserSerializer struct {
c *gin.Context
}
type UserResponse struct {
Username string `json:"username"`
Email string `json:"email"`
Bio string `json:"bio"`
Image string `json:"image"`
Token string `json:"token"`
}
func (self *UserSerializer) Response() UserResponse {
myUserModel := self.c.MustGet("my_user_model").(UserModel)
image := ""
if myUserModel.Image != nil {
image = *myUserModel.Image
}
user := UserResponse{
Username: myUserModel.Username,
Email: myUserModel.Email,
Bio: myUserModel.Bio,
Image: image,
Token: common.GenToken(myUserModel.ID),
}
return user
}

View File

@@ -0,0 +1,554 @@
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)
}

View File

@@ -0,0 +1,85 @@
package users
import (
"github.com/gin-gonic/gin"
"github.com/gothinkster/golang-gin-realworld-example-app/common"
)
// *ModelValidator containing two parts:
// - Validator: write the form/json checking rule according to the doc https://github.com/go-playground/validator
// - DataModel: fill with data from Validator after invoking common.Bind(c, self)
// Then, you can just call model.save() after the data is ready in DataModel.
type UserModelValidator struct {
User struct {
Username string `form:"username" json:"username" binding:"required,min=4,max=255"`
Email string `form:"email" json:"email" binding:"required,email"`
Password string `form:"password" json:"password" binding:"required,min=8,max=255"`
Bio string `form:"bio" json:"bio" binding:"max=1024"`
Image string `form:"image" json:"image" binding:"omitempty,url"`
} `json:"user"`
userModel UserModel `json:"-"`
}
// There are some difference when you create or update a model, you need to fill the DataModel before
// update so that you can use your origin data to cheat the validator.
// BTW, you can put your general binding logic here such as setting password.
func (self *UserModelValidator) Bind(c *gin.Context) error {
err := common.Bind(c, self)
if err != nil {
return err
}
self.userModel.Username = self.User.Username
self.userModel.Email = self.User.Email
self.userModel.Bio = self.User.Bio
if self.User.Password != common.RandomPassword {
self.userModel.setPassword(self.User.Password)
}
if self.User.Image != "" {
self.userModel.Image = &self.User.Image
}
return nil
}
// You can put the default value of a Validator here
func NewUserModelValidator() UserModelValidator {
userModelValidator := UserModelValidator{}
return userModelValidator
}
func NewUserModelValidatorFillWith(userModel UserModel) UserModelValidator {
userModelValidator := NewUserModelValidator()
userModelValidator.User.Username = userModel.Username
userModelValidator.User.Email = userModel.Email
userModelValidator.User.Bio = userModel.Bio
userModelValidator.User.Password = common.RandomPassword
if userModel.Image != nil {
userModelValidator.User.Image = *userModel.Image
}
return userModelValidator
}
type LoginValidator struct {
User struct {
Email string `form:"email" json:"email" binding:"required,email"`
Password string `form:"password" json:"password" binding:"required,min=8,max=255"`
} `json:"user"`
userModel UserModel `json:"-"`
}
func (self *LoginValidator) Bind(c *gin.Context) error {
err := common.Bind(c, self)
if err != nil {
return err
}
self.userModel.Email = self.User.Email
return nil
}
// You can put the default value of a Validator here
func NewLoginValidator() LoginValidator {
loginValidator := LoginValidator{}
return loginValidator
}