Add user api
This commit is contained in:
parent
52b98a66c7
commit
bbb82bd2b0
|
|
@ -0,0 +1,91 @@
|
|||
// Code generated by mockery v1.0.0
|
||||
package mock
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import model "github.com/jmattheis/memo/model"
|
||||
|
||||
// MockUserDatabase is an autogenerated mock type for the UserDatabase type
|
||||
type MockUserDatabase struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// CreateUser provides a mock function with given fields: user
|
||||
func (_m *MockUserDatabase) CreateUser(user *model.User) error {
|
||||
ret := _m.Called(user)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*model.User) error); ok {
|
||||
r0 = rf(user)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteUserByID provides a mock function with given fields: id
|
||||
func (_m *MockUserDatabase) DeleteUserByID(id uint) error {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(uint) error); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetUserByID provides a mock function with given fields: id
|
||||
func (_m *MockUserDatabase) GetUserByID(id uint) *model.User {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 *model.User
|
||||
if rf, ok := ret.Get(0).(func(uint) *model.User); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.User)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetUserByName provides a mock function with given fields: name
|
||||
func (_m *MockUserDatabase) GetUserByName(name string) *model.User {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 *model.User
|
||||
if rf, ok := ret.Get(0).(func(string) *model.User); ok {
|
||||
r0 = rf(name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.User)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetUsers provides a mock function with given fields:
|
||||
func (_m *MockUserDatabase) GetUsers() []*model.User {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*model.User
|
||||
if rf, ok := ret.Get(0).(func() []*model.User); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*model.User)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdateUser provides a mock function with given fields: user
|
||||
func (_m *MockUserDatabase) UpdateUser(user *model.User) {
|
||||
_m.Called(user)
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jmattheis/memo/auth"
|
||||
"github.com/jmattheis/memo/model"
|
||||
)
|
||||
|
||||
type userResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `binding:"required" json:"name" query:"name" form:"name"`
|
||||
Pass string `json:"pass,omitempty" form:"pass" query:"pass"`
|
||||
Admin bool `json:"admin" form:"admin" query:"admin"`
|
||||
}
|
||||
|
||||
// The UserDatabase interface for encapsulating database access.
|
||||
type UserDatabase interface {
|
||||
GetUsers() []*model.User
|
||||
GetUserByID(id uint) *model.User
|
||||
GetUserByName(name string) *model.User
|
||||
DeleteUserByID(id uint) error
|
||||
UpdateUser(user *model.User)
|
||||
CreateUser(user *model.User) error
|
||||
}
|
||||
|
||||
// The UserAPI provides handlers for managing users.
|
||||
type UserAPI struct {
|
||||
DB UserDatabase
|
||||
}
|
||||
|
||||
// GetUsers returns all the users
|
||||
func (a *UserAPI) GetUsers(ctx *gin.Context) {
|
||||
users := a.DB.GetUsers()
|
||||
|
||||
var resp []*userResponse
|
||||
for _, user := range users {
|
||||
resp = append(resp, toExternal(user))
|
||||
}
|
||||
|
||||
ctx.JSON(200, resp)
|
||||
}
|
||||
|
||||
// GetCurrentUser returns the current user
|
||||
func (a *UserAPI) GetCurrentUser(ctx *gin.Context) {
|
||||
user := a.DB.GetUserByID(auth.GetUserID(ctx))
|
||||
ctx.JSON(200, toExternal(user))
|
||||
}
|
||||
|
||||
// CreateUser creates a user
|
||||
func (a *UserAPI) CreateUser(ctx *gin.Context) {
|
||||
user := userResponse{}
|
||||
if err := ctx.Bind(&user); err == nil {
|
||||
if len(user.Pass) == 0 {
|
||||
ctx.AbortWithError(400, errors.New("password may not be empty"))
|
||||
} else {
|
||||
internal := toInternal(&user, []byte{})
|
||||
if a.DB.GetUserByName(internal.Name) == nil {
|
||||
a.DB.CreateUser(internal)
|
||||
} else {
|
||||
ctx.AbortWithError(400, errors.New("username already exists"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserByID returns the user by id
|
||||
func (a *UserAPI) GetUserByID(ctx *gin.Context) {
|
||||
if id, err := toUInt(ctx.Param("id")); err == nil {
|
||||
if user := a.DB.GetUserByID(uint(id)); user != nil {
|
||||
ctx.JSON(200, toExternal(user))
|
||||
} else {
|
||||
ctx.AbortWithError(404, errors.New("user does not exist"))
|
||||
}
|
||||
} else {
|
||||
ctx.AbortWithError(400, errors.New("invalid id"))
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUserByID deletes the user by id
|
||||
func (a *UserAPI) DeleteUserByID(ctx *gin.Context) {
|
||||
if id, err := toUInt(ctx.Param("id")); err == nil {
|
||||
if user := a.DB.GetUserByID(id); user != nil {
|
||||
a.DB.DeleteUserByID(id)
|
||||
} else {
|
||||
ctx.AbortWithError(404, errors.New("user does not exist"))
|
||||
}
|
||||
} else {
|
||||
ctx.AbortWithError(400, errors.New("invalid id"))
|
||||
}
|
||||
}
|
||||
|
||||
type userPassword struct {
|
||||
Pass string `binding:"required" json:"pass" form:"pass" query:"pass" `
|
||||
}
|
||||
|
||||
// ChangePassword changes the password from the current user
|
||||
func (a *UserAPI) ChangePassword(ctx *gin.Context) {
|
||||
pw := userPassword{}
|
||||
if err := ctx.Bind(&pw); err == nil {
|
||||
user := a.DB.GetUserByID(auth.GetUserID(ctx))
|
||||
user.Pass = auth.CreatePassword(pw.Pass)
|
||||
a.DB.UpdateUser(user)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateUserByID updates and user by id
|
||||
func (a *UserAPI) UpdateUserByID(ctx *gin.Context) {
|
||||
if id, err := toUInt(ctx.Param("id")); err == nil {
|
||||
var user *userResponse
|
||||
if err := ctx.Bind(&user); err == nil {
|
||||
if oldUser := a.DB.GetUserByID(id); oldUser != nil {
|
||||
internal := toInternal(user, oldUser.Pass)
|
||||
internal.ID = id
|
||||
a.DB.UpdateUser(internal)
|
||||
ctx.JSON(200, toExternal(internal))
|
||||
} else {
|
||||
ctx.AbortWithError(404, errors.New("user does not exist"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.AbortWithError(400, errors.New("invalid id"))
|
||||
}
|
||||
}
|
||||
|
||||
func toUInt(id string) (uint, error) {
|
||||
parsed, err := strconv.ParseUint(id, 10, 32)
|
||||
return uint(parsed), err
|
||||
}
|
||||
|
||||
func toInternal(response *userResponse, pw []byte) *model.User {
|
||||
user := &model.User{
|
||||
Name: response.Name,
|
||||
Admin: response.Admin,
|
||||
}
|
||||
if response.Pass != "" {
|
||||
user.Pass = auth.CreatePassword(response.Pass)
|
||||
} else {
|
||||
user.Pass = pw
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func toExternal(internal *model.User) *userResponse {
|
||||
return &userResponse{
|
||||
Name: internal.Name,
|
||||
Admin: internal.Admin,
|
||||
ID: internal.ID,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bouk/monkey"
|
||||
"github.com/gin-gonic/gin"
|
||||
apimock "github.com/jmattheis/memo/api/mock"
|
||||
"github.com/jmattheis/memo/auth"
|
||||
"github.com/jmattheis/memo/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
var (
|
||||
adminUser = &model.User{ID: 1, Name: "jmattheis", Pass: []byte{1, 2}, Admin: true}
|
||||
adminUserJSON = `{"id":1,"name":"jmattheis","admin":true}`
|
||||
normalUser = &model.User{ID: 2, Name: "nicories", Pass: []byte{2, 3}, Admin: false}
|
||||
normalUserJSON = `{"id":2,"name":"nicories","admin":false}`
|
||||
)
|
||||
|
||||
func TestUserSuite(t *testing.T) {
|
||||
suite.Run(t, new(UserSuite))
|
||||
}
|
||||
|
||||
type UserSuite struct {
|
||||
suite.Suite
|
||||
db *apimock.MockUserDatabase
|
||||
a *UserAPI
|
||||
ctx *gin.Context
|
||||
recorder *httptest.ResponseRecorder
|
||||
}
|
||||
|
||||
func (s *UserSuite) BeforeTest(suiteName, testName string) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
s.recorder = httptest.NewRecorder()
|
||||
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
||||
s.db = &apimock.MockUserDatabase{}
|
||||
s.a = &UserAPI{DB: s.db}
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_GetUsers() {
|
||||
s.db.On("GetUsers").Return([]*model.User{adminUser, normalUser})
|
||||
|
||||
s.a.GetUsers(s.ctx)
|
||||
|
||||
s.expectJSON(fmt.Sprintf("[%s, %s]", adminUserJSON, normalUserJSON))
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_GetCurrentUser() {
|
||||
patch := monkey.Patch(auth.GetUserID, func(*gin.Context) uint { return 1 })
|
||||
defer patch.Unpatch()
|
||||
s.db.On("GetUserByID", uint(1)).Return(adminUser)
|
||||
|
||||
s.a.GetCurrentUser(s.ctx)
|
||||
|
||||
s.expectJSON(adminUserJSON)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_GetUserByID() {
|
||||
s.db.On("GetUserByID", uint(2)).Return(normalUser)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}
|
||||
|
||||
s.a.GetUserByID(s.ctx)
|
||||
|
||||
s.expectJSON(normalUserJSON)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_GetUserByID_InvalidID() {
|
||||
s.db.On("GetUserByID", uint(2)).Return(normalUser)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "abc"}}
|
||||
|
||||
s.a.GetUserByID(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_GetUserByID_UnknownUser() {
|
||||
s.db.On("GetUserByID", mock.Anything).Return(nil)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "3"}}
|
||||
|
||||
s.a.GetUserByID(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_DeleteUserByID_InvalidID() {
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "abc"}}
|
||||
|
||||
s.a.DeleteUserByID(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_DeleteUserByID_UnknownUser() {
|
||||
s.db.On("GetUserByID", mock.Anything).Return(nil)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "3"}}
|
||||
|
||||
s.a.DeleteUserByID(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_DeleteUserByID() {
|
||||
s.db.On("GetUserByID", uint(2)).Return(normalUser)
|
||||
s.db.On("DeleteUserByID", uint(2)).Return(nil)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}
|
||||
|
||||
s.a.DeleteUserByID(s.ctx)
|
||||
|
||||
s.db.AssertCalled(s.T(), "DeleteUserByID", uint(2))
|
||||
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_CreateUser() {
|
||||
pwByte := []byte{1, 2, 3}
|
||||
patch := monkey.Patch(auth.CreatePassword, func(pw string) []byte {
|
||||
if pw == "mylittlepony" {
|
||||
return pwByte
|
||||
}
|
||||
return []byte{5, 67}
|
||||
})
|
||||
defer patch.Unpatch()
|
||||
|
||||
s.db.On("GetUserByName", "tom").Return(nil)
|
||||
s.db.On("CreateUser", mock.Anything).Return(nil)
|
||||
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "mylittlepony", "admin": true}`))
|
||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
s.a.CreateUser(s.ctx)
|
||||
|
||||
s.db.AssertCalled(s.T(), "CreateUser", &model.User{Name: "tom", Pass: pwByte, Admin: true})
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_CreateUser_NoPassword() {
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "", "admin": true}`))
|
||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
s.a.CreateUser(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_CreateUser_NoName() {
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "", "pass": "asd", "admin": true}`))
|
||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
s.a.CreateUser(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_CreateUser_NameAlreadyExists() {
|
||||
pwByte := []byte{1, 2, 3}
|
||||
monkey.Patch(auth.CreatePassword, func(pw string) []byte { return pwByte })
|
||||
|
||||
s.db.On("GetUserByName", "tom").Return(&model.User{ID: 3, Name: "tom"})
|
||||
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/user", strings.NewReader(`{"name": "tom", "pass": "mylittlepony", "admin": true}`))
|
||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
s.a.CreateUser(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_UpdateUserByID_InvalidID() {
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "abc"}}
|
||||
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/user/abc", strings.NewReader(`{"name": "tom", "pass": "", "admin": false}`))
|
||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
s.a.UpdateUserByID(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_UpdateUserByID_UnknownUser() {
|
||||
s.db.On("GetUserByID", uint(2)).Return(nil)
|
||||
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}
|
||||
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/user/2", strings.NewReader(`{"name": "tom", "pass": "", "admin": false}`))
|
||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
s.a.UpdateUserByID(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_UpdateUserByID_UpdateNotPassword() {
|
||||
s.db.On("GetUserByID", uint(2)).Return(&model.User{Name: "nico", Pass: []byte{5}, Admin: false})
|
||||
expected := &model.User{ID: 2, Name: "tom", Pass: []byte{5}, Admin: true}
|
||||
|
||||
s.db.On("UpdateUser", expected).Return(nil)
|
||||
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}
|
||||
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/user/2", strings.NewReader(`{"name": "tom", "pass": "", "admin": true}`))
|
||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
s.a.UpdateUserByID(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||
s.db.AssertCalled(s.T(), "UpdateUser", expected)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_UpdateUserByID_UpdatePassword() {
|
||||
pwByte := []byte{1, 2, 3}
|
||||
patch := monkey.Patch(auth.CreatePassword, func(pw string) []byte { return pwByte })
|
||||
defer patch.Unpatch()
|
||||
|
||||
s.db.On("GetUserByID", uint(2)).Return(normalUser)
|
||||
expected := &model.User{ID: 2, Name: "tom", Pass: pwByte, Admin: true}
|
||||
|
||||
s.db.On("UpdateUser", expected).Return(nil)
|
||||
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}
|
||||
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/user/2", strings.NewReader(`{"name": "tom", "pass": "secret", "admin": true}`))
|
||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
s.a.UpdateUserByID(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||
s.db.AssertCalled(s.T(), "UpdateUser", expected)
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_UpdatePassword() {
|
||||
pwByte := []byte{1, 2, 3}
|
||||
createPasswordPatch := monkey.Patch(auth.CreatePassword, func(pw string) []byte { return pwByte })
|
||||
defer createPasswordPatch.Unpatch()
|
||||
patchUser := monkey.Patch(auth.GetUserID, func(*gin.Context) uint { return 1 })
|
||||
defer patchUser.Unpatch()
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/user/current/password", strings.NewReader(`{"pass": "secret"}`))
|
||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
s.db.On("GetUserByID", uint(1)).Return(&model.User{ID: 1, Name: "jmattheis", Pass: []byte{1}})
|
||||
s.db.On("UpdateUser", mock.Anything).Return(nil)
|
||||
|
||||
s.a.ChangePassword(s.ctx)
|
||||
|
||||
s.db.AssertCalled(s.T(), "UpdateUser", &model.User{ID: 1, Name: "jmattheis", Pass: pwByte})
|
||||
}
|
||||
|
||||
func (s *UserSuite) Test_UpdatePassword_EmptyPassword() {
|
||||
patchUser := monkey.Patch(auth.GetUserID, func(*gin.Context) uint { return 1 })
|
||||
defer patchUser.Unpatch()
|
||||
s.ctx.Request = httptest.NewRequest("POST", "/user/current/password", strings.NewReader(`{"pass":""}`))
|
||||
s.ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
s.db.On("UpdateUser", mock.Anything).Return(nil)
|
||||
|
||||
s.db.On("GetUserByID", uint(1)).Return(&model.User{ID: 1, Name: "jmattheis", Pass: []byte{1}})
|
||||
|
||||
s.a.ChangePassword(s.ctx)
|
||||
|
||||
s.db.AssertNotCalled(s.T(), "UpdateUser", mock.Anything)
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *UserSuite) expectJSON(json string) {
|
||||
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||
bytes, _ := ioutil.ReadAll(s.recorder.Body)
|
||||
|
||||
assert.JSONEq(s.T(), json, string(bytes))
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package model
|
|||
// The User holds information about the credentials of a user and its application and client tokens.
|
||||
type User struct {
|
||||
ID uint `gorm:"primary_key;unique_index;AUTO_INCREMENT"`
|
||||
Name string
|
||||
Name string `gorm:"unique_index"`
|
||||
Pass []byte
|
||||
Admin bool
|
||||
Applications []Application
|
||||
|
|
|
|||
Loading…
Reference in New Issue