├── models ├── change_password_request.go ├── refresh_token_request.go ├── authentication_response.go ├── access_token.go ├── sign_in_request.go ├── article.go ├── sign_up_request.go ├── client.go └── user.go ├── glide.yaml ├── util ├── slice_and_array.go ├── operationresult │ └── operationresult.go ├── echo_binder.go ├── testhelper │ └── testhelper.go ├── genrandom.go └── specialerror │ └── specialerror.go ├── README.md ├── LICENSE.md ├── glide.lock ├── Makefile ├── .gitignore ├── server.go └── controller ├── article ├── article_controller.go └── article_controller_test.go ├── client ├── client_controller.go └── client_controller_test.go └── user ├── user_controller.go └── user_controller_test.go /models/change_password_request.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ChangePasswordRequestModel struct { 4 | OldPassword string `valid:"length(6|64),required" json:"old_password"` 5 | Password string `valid:"length(6|64),required" json:"password"` 6 | } -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/atahani/golang-rest-api-sample 2 | import: 3 | - package: github.com/asaskevich/govalidator 4 | - package: github.com/labstack/echo 5 | - package: gopkg.in/mcuadros/go-defaults.v1 6 | - package: github.com/dgrijalva/jwt-go 7 | -------------------------------------------------------------------------------- /util/slice_and_array.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | //the helper functions for slice and arrays 4 | func IsStringInSlice(str string, list []string) bool { 5 | for _, v := range list { 6 | if v == str { 7 | return true 8 | } 9 | } 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /models/refresh_token_request.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | 4 | //it's used only for JSON request 5 | type RefreshTokenRequest struct { 6 | AppId string `valid:"required" json:"app_id"` 7 | AppKey string `json:"app_key"` 8 | RefreshToken string `valid:"required" json:"refresh_token"` 9 | } -------------------------------------------------------------------------------- /models/authentication_response.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | //it's used only for JSON response in authentication and refresh token requests 4 | type AuthenticationResponse struct { 5 | TokenType string `json:"token_type"` 6 | AccessToken string `json:"access_token"` 7 | ExpiresInMin float64 `json:"expire_in_min"` 8 | RefreshToken string `json:"refresh_token"` 9 | } 10 | -------------------------------------------------------------------------------- /models/access_token.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gopkg.in/mgo.v2/bson" 5 | "time" 6 | ) 7 | 8 | type AccessToken struct { 9 | Id bson.ObjectId `bson:"_id"` 10 | UserId bson.ObjectId `bson:"user_id"` 11 | TrustedAppId bson.ObjectId `bson:"trusted_app_id"` 12 | Token string `bson:"token"` 13 | ExpireAt time.Time `bson:"expire_at"` 14 | } -------------------------------------------------------------------------------- /models/sign_in_request.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | //it's used only for JSON request 4 | type SignInRequest struct { 5 | AppId string `valid:"required" json:"app_id"` 6 | AppKey string `json:"app_key"` 7 | DeviceModel string `json:"device_model"` 8 | Email string `valid:"email,required" json:"email"` 9 | Password string `valid:"length(6|64),required" json:"password"` 10 | } -------------------------------------------------------------------------------- /models/article.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gopkg.in/mgo.v2/bson" 5 | "time" 6 | ) 7 | 8 | type Article struct { 9 | Id bson.ObjectId `json:"id" bson:"_id"` 10 | Title string `valid:"required" json:"title" bson:"title"` 11 | Content string `valid:"required" json:"content" bson:"content"` 12 | UserId bson.ObjectId `json:"user_id" bson:"user_id"` 13 | CreatedAt time.Time `json:"created_at" bson:"created_at"` 14 | UpdatedAt time.Time `json:"updated_at" bson:"updated_at"` 15 | } -------------------------------------------------------------------------------- /util/operationresult/operationresult.go: -------------------------------------------------------------------------------- 1 | package operationresult 2 | 3 | var ( 4 | SuccessfullyRemoved = New("SUCCESSFULLY_REMOVED", "the item successfully removed") 5 | SuccessfullyUpdated = New("SUCCESSFULLY_UPDATED", "the item successfuly updated") 6 | PasswordSuccessfullyChanged = New("PASSWORD_SUCCESSFULLY_CHANGE", "user password successfully changed") 7 | ) 8 | 9 | type OperationResult struct { 10 | Message string `json:"message"` 11 | Description string `json:"description"` 12 | } 13 | 14 | func New(message, description string) *OperationResult { 15 | return &OperationResult{message, description} 16 | } -------------------------------------------------------------------------------- /models/sign_up_request.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | //it's used only for JSON request 4 | type SignUpRequest struct { 5 | AppId string `valid:"required" json:"app_id"` 6 | AppKey string `json:"app_key"` 7 | DeviceModel string `json:"device_model"` 8 | FirstName string `valid:"required" json:"first_name" bson:"first_name"` 9 | LastName string `valid:"required" json:"last_name" bson:"last_name"` 10 | DisplayName string `valid:"required" json:"display_name" bson:"display_name"` 11 | Email string `valid:"email,required" json:"email"` 12 | Password string `valid:"length(6|64),required" json:"password"` 13 | } -------------------------------------------------------------------------------- /models/client.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gopkg.in/mgo.v2/bson" 5 | "time" 6 | "encoding/hex" 7 | "crypto/md5" 8 | ) 9 | 10 | type Client struct { 11 | AppId bson.ObjectId `json:"app_id" bson:"_id"` 12 | AppKey string `json:"app_key" bson:"key"` 13 | Name string `valid:"required" json:"name" bson:"name"` 14 | Description string `json:"description,omitempty" bson:"description,omitempty"` 15 | IsEnable bool `default:"true" json:"is_enable" bson:"enable_status"` 16 | PlatformType string `default:"web" json:"platform_type" bson:"platform_type"` 17 | CreatedAt time.Time `json:"created_at" bson:"created_at"` 18 | UpdatedAt time.Time `json:"updated_at" bson:"updated_at"` 19 | } 20 | 21 | func (cli *Client) HashedAppKey() string { 22 | hasher := md5.New() 23 | hasher.Write([]byte(cli.AppKey)) 24 | return hex.EncodeToString(hasher.Sum(nil)) 25 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## REST API Sample Write in Go 2 | 3 | ### Project Features 4 | 5 | * Using [echo](https://labstack.com/echo) as high performance framework 6 | * Implement custom Error Handling 7 | * Implement OAuth Authentication mechanism 8 | * Using JWT as Token via [jwt-go package](https://github.com/dgrijalva/jwt-go) 9 | * Implement Role base authorization 10 | * Write unit test for API endpoint and middlewares 11 | * Using [glide](https://glide.sh) as package manager 12 | 13 | ### Project Dependencies 14 | 1. Install Golang (tested with Go 1.6) 15 | 2. Install [Glide](https://github.com/Masterminds/glide) as package manager 16 | 3. Install and run MongoDB service on your localhost for storing data 17 | 18 | 19 | ### How to use from this sample project 20 | ##### Clone the repository 21 | ``` 22 | git clone https://github.com/atahani/golang-rest-api-sample.git 23 | cd golang-rest-api-sample 24 | ``` 25 | 26 | ##### Install dependencies via glide 27 | ``` 28 | glide install 29 | ``` 30 | 31 | ##### Build, serve or test using make 32 | 33 | ``` 34 | make build 35 | make serve 36 | make test 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## License 2 | (The MIT License) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | 'Software'), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 19 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /util/echo_binder.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | "reflect" 6 | "encoding/json" 7 | 8 | "github.com/labstack/echo" 9 | "github.com/asaskevich/govalidator" 10 | 11 | "github.com/atahani/golang-rest-api-sample/util/specialerror" 12 | ) 13 | 14 | //this is custom bind function for echo to validate struct 15 | 16 | type customBinderWithValidation struct { 17 | } 18 | 19 | func NewCustomBinderWithValidation() *customBinderWithValidation { 20 | return &customBinderWithValidation{} 21 | } 22 | 23 | func (customBinderWithValidation) Bind(i interface{}, c echo.Context) error { 24 | rq := c.Request() 25 | ct := rq.Header().Get(echo.HeaderContentType) 26 | //first check the require fields 27 | if !strings.HasPrefix(ct, echo.MIMEApplicationJSON) { 28 | return echo.ErrUnsupportedMediaType 29 | } 30 | if err := json.NewDecoder(rq.Body()).Decode(i); err != nil { 31 | return specialerror.ErrSomeFieldAreNotValid 32 | } 33 | //data decoded now should check validation if it's struct 34 | val := reflect.ValueOf(i) 35 | if val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr { 36 | val = val.Elem() 37 | } 38 | if val.Kind() == reflect.Struct { 39 | if isValid, err2 := govalidator.ValidateStruct(i); !isValid || err2 != nil { 40 | return specialerror.ErrSomeFieldAreNotValid 41 | } 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /util/testhelper/testhelper.go: -------------------------------------------------------------------------------- 1 | package testhelper 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "gopkg.in/mgo.v2" 8 | 9 | "github.com/labstack/echo" 10 | 11 | "github.com/atahani/golang-rest-api-sample/util" 12 | "github.com/atahani/golang-rest-api-sample/util/specialerror" 13 | ) 14 | 15 | const ( 16 | DB_TEST_NAME = "golang_sample_test" 17 | ) 18 | 19 | //utilities for testing used in unit testing 20 | type TestingProvider struct { 21 | Session *mgo.Session 22 | Echo *echo.Echo 23 | Router *echo.Router 24 | } 25 | 26 | func (provider *TestingProvider) StartTesting() { 27 | provider.Session = getSession() 28 | //create new echo server 29 | provider.Echo = echo.New() 30 | provider.Router = provider.Echo.Router() 31 | //set custom binder to validate model 32 | bi := util.NewCustomBinderWithValidation() 33 | provider.Echo.SetBinder(bi) 34 | //set Custom Error Handler 35 | provider.Echo.SetHTTPErrorHandler(specialerror.CustomErrorHandler) 36 | provider.Echo.SetDebug(true) 37 | } 38 | 39 | func getSession() *mgo.Session { 40 | mongoDBDialInfo := &mgo.DialInfo{ 41 | Addrs: []string{"localhost"}, 42 | Timeout: 60 * time.Second, 43 | Database: DB_TEST_NAME, 44 | } 45 | //first should set path the fake database 46 | //dbServer.SetPath(folderPath) 47 | session, err := mgo.DialWithInfo(mongoDBDialInfo) 48 | if err != nil { 49 | fmt.Printf("connection %s\n", err) 50 | } 51 | return session 52 | } 53 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: d0c907cd77f5324b91b98501b6f3cbd6a90eb7a4dc66cee87efe1bccb2706d8b 2 | updated: 2016-04-14T15:08:13.589231839+04:30 3 | imports: 4 | - name: github.com/asaskevich/govalidator 5 | version: d1e14c504700969ddf41264c7f3e084c7b99de59 6 | - name: github.com/dgrijalva/jwt-go 7 | version: a2c85815a77d0f951e33ba4db5ae93629a1530af 8 | - name: github.com/labstack/echo 9 | version: 11eafe9b901c7598ddbac94294120e5c695941f1 10 | subpackages: 11 | - engine/standard 12 | - middleware 13 | - engine 14 | - name: github.com/labstack/gommon 15 | version: f8343700e8769e645b5f9949ec2e3a69f9fd71eb 16 | subpackages: 17 | - log 18 | - color 19 | - name: github.com/mattn/go-colorable 20 | version: 9cbef7c35391cca05f15f8181dc0b18bc9736dbb 21 | - name: github.com/mattn/go-isatty 22 | version: 56b76bdf51f7708750eac80fa38b952bb9f32639 23 | - name: github.com/valyala/fasttemplate 24 | version: 3b874956e03f1636d171bda64b130f9135f42cff 25 | - name: golang.org/x/net 26 | version: fb93926129b8ec0056f2f458b1f519654814edf0 27 | subpackages: 28 | - context 29 | - name: golang.org/x/sys 30 | version: 324e1375807370c3934e14f3a97e7d160832f6b4 31 | subpackages: 32 | - unix 33 | - name: gopkg.in/mcuadros/go-defaults.v1 34 | version: ac8540f0fc7e0fb5f1eb9e25c6fd0b8db8f97eed 35 | - name: gopkg.in/mgo.v2 36 | version: b6e2fa371e64216a45e61072a96d4e3859f169da 37 | subpackages: 38 | - bson 39 | - internal/sasl 40 | - internal/scram 41 | devImports: [] 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile to perform "live code reloading" after changes to .go files. 3 | # 4 | # n.b. you must install fswatch (OS X: `brew install fswatch`) 5 | # 6 | # To start live reloading run the following command: 7 | # $ make serve 8 | # 9 | 10 | # binary name to kill/restart 11 | PROG = golang-rest-api-sample 12 | 13 | # targets not associated with files 14 | .PHONY: dependencies default build test coverage clean kill restart serve 15 | 16 | # check we have a couple of dependencies 17 | dependencies: 18 | @command -v fswatch --version >/dev/null 2>&1 || { printf >&2 "fswatch is not installed, please run: brew install fswatch\n"; exit 1; } 19 | 20 | # default targets to run when only running `make` 21 | default: dependencies test 22 | 23 | # clean up 24 | clean: 25 | go clean 26 | 27 | # run formatting tool and build 28 | build: dependencies clean 29 | go fmt 30 | go build 31 | 32 | # run unit tests with code coverage 33 | test: dependencies 34 | go test -cover $$(go list ./... | grep -v /vendor/) 35 | 36 | # generate code coverage report 37 | coverage: test 38 | go test -coverprofile=.coverage.out $$(go list ./... | grep -v /vendor/) 39 | go tool cover -html=.coverage.out 40 | 41 | # attempt to kill running server 42 | kill: 43 | -@killall -9 $(PROG) 2>/dev/null || true 44 | 45 | # attempt to build and start server 46 | restart: 47 | @make kill 48 | @make build; (if [ "$$?" -eq 0 ]; then (./${PROG} &); fi) 49 | 50 | # watch .go files for changes then recompile & try to start server 51 | # will also kill server after ctrl+c 52 | serve: dependencies 53 | @make restart 54 | @fswatch -o ./*.go ./*/*.go ./*/*/*.go | xargs -n1 -I{} make restart || make kill 55 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gopkg.in/mgo.v2/bson" 5 | "time" 6 | ) 7 | 8 | 9 | //used for database and JSON 10 | type User struct { 11 | Id bson.ObjectId `json:"id" bson:"_id"` 12 | FirstName string `valid:"required" json:"first_name" bson:"first_name"` 13 | LastName string `valid:"required" json:"last_name" bson:"last_name"` 14 | DisplayName string `valid:"required" json:"display_name" bson:"display_name"` 15 | Email string `valid:"email,required" json:"email" bson:"email"` 16 | HashedPassword string `json:"password,omitempty" bson:"hashed_password"` 17 | ImageFileName string `default:"default_image_profile.jpeg" json:"image_profile_url" bson:"image_profile_file_name"` 18 | IsEnable bool `default:"true" json:"is_enable" bson:"enable_status"` 19 | TrustedApps []TrustedApp `json:"-" bson:"trusted_apps,omitempty"` 20 | Roles []string `json:"roles" bson:"roles"` 21 | JoinedAt time.Time `json:"joined_at" bson:"joined_at"` 22 | UpdatedAt time.Time `json:"updated_at" bson:"updated_at"` 23 | } 24 | 25 | //only for database models 26 | type TrustedApp struct { 27 | Id bson.ObjectId `bson:"_id"` 28 | ClientId bson.ObjectId `bson:"_client"` 29 | RefreshToken string `bson:"refresh_token"` 30 | DeviceModel string `bson:"device_model,omitempty"` 31 | OSVersion string `bson:"os_version,omitempty"` 32 | AppVersion string `bson:"app_version,omitempty"` 33 | MessageTokenType string `bson:"message_token_type,omitempty"` 34 | MessageToken string `bson:"message_token,omitempty"` 35 | GrantedAt time.Time `bson:"granted_at"` 36 | } -------------------------------------------------------------------------------- /util/genrandom.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io" 5 | "time" 6 | "fmt" 7 | mrand "math/rand" 8 | "crypto/rand" 9 | ) 10 | 11 | var stdCharsType1 = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*") 12 | var stdCharsType2 = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") 13 | 14 | //generate new random password 15 | func NewRandomPassword(length int) string { 16 | return rand_char(length, stdCharsType1) 17 | } 18 | 19 | //generate new app key for new clients 20 | func NewAppKey() string { 21 | return rand_char(16, stdCharsType2) 22 | } 23 | 24 | //generate new refresh token uuid 25 | func GenerateNewRefreshToken() (string, error) { 26 | uuid := make([]byte, 16) 27 | n, err := io.ReadFull(rand.Reader, uuid) 28 | if n != len(uuid) || err != nil { 29 | return "", err 30 | } 31 | // variant bits; see section 4.1.1 32 | uuid[8] = uuid[8] &^ 0xc0 | 0x80 33 | // version 4 (pseudo-random); see section 4.1.3 34 | uuid[6] = uuid[6] &^ 0xf0 | 0x40 35 | return fmt.Sprintf("%x", uuid), nil 36 | } 37 | 38 | //generate random number in range 39 | func GenerateRandomNumber(min, max int) int { 40 | mrand.Seed(time.Now().UTC().UnixNano()) 41 | return mrand.Intn(max - min) + min 42 | } 43 | 44 | //random char string with length and bytes sets 45 | func rand_char(length int, chars []byte) string { 46 | new_random := make([]byte, length) 47 | // storage for random bytes 48 | random_data := make([]byte, length + (length / 4)) 49 | clen := byte(len(chars)) 50 | maxrb := byte(256 - (256 % len(chars))) 51 | i := 0 52 | for { 53 | if _, err := io.ReadFull(rand.Reader, random_data); err != nil { 54 | panic(err) 55 | } 56 | for _, c := range random_data { 57 | if c >= maxrb { 58 | continue 59 | } 60 | new_random[i] = chars[c % clen] 61 | i++ 62 | if i == length { 63 | return string(new_random) 64 | } 65 | } 66 | } 67 | panic("unreachable") 68 | } 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,go,intellij,windows 3 | 4 | ### OSX ### 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | 32 | ### Go ### 33 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 34 | *.o 35 | *.a 36 | *.so 37 | 38 | # Folders 39 | _obj 40 | _test 41 | 42 | # Architecture specific extensions/prefixes 43 | *.[568vq] 44 | [568vq].out 45 | 46 | *.cgo1.go 47 | *.cgo2.c 48 | _cgo_defun.c 49 | _cgo_gotypes.go 50 | _cgo_export.* 51 | 52 | _testmain.go 53 | 54 | *.exe 55 | *.test 56 | *.prof 57 | 58 | 59 | ### Intellij ### 60 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 61 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 62 | 63 | .idea/ 64 | 65 | ## VSCODE 66 | .vscode 67 | 68 | ## File-based project format: 69 | *.iws 70 | 71 | ## Plugin-specific files: 72 | 73 | # IntelliJ 74 | /out/ 75 | 76 | # mpeltonen/sbt-idea plugin 77 | .idea_modules/ 78 | 79 | # JIRA plugin 80 | atlassian-ide-plugin.xml 81 | 82 | # Crashlytics plugin (for Android Studio and IntelliJ) 83 | com_crashlytics_export_strings.xml 84 | crashlytics.properties 85 | crashlytics-build.properties 86 | fabric.properties 87 | 88 | ### Intellij Patch ### 89 | *.iml 90 | 91 | 92 | ### Windows ### 93 | # Windows image file caches 94 | Thumbs.db 95 | ehthumbs.db 96 | 97 | # Folder config file 98 | Desktop.ini 99 | 100 | # Recycle Bin used on file shares 101 | $RECYCLE.BIN/ 102 | 103 | # Windows Installer files 104 | *.cab 105 | *.msi 106 | *.msm 107 | *.msp 108 | 109 | # Windows shortcuts 110 | *.lnk 111 | 112 | 113 | # golang-rest-api-sample rules 114 | .pid 115 | vendor/ 116 | golang-rest-api-sample -------------------------------------------------------------------------------- /util/specialerror/specialerror.go: -------------------------------------------------------------------------------- 1 | package specialerror 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/labstack/echo" 8 | ) 9 | 10 | var ( 11 | ErrUnsupportedMediaType = New(http.StatusUnsupportedMediaType, http.StatusUnsupportedMediaType, "UN_SUPPORT_MEDIA_TYPE", "API support only application/json type") 12 | ErrSomeFieldAreNotValid = New(http.StatusBadRequest, http.StatusBadRequest, "SOME_FIELDS_ARE_NOT_VALID", "some fields are not valid in JSON format") 13 | ErrNotFound = New(http.StatusNotFound, http.StatusNotFound, "NOT_FOUND", "not found this resource") 14 | ErrUnauthorized = New(http.StatusUnauthorized, http.StatusUnauthorized, "UN_AUTHORIZED", "you don't have access to this resource") 15 | ErrNotValidCredentialInfo = New(http.StatusNonAuthoritativeInfo, http.StatusNonAuthoritativeInfo, "CREDENTIAL_INFORMATION_IS_NOT_VALID", "credential information is not valid") 16 | ErrMethodNotAllowed = New(http.StatusMethodNotAllowed, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed, please see the API document for more information") 17 | ErrNotValidItemId = New(http.StatusBadRequest, http.StatusBadRequest, "NOT_VALID_ITEM_ID", "not valid item id") 18 | ErrNotFoundAnyItemWithThisId = New(http.StatusNotFound, http.StatusNotFound, "NOT_FOUND_ANY_ITEM_WITH_THIS_ID", "not found any item with this id") 19 | ErrInternalServerError = New(http.StatusInternalServerError, http.StatusInternalServerError, "INTERNAL_SERVER_ERROR", "internal server error") 20 | ErrNotValidClientInformation = New(http.StatusNonAuthoritativeInfo, http.StatusNonAuthoritativeInfo, "CLIENT_INFORMATION_IS_NOT_VALID", "client information is not valid") 21 | ErrClientIsNotValidToCommunicate = New(http.StatusForbidden, http.StatusForbidden, "CLIENT_IS_NOT_VALID_TO_COMMUNICATE", "client is not valid to communicate") 22 | ErrRefreshTokenIsNotValid = New(http.StatusBadRequest, http.StatusBadRequest, "REFRESH_TOKEN_IS_NOT_VALID", "refresh token is not valid") 23 | ErrCanNotAccessToTheseResource = New(http.StatusForbidden, http.StatusForbidden, "CAN_NOT_ACCESS_TO_THESE_RESOURCES", "you can't access to these resources") 24 | ErrUserIsDisable = New(http.StatusForbidden, http.StatusForbidden, "USER_IS_DISABLED", "user is disabled !") 25 | ErrAlreadyHaveUserWithThisEmailAddress = New(http.StatusBadRequest, http.StatusBadRequest, "ALREADY_HAVE_USER_WITH_EMAIL_ADDRESS", "already have user with this email address") 26 | ) 27 | 28 | type Error struct { 29 | HttpCode int `json:"-"` 30 | Code int `json:"code" bson:"code"` 31 | Message string `json:"error" bson:"message"` 32 | Description string `json:"description" bson:"description"` 33 | } 34 | 35 | func New(httpCode int, code int, message string, description string) *Error { 36 | return &Error{httpCode, code, message, description} 37 | } 38 | 39 | func (e *Error) Error() string { 40 | return fmt.Sprintf("Error Code is %d - %s - %s", e.Code, e.Message, e.Description) 41 | } 42 | 43 | func CustomErrorHandler(err error, c echo.Context) { 44 | speError := New(http.StatusInternalServerError, 45 | http.StatusInternalServerError, 46 | http.StatusText(http.StatusInternalServerError), 47 | http.StatusText(http.StatusInternalServerError), 48 | ) 49 | if he, ok := err.(*Error); ok { 50 | speError = he 51 | } 52 | if !c.Response().Committed() { 53 | c.JSON(speError.HttpCode, speError) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "gopkg.in/mgo.v2" 9 | 10 | "github.com/labstack/echo" 11 | "github.com/labstack/echo/engine/standard" 12 | "github.com/labstack/echo/middleware" 13 | 14 | "github.com/atahani/golang-rest-api-sample/controller/article" 15 | "github.com/atahani/golang-rest-api-sample/controller/client" 16 | "github.com/atahani/golang-rest-api-sample/controller/user" 17 | "github.com/atahani/golang-rest-api-sample/util" 18 | "github.com/atahani/golang-rest-api-sample/util/specialerror" 19 | ) 20 | 21 | func main() { 22 | //Echo instance 23 | app := echo.New() 24 | 25 | //set custom binder to validate payloads 26 | bi := util.NewCustomBinderWithValidation() 27 | app.SetBinder(bi) 28 | 29 | //set custom error handler 30 | app.SetHTTPErrorHandler(specialerror.CustomErrorHandler) 31 | 32 | //set the port listener 33 | port := "8090" 34 | if os.Getenv("PORT") != "" { 35 | port = os.Getenv("PORT") 36 | } 37 | 38 | //Configs in different app environment mode 39 | var applicationEnv string 40 | var mongoDBDialInfo *mgo.DialInfo 41 | switch os.Getenv("APP_ENV") { 42 | case "development": 43 | applicationEnv = "development" 44 | mongoDBDialInfo = &mgo.DialInfo{ 45 | Addrs: []string{"localhost:27017"}, 46 | Timeout: 60 * time.Second, 47 | Database: "golang_sample_dev", 48 | } 49 | app.SetDebug(true) 50 | //Custom logger for console 51 | app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ 52 | Format: "${method}-${status} at > ${uri} < in ${response_time} - ${response_size} bytes\n", 53 | })) 54 | case "production": 55 | applicationEnv = "production" 56 | mongoDBDialInfo = &mgo.DialInfo{ 57 | Addrs: []string{"localhost"}, 58 | Timeout: 60 * time.Second, 59 | Database: "golang_sample", 60 | } 61 | app.Use(middleware.Recover()) 62 | app.SetDebug(false) 63 | app.Use(middleware.GzipWithConfig(middleware.GzipConfig{ 64 | Level: 5, 65 | })) 66 | default: 67 | applicationEnv = "development" 68 | mongoDBDialInfo = &mgo.DialInfo{ 69 | Addrs: []string{"localhost:27017"}, 70 | Timeout: 60 * time.Second, 71 | Database: "golang_sample_dev", 72 | } 73 | app.SetDebug(true) 74 | //Custom logger for console 75 | app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ 76 | Format: "${method}-${status} at > ${uri} < in ${response_time} - ${response_size} bytes\n", 77 | })) 78 | } 79 | 80 | //create a session with maintains a pool of socket connections to out mongodb 81 | mongoSession, err := mgo.DialWithInfo(mongoDBDialInfo) 82 | if err != nil { 83 | fmt.Printf("connection %s\n", err) 84 | } 85 | //check and ensure database indexes 86 | mongoSession.DB(mongoDBDialInfo.Database).C(user.ACCESS_TOKEN_COLLECTION_NAME).EnsureIndex(mgo.Index{ 87 | Key: []string{"expire_at"}, 88 | Unique: false, 89 | DropDups: false, 90 | Background: true, 91 | ExpireAfter: time.Second * 1, 92 | }) 93 | 94 | clientController := client.NewClientController(mongoSession, mongoDBDialInfo.Database) 95 | userController := user.NewUserController(mongoSession, mongoDBDialInfo.Database) 96 | articleController := article.NewArticleController(mongoSession, mongoDBDialInfo.Database) 97 | //auth endpoint 98 | app.Post("/auth/signup", userController.SignUpNewUser) 99 | app.Post("/auth/singin", userController.SignIn) 100 | app.Post("/auth/token/refresh", userController.RefreshAccessToken) 101 | 102 | //manage endpoint for client 103 | apiAdmin := app.Group("/api/manage", user.JWTAuthenticationMiddleware(mongoSession, mongoDBDialInfo.Database), user.AuthorizeUserByRolesMiddleware([]string{"admin"})) 104 | //manage clients 105 | apiAdmin.Get("/client", clientController.GetClients) 106 | apiAdmin.Post("/client", clientController.CreateNewClient) 107 | apiAdmin.Get("/client/:id", clientController.GetClientById) 108 | apiAdmin.Put("/client/:id", clientController.UpdateClientById) 109 | apiAdmin.Delete("/client/:id", clientController.DeleteClientById) 110 | 111 | apiUser := app.Group("/api", user.JWTAuthenticationMiddleware(mongoSession, mongoDBDialInfo.Database), user.AuthorizeUserByRolesMiddleware([]string{"user"})) 112 | //user profile 113 | apiUser.Put("/user/profile", userController.UpdateUserProfile) 114 | apiUser.Put("/user/password", userController.ChangeUserPassword) 115 | //article 116 | apiUser.Get("/article", articleController.GetArticlesOfUser) 117 | apiUser.Post("/article", articleController.CreateArticle) 118 | apiUser.Get("/article/:id", articleController.GetArticleById) 119 | apiUser.Put("/article/:id", articleController.UpdateArticleById) 120 | apiUser.Delete("/article/:id", articleController.DeleteArticleById) 121 | 122 | //start server 123 | fmt.Printf("API Management Listen to %s port in %s\n", port, applicationEnv) 124 | app.Run(standard.New(fmt.Sprint(":", port))) 125 | } 126 | -------------------------------------------------------------------------------- /controller/article/article_controller.go: -------------------------------------------------------------------------------- 1 | package article 2 | 3 | import ( 4 | "time" 5 | "net/http" 6 | 7 | "gopkg.in/mgo.v2" 8 | "gopkg.in/mgo.v2/bson" 9 | 10 | "github.com/labstack/echo" 11 | 12 | "github.com/atahani/golang-rest-api-sample/models" 13 | "github.com/atahani/golang-rest-api-sample/util/specialerror" 14 | "github.com/atahani/golang-rest-api-sample/util/operationresult" 15 | "github.com/atahani/golang-rest-api-sample/controller/user" 16 | ) 17 | 18 | const ( 19 | ARTICLE_COLLECTION_NAME = "articles" 20 | ) 21 | 22 | type ArticleController struct { 23 | Session *mgo.Session 24 | DBName string 25 | } 26 | 27 | func NewArticleController(s *mgo.Session, dbName string) *ArticleController { 28 | return &ArticleController{s, dbName} 29 | } 30 | 31 | func (ac ArticleController) CreateArticle(c echo.Context) error { 32 | //get user_id from context 33 | userId, ok := c.Get(user.USER_ID_KEY).(bson.ObjectId) 34 | if !ok { 35 | return specialerror.ErrInternalServerError 36 | } 37 | //get article from request 38 | article := models.Article{ 39 | Id:bson.NewObjectId(), 40 | UserId:userId, 41 | CreatedAt:time.Now(), 42 | UpdatedAt:time.Now(), 43 | } 44 | if err := c.Bind(&article); err != nil { 45 | return err 46 | } 47 | //get copy of db session 48 | session := ac.Session.Copy() 49 | defer session.Close() 50 | if err := session.DB(ac.DBName).C(ARTICLE_COLLECTION_NAME).Insert(&article); err != nil { 51 | return specialerror.ErrInternalServerError 52 | } 53 | //return the new article 54 | c.JSON(http.StatusCreated, article) 55 | return nil 56 | } 57 | 58 | func (ac ArticleController) GetArticleById(c echo.Context) error { 59 | //first check is id valid or not 60 | if !bson.IsObjectIdHex(c.Param("id")) { 61 | return specialerror.ErrNotValidItemId 62 | } 63 | //get copy of db session 64 | session := ac.Session.Copy() 65 | defer session.Close() 66 | article := models.Article{} 67 | if err := session.DB(ac.DBName).C(ARTICLE_COLLECTION_NAME).FindId(bson.ObjectIdHex(c.Param("id"))).One(&article); err != nil { 68 | if err == mgo.ErrNotFound { 69 | return specialerror.ErrNotFoundAnyItemWithThisId 70 | } 71 | return specialerror.ErrInternalServerError 72 | } 73 | //send the article 74 | c.JSON(http.StatusOK, article) 75 | return nil 76 | } 77 | 78 | func (ac ArticleController) DeleteArticleById(c echo.Context) error { 79 | //get the userId from context 80 | userId, ok := c.Get(user.USER_ID_KEY).(bson.ObjectId) 81 | if !ok { 82 | return specialerror.ErrInternalServerError 83 | } 84 | //first check is id valid or not 85 | if !bson.IsObjectIdHex(c.Param("id")) { 86 | return specialerror.ErrNotValidItemId 87 | } 88 | //get copy of db session 89 | session := ac.Session.Copy() 90 | defer session.Close() 91 | if err := session.DB(ac.DBName).C(ARTICLE_COLLECTION_NAME).Remove(bson.M{"_id":bson.ObjectIdHex(c.Param("id")), "user_id":userId}); err != nil { 92 | if err == mgo.ErrNotFound { 93 | return specialerror.ErrNotFoundAnyItemWithThisId 94 | } 95 | return specialerror.ErrInternalServerError 96 | } 97 | //inform user that this article removed successfully 98 | c.JSON(http.StatusOK, operationresult.SuccessfullyRemoved) 99 | return nil 100 | } 101 | 102 | func (ac ArticleController) UpdateArticleById(c echo.Context) error { 103 | //get the userId from context 104 | userId, ok := c.Get(user.USER_ID_KEY).(bson.ObjectId) 105 | if !ok { 106 | return specialerror.ErrInternalServerError 107 | } 108 | //first check is id valid or not 109 | if !bson.IsObjectIdHex(c.Param("id")) { 110 | return specialerror.ErrNotValidItemId 111 | } 112 | updatedArticle := models.Article{} 113 | //the binder check if struct is not valid return err 114 | if err := c.Bind(&updatedArticle); err != nil { 115 | return err 116 | } 117 | articleUpdateSet := bson.M{ 118 | "title": updatedArticle.Title, 119 | "content":updatedArticle.Content, 120 | "updated_at":time.Now(), 121 | } 122 | //get copy of db session 123 | session := ac.Session.Copy() 124 | defer session.Close() 125 | //NOTE: since we want to update article with one query we don't check is article own by this user separately 126 | if err := session.DB(ac.DBName).C(ARTICLE_COLLECTION_NAME).Update(bson.M{"_id":bson.ObjectIdHex(c.Param("id")), "user_id":userId}, bson.M{"$set":articleUpdateSet}); err != nil { 127 | if err == mgo.ErrNotFound { 128 | return specialerror.ErrCanNotAccessToTheseResource 129 | } 130 | return specialerror.ErrInternalServerError 131 | } 132 | //inform user this article update successfully 133 | c.JSON(http.StatusOK, operationresult.SuccessfullyUpdated) 134 | return nil 135 | } 136 | 137 | func (ac ArticleController) GetArticlesOfUser(c echo.Context) error { 138 | //get copy of db session 139 | session := ac.Session.Copy() 140 | defer session.Close() 141 | //get user_id from Context 142 | userId, ok := c.Get(user.USER_ID_KEY).(bson.ObjectId); 143 | if !ok { 144 | return specialerror.ErrInternalServerError 145 | } 146 | result := [] models.Article{} 147 | if err := session.DB(ac.DBName).C(ARTICLE_COLLECTION_NAME).Find(bson.M{"user_id":userId}).All(&result); err != nil { 148 | return specialerror.ErrInternalServerError 149 | } 150 | //send articles 151 | c.JSON(http.StatusOK, result) 152 | return nil 153 | } 154 | -------------------------------------------------------------------------------- /controller/client/client_controller.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "time" 5 | "net/http" 6 | 7 | "gopkg.in/mgo.v2" 8 | "gopkg.in/mgo.v2/bson" 9 | "gopkg.in/mcuadros/go-defaults.v1" 10 | 11 | "github.com/labstack/echo" 12 | 13 | "github.com/atahani/golang-rest-api-sample/models" 14 | "github.com/atahani/golang-rest-api-sample/util" 15 | "github.com/atahani/golang-rest-api-sample/util/specialerror" 16 | "github.com/atahani/golang-rest-api-sample/util/operationresult" 17 | ) 18 | 19 | const ( 20 | CLIENT_COLLECTION_NAME = "clients" 21 | WEB_PLATFORM_TYPE = "web" 22 | ) 23 | 24 | type ClientController struct { 25 | Session *mgo.Session 26 | DBName string 27 | } 28 | 29 | func NewClientController(s *mgo.Session, dbName string) *ClientController { 30 | return &ClientController{s, dbName} 31 | } 32 | 33 | func (cc ClientController) ClientAuthorization(s *mgo.Session, dbName, appId, appKey string) (bool, error) { 34 | //get copy of db session 35 | session := s.Copy() 36 | defer session.Close() 37 | //check the client id format 38 | if !bson.IsObjectIdHex(appId) { 39 | return false, specialerror.ErrNotValidClientInformation 40 | } 41 | //find client with this appId 42 | client := models.Client{} 43 | if err := session.DB(cc.DBName).C(CLIENT_COLLECTION_NAME).FindId(bson.ObjectIdHex(appId)).One(&client); err != nil { 44 | if err == mgo.ErrNotFound { 45 | return false, specialerror.ErrClientIsNotValidToCommunicate 46 | } 47 | return false, specialerror.ErrInternalServerError 48 | } 49 | //first check is client enable or not 50 | if !client.IsEnable { 51 | return false, specialerror.ErrClientIsNotValidToCommunicate 52 | } 53 | //if it's not web platform check the AppKey is valid or not 54 | if client.PlatformType != WEB_PLATFORM_TYPE { 55 | //check the AppKey is valid or not 56 | if client.HashedAppKey() != appKey { 57 | return false, specialerror.ErrClientIsNotValidToCommunicate 58 | } 59 | return true, nil 60 | } 61 | return true, nil 62 | } 63 | 64 | func (cc ClientController) CreateNewClient(c echo.Context) error { 65 | //get copy of db session 66 | session := cc.Session.Copy() 67 | defer session.Close() 68 | appKey := util.NewAppKey() 69 | client := models.Client{ 70 | AppId: bson.NewObjectId(), 71 | AppKey: appKey, 72 | } 73 | defaults.SetDefaults(&client) 74 | client.CreatedAt = time.Now() 75 | client.UpdatedAt = time.Now() 76 | //the binder check if struct is not valid return err 77 | if err := c.Bind(&client); err != nil { 78 | return err 79 | } 80 | //save the client to DB 81 | if err := session.DB(cc.DBName).C(CLIENT_COLLECTION_NAME).Insert(&client); err != nil { 82 | return specialerror.ErrInternalServerError 83 | } 84 | //replace the hashed App Key 85 | client.HashedAppKey() 86 | c.JSON(http.StatusCreated, client) 87 | return nil 88 | } 89 | 90 | func (cc ClientController) UpdateClientById(c echo.Context) error { 91 | //first check is id valid or not 92 | if !bson.IsObjectIdHex(c.Param("id")) { 93 | return specialerror.ErrNotValidItemId 94 | } 95 | updatedClient := models.Client{} 96 | defaults.SetDefaults(&updatedClient) 97 | //the binder check if struct is not valid return err 98 | if err := c.Bind(&updatedClient); err != nil { 99 | return err 100 | } 101 | //get copy of session 102 | session := cc.Session.Copy() 103 | defer session.Close() 104 | clientUpdateSet := bson.M{ 105 | "name":updatedClient.Name, 106 | "description":updatedClient.Description, 107 | "is_enable":updatedClient.IsEnable, 108 | "platform_type": updatedClient.PlatformType, 109 | "updated_at": time.Now(), 110 | } 111 | //update the client information by one query 112 | if err := session.DB(cc.DBName).C(CLIENT_COLLECTION_NAME).UpdateId(bson.ObjectIdHex(c.Param("id")), clientUpdateSet); err != nil { 113 | if err == mgo.ErrNotFound { 114 | return specialerror.ErrNotFoundAnyItemWithThisId 115 | } 116 | return specialerror.ErrInternalServerError 117 | } 118 | //inform the item successfully updated 119 | c.JSON(http.StatusOK, operationresult.SuccessfullyUpdated) 120 | return nil 121 | } 122 | 123 | func (cc ClientController) GetClientById(c echo.Context) error { 124 | //first check is id valid or not 125 | if !bson.IsObjectIdHex(c.Param("id")) { 126 | return specialerror.ErrNotValidItemId 127 | } 128 | //get copy of session 129 | session := cc.Session.Copy() 130 | defer session.Close() 131 | client := models.Client{} 132 | if err := session.DB(cc.DBName).C(CLIENT_COLLECTION_NAME).FindId(bson.ObjectIdHex(c.Param("id"))).One(&client); err != nil { 133 | if err == mgo.ErrNotFound { 134 | return specialerror.ErrNotFoundAnyItemWithThisId 135 | } 136 | return specialerror.ErrInternalServerError 137 | } 138 | //replace the hashed app key 139 | client.HashedAppKey() 140 | c.JSON(http.StatusOK, client) 141 | return nil 142 | } 143 | 144 | func (cc ClientController) GetClients(c echo.Context) error { 145 | //get copy of session 146 | session := cc.Session.Copy() 147 | defer session.Close() 148 | result := [] models.Client{} 149 | //get client from database 150 | if err := session.DB(cc.DBName).C(CLIENT_COLLECTION_NAME).Find(nil).All(&result); err != nil { 151 | return specialerror.ErrInternalServerError 152 | } 153 | //should replace the hashed app key 154 | for i, cli := range result { 155 | result[i].AppKey = cli.HashedAppKey() 156 | } 157 | c.JSON(http.StatusOK, result) 158 | return nil 159 | } 160 | 161 | func (cc ClientController) DeleteClientById(c echo.Context) error { 162 | //first check is id valid or not 163 | if !bson.IsObjectIdHex(c.Param("id")) { 164 | return specialerror.ErrNotValidItemId 165 | } 166 | //get copy of session 167 | session := cc.Session.Copy() 168 | defer session.Close() 169 | if err := session.DB(cc.DBName).C(CLIENT_COLLECTION_NAME).RemoveId(bson.ObjectIdHex(c.Param("id"))); err != nil { 170 | if err == mgo.ErrNotFound { 171 | return specialerror.ErrNotFoundAnyItemWithThisId 172 | } 173 | return specialerror.ErrInternalServerError 174 | } 175 | //inform this item successfully removed 176 | c.JSON(http.StatusOK, operationresult.SuccessfullyRemoved) 177 | return nil 178 | } 179 | -------------------------------------------------------------------------------- /controller/client/client_controller_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "os" 5 | "bytes" 6 | "fmt" 7 | "testing" 8 | "encoding/json" 9 | 10 | "github.com/labstack/echo" 11 | "github.com/labstack/echo/engine" 12 | "github.com/labstack/echo/test" 13 | 14 | "github.com/atahani/golang-rest-api-sample/models" 15 | "github.com/atahani/golang-rest-api-sample/util/operationresult" 16 | "github.com/atahani/golang-rest-api-sample/util/specialerror" 17 | "github.com/atahani/golang-rest-api-sample/util/testhelper" 18 | ) 19 | 20 | var testingProvider testhelper.TestingProvider 21 | var newAppIdStrForClient string 22 | 23 | func TestCreateNewClient(t *testing.T) { 24 | //get copy of session 25 | session := testingProvider.Session.Copy() 26 | defer session.Close() 27 | clientController := NewClientController(session, testhelper.DB_TEST_NAME) 28 | //define different case 29 | path := "/api/manage/client" 30 | method := echo.POST 31 | cases := []struct { 32 | req engine.Request 33 | res *test.ResponseRecorder 34 | expectedError error 35 | }{ 36 | { 37 | req: test.NewRequest(method, path, bytes.NewBuffer([]byte(`{"name":"client for web"}`))), 38 | res: test.NewResponseRecorder(), 39 | expectedError: nil, 40 | }, 41 | { 42 | req: test.NewRequest(method, path, bytes.NewBuffer([]byte(`{"platform_type":"android"}`))), 43 | res: test.NewResponseRecorder(), 44 | expectedError: specialerror.ErrSomeFieldAreNotValid, 45 | }, 46 | } 47 | for _, c := range cases { 48 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 49 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 50 | if err := clientController.CreateNewClient(context); err != c.expectedError { 51 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 52 | } 53 | if c.expectedError == nil { 54 | //decode response body 55 | client := models.Client{} 56 | if err := json.NewDecoder(c.res.Body).Decode(&client); err == nil { 57 | newAppIdStrForClient = string(client.AppId.Hex()) 58 | } 59 | } 60 | } 61 | } 62 | 63 | func TestGetClientById(t *testing.T) { 64 | //since the path have id param should add it to Router 65 | testingProvider.Router.Add(echo.GET, "/api/manage/client/:id", nil, testingProvider.Echo) 66 | //get session 67 | session := testingProvider.Session.Copy() 68 | defer session.Close() 69 | clientController := NewClientController(session, testhelper.DB_TEST_NAME) 70 | //define different cases 71 | path := fmt.Sprintf("/api/manage/client/%s", newAppIdStrForClient) 72 | method := echo.GET 73 | cases := []struct { 74 | path string 75 | req engine.Request 76 | res *test.ResponseRecorder 77 | expectedError error 78 | }{ 79 | { 80 | path: fmt.Sprintf("%ssomeinvalid", path), 81 | req: test.NewRequest(method, fmt.Sprintf("%ssomeinvalid", path), nil), 82 | res: test.NewResponseRecorder(), 83 | expectedError: specialerror.ErrNotValidItemId, 84 | }, 85 | { 86 | path: path, 87 | req: test.NewRequest(method, path, nil), 88 | res: test.NewResponseRecorder(), 89 | expectedError: nil, 90 | }, 91 | } 92 | for _, c := range cases { 93 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 94 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 95 | testingProvider.Router.Find(echo.GET, c.path, context) 96 | if err := clientController.GetClientById(context); err != c.expectedError { 97 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 98 | } 99 | } 100 | } 101 | 102 | func TestUpdateClientById(t *testing.T) { 103 | //since the path have id param should add it to Router 104 | testingProvider.Router.Add(echo.PUT, "/api/manage/client/:id", nil, testingProvider.Echo) 105 | //get session 106 | session := testingProvider.Session.Copy() 107 | defer session.Close() 108 | clientController := NewClientController(session, testhelper.DB_TEST_NAME) 109 | //define different case 110 | path := fmt.Sprintf("/api/manage/client/%s", newAppIdStrForClient) 111 | method := echo.PUT 112 | cases := []struct { 113 | path string 114 | req engine.Request 115 | res *test.ResponseRecorder 116 | responseBody *operationresult.OperationResult 117 | expectedError error 118 | }{ 119 | { 120 | path: fmt.Sprintf("%ssomeinvalid", path), 121 | req: test.NewRequest(method, fmt.Sprintf("%ssomeinvalid", path), nil), 122 | res: test.NewResponseRecorder(), 123 | responseBody: operationresult.New("", ""), 124 | expectedError: specialerror.ErrNotValidItemId, 125 | }, 126 | { 127 | path: path, 128 | req: test.NewRequest(method, path, 129 | bytes.NewBuffer([]byte(`{"name":"updated name"}`))), 130 | res: test.NewResponseRecorder(), 131 | responseBody: operationresult.SuccessfullyUpdated, 132 | expectedError: nil, 133 | }, 134 | { 135 | path: path, 136 | req: test.NewRequest(method, path, 137 | bytes.NewBuffer([]byte(`{"platform_type":"android"}`))), 138 | res: test.NewResponseRecorder(), 139 | responseBody: operationresult.New("", ""), 140 | expectedError: specialerror.ErrSomeFieldAreNotValid, 141 | }, 142 | } 143 | for _, c := range cases { 144 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 145 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 146 | testingProvider.Router.Find(echo.PUT, c.path, context) 147 | if err := clientController.UpdateClientById(context); err != c.expectedError { 148 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 149 | } 150 | } 151 | } 152 | 153 | func TestGetClients(t *testing.T) { 154 | //get session 155 | session := testingProvider.Session.Copy() 156 | defer session.Close() 157 | clientController := NewClientController(session, testhelper.DB_TEST_NAME) 158 | path := "/api/manage/client" 159 | req := test.NewRequest(echo.GET, path, nil) 160 | res := test.NewResponseRecorder() 161 | context := echo.NewContext(req, res, testingProvider.Echo) 162 | if err := clientController.GetClients(context); err != nil { 163 | t.Errorf("Error should %q \t but get %q", nil, err) 164 | } 165 | //check the number of clients 166 | result := []models.Client{} 167 | if err := json.NewDecoder(res.Body).Decode(&result); err == nil { 168 | if len(result) == 0 { 169 | t.Error("should at least one client in this get clients request !") 170 | } 171 | } 172 | } 173 | 174 | func TestDeleteClientById(t *testing.T) { 175 | //since the URL have id param should add it to Router 176 | testingProvider.Router.Add(echo.DELETE, "/api/manage/client/:id", nil, testingProvider.Echo) 177 | //copy db session 178 | session := testingProvider.Session.Copy() 179 | defer session.Close() 180 | clientController := NewClientController(session, testhelper.DB_TEST_NAME) 181 | //define different cases 182 | path := fmt.Sprintf("/api/manage/client/%s", newAppIdStrForClient) 183 | method := echo.DELETE 184 | cases := []struct { 185 | path string 186 | req engine.Request 187 | res *test.ResponseRecorder 188 | responseBody *operationresult.OperationResult 189 | expectedError error 190 | }{ 191 | { 192 | path: fmt.Sprintf("%ssomeinvalid", path), 193 | req: test.NewRequest(method, fmt.Sprintf("%ssomeinvalid", path), nil), 194 | res: test.NewResponseRecorder(), 195 | responseBody: operationresult.New("", ""), 196 | expectedError: specialerror.ErrNotValidItemId, 197 | }, 198 | { 199 | path: path, 200 | req: test.NewRequest(method, path, nil), 201 | res: test.NewResponseRecorder(), 202 | responseBody: operationresult.SuccessfullyRemoved, 203 | expectedError: nil, 204 | }, 205 | { 206 | path: path, 207 | req: test.NewRequest(method, path, nil), 208 | res: test.NewResponseRecorder(), 209 | responseBody: operationresult.New("", ""), 210 | expectedError: specialerror.ErrNotFoundAnyItemWithThisId, //since the last case delete this client now notfound :) 211 | }, 212 | } 213 | for _, c := range cases { 214 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 215 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 216 | testingProvider.Router.Find(echo.DELETE, c.path, context) 217 | if err := clientController.DeleteClientById(context); err != c.expectedError { 218 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 219 | } 220 | } 221 | } 222 | 223 | func TestMain(m *testing.M) { 224 | //start of testing 225 | testingProvider = testhelper.TestingProvider{} 226 | testingProvider.StartTesting() 227 | ret := m.Run() 228 | os.Exit(ret) 229 | } 230 | -------------------------------------------------------------------------------- /controller/article/article_controller_test.go: -------------------------------------------------------------------------------- 1 | package article 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "testing" 7 | "bytes" 8 | "encoding/json" 9 | 10 | "gopkg.in/mgo.v2/bson" 11 | 12 | "github.com/labstack/echo" 13 | "github.com/labstack/echo/test" 14 | "github.com/labstack/echo/engine" 15 | 16 | "github.com/atahani/golang-rest-api-sample/models" 17 | "github.com/atahani/golang-rest-api-sample/util/testhelper" 18 | "github.com/atahani/golang-rest-api-sample/util/specialerror" 19 | "github.com/atahani/golang-rest-api-sample/util/operationresult" 20 | "github.com/atahani/golang-rest-api-sample/controller/user" 21 | ) 22 | 23 | var testingProvider testhelper.TestingProvider 24 | var newArticleIdStr string 25 | var userIdObj bson.ObjectId 26 | 27 | func TestCreateArticle(t *testing.T) { 28 | //get copy of session 29 | session := testingProvider.Session.Copy() 30 | defer session.Close() 31 | articleController := NewArticleController(session, testhelper.DB_TEST_NAME) 32 | //define different cases 33 | path := "/api/article" 34 | method := echo.POST 35 | cases := []struct { 36 | req engine.Request 37 | res *test.ResponseRecorder 38 | expectedError error 39 | }{ 40 | { 41 | req: test.NewRequest(method, path, bytes.NewBuffer([]byte(`{"title":"new article"}`))), 42 | res: test.NewResponseRecorder(), 43 | expectedError: specialerror.ErrSomeFieldAreNotValid, 44 | }, 45 | { 46 | req: test.NewRequest(method, path, bytes.NewBuffer([]byte(`{"title":"new article","content":"some content ..."}`))), 47 | res: test.NewResponseRecorder(), 48 | expectedError: nil, 49 | }, 50 | } 51 | for _, c := range cases { 52 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 53 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 54 | //set user_id for context 55 | context.Set(user.USER_ID_KEY, userIdObj) 56 | if err := articleController.CreateArticle(context); err != c.expectedError { 57 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 58 | } 59 | if c.expectedError == nil { 60 | //decode response body 61 | article := models.Article{} 62 | if err := json.NewDecoder(c.res.Body).Decode(&article); err == nil { 63 | newArticleIdStr = string(article.Id.Hex()) 64 | } 65 | } 66 | } 67 | } 68 | 69 | func TestGetArticleById(t *testing.T) { 70 | //since the path have id param should add it to routeer 71 | testingProvider.Router.Add(echo.GET, "/api/article/:id", nil, testingProvider.Echo) 72 | //get copy of db session 73 | session := testingProvider.Session.Copy() 74 | defer session.Close() 75 | articleController := NewArticleController(session, testhelper.DB_TEST_NAME) 76 | path := fmt.Sprintf("/api/article/%s", newArticleIdStr) 77 | method := echo.GET 78 | cases := []struct { 79 | path string 80 | req engine.Request 81 | res *test.ResponseRecorder 82 | expectedError error 83 | }{ 84 | { 85 | path: fmt.Sprintf("%ssomeinvalid", path), 86 | req: test.NewRequest(method, fmt.Sprintf("%ssomeinvalid", path), nil), 87 | res: test.NewResponseRecorder(), 88 | expectedError: specialerror.ErrNotValidItemId, 89 | }, 90 | { 91 | path: path, 92 | req: test.NewRequest(method, path, nil), 93 | res: test.NewResponseRecorder(), 94 | expectedError: nil, 95 | }, 96 | } 97 | for _, c := range cases { 98 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 99 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 100 | testingProvider.Router.Find(echo.GET, c.path, context) 101 | //set the user_id for context 102 | context.Set(user.USER_ID_KEY, userIdObj) 103 | if err := articleController.GetArticleById(context); err != c.expectedError { 104 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 105 | } 106 | } 107 | } 108 | 109 | func TestUpdateArticleById(t *testing.T) { 110 | //add path with id to router 111 | testingProvider.Router.Add(echo.PUT, "/api/article/:id", nil, testingProvider.Echo) 112 | session := testingProvider.Session.Copy() 113 | defer session.Close() 114 | articleController := NewArticleController(session, testhelper.DB_TEST_NAME) 115 | //define different cases 116 | path := fmt.Sprintf("/api/article/%s", newArticleIdStr) 117 | method := echo.PUT 118 | cases := []struct { 119 | path string 120 | req engine.Request 121 | res *test.ResponseRecorder 122 | responseBody *operationresult.OperationResult 123 | expectedError error 124 | }{ 125 | { 126 | path: fmt.Sprintf("%ssomeinvalid", path), 127 | req: test.NewRequest(method, fmt.Sprintf("%ssomeinvalid", path), nil), 128 | res: test.NewResponseRecorder(), 129 | responseBody: operationresult.New("", ""), 130 | expectedError: specialerror.ErrNotValidItemId, 131 | }, 132 | { 133 | path: path, 134 | req: test.NewRequest(method, path, bytes.NewBuffer([]byte(`{"title":"updated article","content":"some content updated ..."}`))), 135 | res: test.NewResponseRecorder(), 136 | responseBody: operationresult.SuccessfullyUpdated, 137 | expectedError: nil, 138 | }, 139 | { 140 | path: path, 141 | req: test.NewRequest(method, path, bytes.NewBuffer([]byte(`{"title":"new article"}`))), 142 | res: test.NewResponseRecorder(), 143 | responseBody: operationresult.New("", ""), 144 | expectedError: specialerror.ErrSomeFieldAreNotValid, 145 | }, 146 | } 147 | for _, c := range cases { 148 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 149 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 150 | testingProvider.Router.Find(method, c.path, context) 151 | //set the user_id for context 152 | context.Set(user.USER_ID_KEY, userIdObj) 153 | if err := articleController.UpdateArticleById(context); err != c.expectedError { 154 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 155 | } 156 | } 157 | } 158 | 159 | func TestGetArticlesOfUser(t *testing.T) { 160 | session := testingProvider.Session.Copy() 161 | defer session.Close() 162 | articleController := NewArticleController(session, testhelper.DB_TEST_NAME) 163 | path := "/api/article" 164 | req := test.NewRequest(echo.GET, path, nil) 165 | res := test.NewResponseRecorder() 166 | context := echo.NewContext(req, res, testingProvider.Echo) 167 | //set the user_id for context 168 | context.Set(user.USER_ID_KEY, userIdObj) 169 | if err := articleController.GetArticlesOfUser(context); err != nil { 170 | t.Errorf("Error should %q \t but get %q", nil, err) 171 | } 172 | //check have at least one article 173 | result := [] models.Article{} 174 | if err := json.NewDecoder(res.Body).Decode(&result); err == nil { 175 | if len(result) == 0 { 176 | t.Error("should at least one article in get articles request") 177 | } 178 | } 179 | } 180 | 181 | func TestDeleteArticleById(t *testing.T) { 182 | //since the path have id param should add it to routeer 183 | testingProvider.Router.Add(echo.DELETE, "/api/article/:id", nil, testingProvider.Echo) 184 | //get copy of db session 185 | session := testingProvider.Session.Copy() 186 | defer session.Close() 187 | articleController := NewArticleController(session, testhelper.DB_TEST_NAME) 188 | path := fmt.Sprintf("/api/article/%s", newArticleIdStr) 189 | method := echo.DELETE 190 | cases := []struct { 191 | path string 192 | req engine.Request 193 | res *test.ResponseRecorder 194 | responseBody *operationresult.OperationResult 195 | expectedError error 196 | }{ 197 | { 198 | path: fmt.Sprintf("%ssomeinvalid", path), 199 | req: test.NewRequest(method, fmt.Sprintf("%ssomeinvalid", path), nil), 200 | res: test.NewResponseRecorder(), 201 | responseBody: operationresult.New("", ""), 202 | expectedError: specialerror.ErrNotValidItemId, 203 | }, 204 | { 205 | path: path, 206 | req: test.NewRequest(method, path, nil), 207 | res: test.NewResponseRecorder(), 208 | responseBody: operationresult.SuccessfullyRemoved, 209 | expectedError: nil, 210 | }, 211 | } 212 | for _, c := range cases { 213 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 214 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 215 | testingProvider.Router.Find(method, c.path, context) 216 | //set the user_id for context 217 | context.Set(user.USER_ID_KEY, userIdObj) 218 | if err := articleController.DeleteArticleById(context); err != c.expectedError { 219 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 220 | } 221 | } 222 | } 223 | 224 | func TestMain(m *testing.M) { 225 | //start of testing 226 | testingProvider = testhelper.TestingProvider{} 227 | testingProvider.StartTesting() 228 | userIdObj = bson.NewObjectId() 229 | ret := m.Run() 230 | os.Exit(ret) 231 | } 232 | -------------------------------------------------------------------------------- /controller/user/user_controller.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "time" 5 | "sync" 6 | "strings" 7 | "net/http" 8 | 9 | "golang.org/x/crypto/bcrypt" 10 | "gopkg.in/mgo.v2" 11 | "gopkg.in/mgo.v2/bson" 12 | "gopkg.in/mcuadros/go-defaults.v1" 13 | 14 | "github.com/dgrijalva/jwt-go" 15 | "github.com/labstack/echo" 16 | 17 | "github.com/atahani/golang-rest-api-sample/controller/client" 18 | "github.com/atahani/golang-rest-api-sample/models" 19 | "github.com/atahani/golang-rest-api-sample/util" 20 | "github.com/atahani/golang-rest-api-sample/util/operationresult" 21 | "github.com/atahani/golang-rest-api-sample/util/specialerror" 22 | ) 23 | 24 | const ( 25 | USER_COLLECTION_NAME = "users" 26 | ACCESS_TOKEN_COLLECTION_NAME = "accessTokens" 27 | JWT_SIGNING_KEY_PHRASE = "sectet_keys_to_hash_jwt_token" 28 | BEARER_AUTHENTICATION_TYPE = "Bearer" 29 | ROLES_KEY = "roles" 30 | USER_ID_KEY = "user_id" 31 | TOKEN_ID_KEY = "token_id" 32 | TRUSTED_APP_ID_KEY = "trusted_app_id" 33 | ) 34 | 35 | type UserController struct { 36 | Session *mgo.Session 37 | DBName string 38 | } 39 | 40 | func NewUserController(s *mgo.Session, dbName string) *UserController { 41 | return &UserController{s, dbName} 42 | } 43 | 44 | //echo middleware for checking JWT token is valid and authorize request 45 | func JWTAuthenticationMiddleware(s *mgo.Session, dbName string) echo.MiddlewareFunc { 46 | return func(next echo.HandlerFunc) echo.HandlerFunc { 47 | return func(c echo.Context) error { 48 | authHeader := c.Request().Header().Get(echo.HeaderAuthorization) 49 | l := len(BEARER_AUTHENTICATION_TYPE) 50 | he := specialerror.ErrUnauthorized 51 | if len(authHeader) > l + 1 && authHeader[:l] == BEARER_AUTHENTICATION_TYPE { 52 | token := string(authHeader[l + 1:]) 53 | t, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { 54 | //always check the signing method 55 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 56 | return nil, he 57 | } 58 | //return the key for validation 59 | return []byte(JWT_SIGNING_KEY_PHRASE), nil 60 | }) 61 | if err == nil && t.Valid { 62 | //get copy of database session 63 | session := s.Copy() 64 | defer session.Close() 65 | accessToken := models.AccessToken{} 66 | if err := session.DB(dbName).C(ACCESS_TOKEN_COLLECTION_NAME).Find(bson.M{"token": token}).One(&accessToken); err != nil { 67 | if err == mgo.ErrNotFound { 68 | return he 69 | } 70 | return specialerror.ErrInternalServerError 71 | } 72 | //get the user and check is enable or not 73 | user := models.User{} 74 | if err := session.DB(dbName).C(USER_COLLECTION_NAME).FindId(accessToken.UserId).One(&user); err != nil { 75 | return specialerror.ErrInternalServerError 76 | } 77 | if user.IsEnable { 78 | //set some information that need in routes handler 79 | c.Set(USER_ID_KEY, user.Id) 80 | c.Set(ROLES_KEY, user.Roles) 81 | c.Set(TOKEN_ID_KEY, accessToken.Id) 82 | c.Set(TRUSTED_APP_ID_KEY, accessToken.TrustedAppId) 83 | //process the next and finish this middleware 84 | return next(c) 85 | } 86 | return specialerror.ErrUserIsDisable 87 | } 88 | } 89 | return he 90 | } 91 | } 92 | } 93 | 94 | //echo middleware to check is this user have role 95 | func AuthorizeUserByRolesMiddleware(roles []string) echo.MiddlewareFunc { 96 | return func(next echo.HandlerFunc) echo.HandlerFunc { 97 | return func(c echo.Context) error { 98 | data := c.Get(ROLES_KEY) 99 | if roleSlice, ok := data.([]string); ok { 100 | //check is have these role or not 101 | for _, v := range roles { 102 | if !util.IsStringInSlice(v, roleSlice) { 103 | return specialerror.ErrCanNotAccessToTheseResource 104 | } 105 | } 106 | //it's mean the user have all of the roles 107 | //process the next and finish this middleware 108 | return next(c) 109 | } 110 | return specialerror.ErrInternalServerError 111 | } 112 | } 113 | } 114 | 115 | func (uc UserController) SignUpNewUser(c echo.Context) error { 116 | //create the user model 117 | signUpModel := models.SignUpRequest{} 118 | if err := c.Bind(&signUpModel); err != nil { 119 | return err 120 | } 121 | //get copy of session 122 | session := uc.Session.Copy() 123 | defer session.Copy() 124 | //TODO : please NOTE should check is't new email for this user ? if yes ? send verification email to this email 125 | //first check is already have user with this email address > unique or not 126 | count, err := session.DB(uc.DBName).C(USER_COLLECTION_NAME).Find(bson.M{"email": strings.ToLower(signUpModel.Email)}).Count(); if err != nil { 127 | return specialerror.ErrInternalServerError 128 | } 129 | if count != 0 { 130 | return specialerror.ErrAlreadyHaveUserWithThisEmailAddress 131 | } 132 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(signUpModel.Password), bcrypt.DefaultCost); if err != nil { 133 | return specialerror.ErrInternalServerError 134 | } 135 | //check client information is valid or not 136 | cliController := client.NewClientController(uc.Session, uc.DBName) 137 | isWebClient, err := cliController.ClientAuthorization(uc.Session, uc.DBName, signUpModel.AppId, signUpModel.AppKey); if err != nil { 138 | return err 139 | } 140 | //create new user and assign attributes 141 | u := models.User{ 142 | Id: bson.NewObjectId(), 143 | FirstName: signUpModel.FirstName, 144 | LastName: signUpModel.LastName, 145 | DisplayName: signUpModel.DisplayName, 146 | Email: strings.ToLower(signUpModel.Email), 147 | HashedPassword: string(hashedPassword), 148 | Roles: []string{"user"}, 149 | JoinedAt: time.Now(), 150 | UpdatedAt: time.Now(), 151 | } 152 | //set defaults values for user model 153 | defaults.SetDefaults(&u) 154 | //store new user into database 155 | if err := session.DB(uc.DBName).C(USER_COLLECTION_NAME).Insert(u); err != nil { 156 | return specialerror.ErrInternalServerError 157 | } 158 | //should generate access token and send it 159 | authResponse, err := generateAccessToken(session, uc.DBName, &u, bson.ObjectIdHex(signUpModel.AppId), bson.NewObjectId(), signUpModel.DeviceModel, false, isWebClient); if err != nil { 160 | return err 161 | } 162 | //return the authentication response 163 | c.JSON(http.StatusOK, &authResponse) 164 | return nil 165 | } 166 | 167 | //authenticate the user with credential information email/username with password and generate access token 168 | func (uc UserController) SignIn(c echo.Context) error { 169 | //get copy of database session 170 | session := uc.Session.Copy() 171 | defer session.Close() 172 | //check the credential information if it's valid 173 | signInRequest := models.SignInRequest{} 174 | //populate user credential information from request 175 | //the binder check if struct is not valid return err 176 | if err := c.Bind(&signInRequest); err != nil { 177 | return err 178 | } 179 | //check client information is valid or not 180 | cliController := client.NewClientController(uc.Session, uc.DBName) 181 | isWebClient, err := cliController.ClientAuthorization(uc.Session, uc.DBName, signInRequest.AppId, signInRequest.AppKey); if err != nil { 182 | return err 183 | } 184 | var user models.User 185 | //get user by email 186 | if err := session.DB(uc.DBName).C(USER_COLLECTION_NAME).Find(bson.M{"email": strings.ToLower(signInRequest.Email)}).One(&user); err != nil { 187 | if err == mgo.ErrNotFound { 188 | return specialerror.ErrNotValidCredentialInfo 189 | } 190 | return specialerror.ErrInternalServerError 191 | } 192 | //check user password with hashed password in db 193 | if err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(signInRequest.Password)); err != nil { 194 | return specialerror.ErrNotValidCredentialInfo 195 | } 196 | //it's mean the credential information is valid so should generate JWT token as send it as JSON 197 | authResponse, err := generateAccessToken(session, uc.DBName, &user, bson.ObjectIdHex(signInRequest.AppId), bson.NewObjectId(), signInRequest.DeviceModel, false, isWebClient); if err != nil { 198 | return err 199 | } 200 | //return the authentication response 201 | c.JSON(http.StatusOK, &authResponse) 202 | return nil 203 | } 204 | 205 | //refresh the user access token 206 | func (ac UserController) RefreshAccessToken(c echo.Context) error { 207 | //get copy of database session 208 | session := ac.Session.Copy() 209 | defer session.Close() 210 | refreshTokenRequest := models.RefreshTokenRequest{} 211 | //the binder check if struct is not valid return err 212 | if err := c.Bind(&refreshTokenRequest); err != nil { 213 | return err 214 | } 215 | cliController := client.NewClientController(ac.Session, ac.DBName) 216 | //check client information is valid or not 217 | isWebClient, err := cliController.ClientAuthorization(session, ac.DBName, refreshTokenRequest.AppId, refreshTokenRequest.AppKey); if err != nil { 218 | return err 219 | } 220 | //check is refresh token valid or not 221 | user := models.User{} 222 | if err := session.DB(ac.DBName).C(USER_COLLECTION_NAME).Find(bson.M{"trusted_apps._client": bson.ObjectIdHex(refreshTokenRequest.AppId), "trusted_apps.refresh_token": refreshTokenRequest.RefreshToken}).One(&user); err != nil { 223 | if err == mgo.ErrNotFound { 224 | return specialerror.ErrRefreshTokenIsNotValid 225 | } 226 | return specialerror.ErrInternalServerError 227 | } 228 | //get the trusted app 229 | var trustedAppId bson.ObjectId 230 | for _, trustedApp := range user.TrustedApps { 231 | if trustedApp.RefreshToken == refreshTokenRequest.RefreshToken { 232 | trustedAppId = trustedApp.Id 233 | } 234 | } 235 | //generate the access token 236 | authResponse, err := generateAccessToken(session, ac.DBName, &user, bson.ObjectIdHex(refreshTokenRequest.AppId), trustedAppId, "", true, isWebClient); if err != nil { 237 | return err 238 | } 239 | //return the authentication response 240 | c.JSON(http.StatusOK, &authResponse) 241 | return nil 242 | } 243 | 244 | //update user model such as email , first, last, display name 245 | //TODO : should implement user change image profile, get uploaded image resize it and save it as user image profile 246 | func (uc UserController) UpdateUserProfile(c echo.Context) error { 247 | userId, ok := c.Get(USER_ID_KEY).(bson.ObjectId) 248 | if !ok { 249 | return specialerror.ErrInternalServerError 250 | } 251 | u := models.User{} 252 | //get some fields such as email, first,last,display name from payload 253 | if err := c.Bind(&u); err != nil { 254 | return err 255 | } 256 | //copy session db 257 | session := uc.Session.Copy() 258 | defer session.Close() 259 | //TODO : please NOTE should check is't new email for this user ? if yes ? send verification email to this email 260 | //check is email address unique or not 261 | count, err := session.DB(uc.DBName).C(USER_COLLECTION_NAME).Find(bson.M{"email": strings.ToLower(u.Email), "_id": bson.M{"$ne": userId}}).Count(); if err != nil { 262 | return specialerror.ErrInternalServerError 263 | } 264 | if count != 0 { 265 | return specialerror.ErrAlreadyHaveUserWithThisEmailAddress 266 | } 267 | userUpdateSet := bson.M{ 268 | "first_name": u.FirstName, 269 | "last_name": u.LastName, 270 | "display_name": u.DisplayName, 271 | "email": u.Email, 272 | "updated_at": time.Now(), 273 | } 274 | //update the user profile in one query 275 | if err := session.DB(uc.DBName).C(USER_COLLECTION_NAME).UpdateId(userId, bson.M{"$set": userUpdateSet}); err != nil { 276 | return specialerror.ErrInternalServerError 277 | } 278 | c.JSON(http.StatusOK, operationresult.SuccessfullyUpdated) 279 | return nil 280 | } 281 | 282 | //change password with authorized user NOT reset password 283 | //TODO : should implement reset password flow like send reset password link to user by email 284 | func (uc UserController) ChangeUserPassword(c echo.Context) error { 285 | chPasswordReqModel := models.ChangePasswordRequestModel{} 286 | if err := c.Bind(&chPasswordReqModel); err != nil { 287 | return err 288 | } 289 | //get user id from c 290 | userId, ok := c.Get(USER_ID_KEY).(bson.ObjectId) 291 | if !ok { 292 | return specialerror.ErrInternalServerError 293 | } 294 | //copy db session 295 | session := uc.Session.Copy() 296 | defer session.Close() 297 | //get user model from db to check password and update it 298 | u := models.User{} 299 | if err := session.DB(uc.DBName).C(USER_COLLECTION_NAME).FindId(userId).One(&u); err != nil { 300 | return specialerror.ErrInternalServerError 301 | } 302 | //check old password is valid or not 303 | if err := bcrypt.CompareHashAndPassword([]byte(u.HashedPassword), []byte(chPasswordReqModel.OldPassword)); err != nil { 304 | return specialerror.ErrNotValidCredentialInfo 305 | } 306 | //hash new password and save it 307 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(chPasswordReqModel.Password), bcrypt.DefaultCost) 308 | if err != nil { 309 | return specialerror.ErrInternalServerError 310 | } 311 | u.HashedPassword = string(hashedPassword) 312 | u.UpdatedAt = time.Now() 313 | if err := session.DB(uc.DBName).C(USER_COLLECTION_NAME).UpdateId(userId, &u); err != nil { 314 | return specialerror.ErrInternalServerError 315 | } 316 | //inform user the password successfully changed 317 | c.JSON(http.StatusOK, operationresult.PasswordSuccessfullyChanged) 318 | return nil 319 | } 320 | 321 | func generateAccessToken(s *mgo.Session, dbName string, u *models.User, clientId, trustedAppId bson.ObjectId, deviceModel string, isRefreshToken, isWebClient bool) (*models.AuthenticationResponse, error) { 322 | //define access token model 323 | accessToken := models.AccessToken{ 324 | Id: bson.NewObjectId(), 325 | UserId: u.Id, 326 | TrustedAppId: trustedAppId, 327 | } 328 | expireIn := time.Hour * time.Duration(util.GenerateRandomNumber(24, 72)) 329 | accessToken.ExpireAt = time.Now().Add(expireIn) 330 | //new JWT 331 | token := jwt.New(jwt.SigningMethodHS256) 332 | //set headers 333 | token.Header["type"] = "JWT" 334 | token.Claims["exp"] = accessToken.ExpireAt.Unix() 335 | token.Claims["uid"] = u.Id.Hex() 336 | token.Claims["roles"] = u.Roles 337 | token.Claims["name"] = u.DisplayName 338 | token.Claims["img"] = u.ImageFileName 339 | token.Claims["aid"] = trustedAppId.Hex() 340 | token.Claims["tid"] = accessToken.Id.Hex() 341 | sToken, err := token.SignedString([]byte(JWT_SIGNING_KEY_PHRASE)); if err != nil { 342 | return nil, specialerror.ErrInternalServerError 343 | } 344 | //assign access Token 345 | accessToken.Token = sToken 346 | //generate the refresh token 347 | refreshToken, err := util.GenerateNewRefreshToken(); if err != nil { 348 | return nil, specialerror.ErrInternalServerError 349 | } 350 | //save trusted app for this user and save the access token in database 351 | //check is web client 352 | if isRefreshToken { 353 | //so already have trusted app for this user 354 | foundIt := false 355 | for i, trustedApp := range u.TrustedApps { 356 | if trustedApp.Id == trustedAppId { 357 | foundIt = true 358 | u.TrustedApps[i].RefreshToken = refreshToken 359 | } 360 | } 361 | if !foundIt { 362 | return nil, specialerror.ErrInternalServerError 363 | } 364 | } else { 365 | if isWebClient { 366 | //search in trustedApps have already this webClient 367 | foundIt := false 368 | for i, trustedApp := range u.TrustedApps { 369 | if trustedApp.ClientId == clientId { 370 | foundIt = true 371 | u.TrustedApps[i].RefreshToken = refreshToken 372 | } 373 | } 374 | if !foundIt { 375 | //so should create new trusted app 376 | newTrustedAppForWebClient := models.TrustedApp{ 377 | Id: trustedAppId, 378 | ClientId: clientId, 379 | RefreshToken: refreshToken, 380 | GrantedAt: time.Now(), 381 | } 382 | //assign this trusted app for web client 383 | u.TrustedApps = append(u.TrustedApps, newTrustedAppForWebClient) 384 | } 385 | } else { 386 | //first check is already have any trusted app with this client and device model 387 | foundIt := false 388 | for i, trustedApp := range u.TrustedApps { 389 | if trustedApp.ClientId == clientId && trustedApp.DeviceModel == deviceModel { 390 | foundIt = true 391 | u.TrustedApps[i].RefreshToken = refreshToken 392 | } 393 | } 394 | if !foundIt { 395 | //create new trusted app 396 | newTrustedApp := models.TrustedApp{ 397 | Id: trustedAppId, 398 | ClientId: clientId, 399 | RefreshToken: refreshToken, 400 | DeviceModel: deviceModel, 401 | GrantedAt: time.Now(), 402 | } 403 | //assign this trusted app to user 404 | u.TrustedApps = append(u.TrustedApps, newTrustedApp) 405 | } 406 | } 407 | } 408 | var err1, err2 error 409 | waitGroup := sync.WaitGroup{} 410 | waitGroup.Add(2) 411 | go func() { 412 | defer waitGroup.Done() 413 | //copy the db session 414 | session1 := s.Copy() 415 | defer session1.Close() 416 | //now should save the user 417 | err1 = session1.DB(dbName).C(USER_COLLECTION_NAME).UpdateId(u.Id, &u) 418 | }() 419 | go func() { 420 | defer waitGroup.Done() 421 | //copy the db session 422 | session2 := s.Copy() 423 | defer session2.Close() 424 | //save access token to db 425 | err2 = session2.DB(dbName).C(ACCESS_TOKEN_COLLECTION_NAME).Insert(&accessToken) 426 | }() 427 | waitGroup.Wait() 428 | if err1 != nil || err2 != nil { 429 | return nil, specialerror.ErrInternalServerError 430 | } 431 | AuthResponse := models.AuthenticationResponse{ 432 | TokenType: BEARER_AUTHENTICATION_TYPE, 433 | AccessToken: accessToken.Token, 434 | ExpiresInMin: expireIn.Minutes(), 435 | RefreshToken: refreshToken, 436 | } 437 | return &AuthResponse, nil 438 | } 439 | -------------------------------------------------------------------------------- /controller/user/user_controller_test.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "testing" 7 | "bytes" 8 | "encoding/json" 9 | "net/http" 10 | 11 | "gopkg.in/mgo.v2" 12 | "gopkg.in/mgo.v2/bson" 13 | 14 | "github.com/labstack/echo" 15 | "github.com/labstack/echo/engine" 16 | "github.com/labstack/echo/test" 17 | "github.com/syndtr/goleveldb/leveldb/errors" 18 | "github.com/dgrijalva/jwt-go" 19 | 20 | "github.com/atahani/golang-rest-api-sample/models" 21 | "github.com/atahani/golang-rest-api-sample/util/specialerror" 22 | "github.com/atahani/golang-rest-api-sample/util/testhelper" 23 | "github.com/atahani/golang-rest-api-sample/controller/client" 24 | ) 25 | 26 | var testingProvider testhelper.TestingProvider 27 | var newAppIdStr string 28 | var authResponse models.AuthenticationResponse 29 | var userEmail, userPassword, newPassword string 30 | 31 | func TestSignUpNewUser(t *testing.T) { 32 | //assign the email and password 33 | userEmail = "ahmad.tahani@gmail.com" 34 | userPassword = "123456abcz" 35 | newPassword = "987654321mnbvcx" 36 | //get copy of db session 37 | session := testingProvider.Session.Copy() 38 | defer session.Close() 39 | userController := NewUserController(session, testhelper.DB_TEST_NAME) 40 | session.DB(testhelper.DB_TEST_NAME).C(USER_COLLECTION_NAME).DropCollection() 41 | //define different cases 42 | reqBodyInvalidAppId := models.SignUpRequest{ 43 | AppId: "123124", 44 | FirstName: "ahmad", 45 | LastName: "tahani", 46 | DisplayName: "Ahmad", 47 | Email: "ahmad.tahani@gmail.com", 48 | Password: "123456abc", 49 | } 50 | reqBodyInvalidAppIdJ, _ := json.Marshal(reqBodyInvalidAppId) 51 | reqBodyNotHaveSomeField := models.SignUpRequest{ 52 | AppId: newAppIdStr, 53 | FirstName: "ahmad", 54 | Email: "ahmad.tahani@gmail.com", 55 | Password: "123456abc", 56 | } 57 | reqBodyNotHaveSomeFieldJ, _ := json.Marshal(reqBodyNotHaveSomeField) 58 | reqBodyInvalidEmail := models.SignUpRequest{ 59 | AppId: newAppIdStr, 60 | FirstName: "ahmad", 61 | LastName: "tahani", 62 | DisplayName: "Ahmad", 63 | Email: "ahmad@tahani@@gmail.com", 64 | Password: userPassword, 65 | } 66 | reqBodyInvalidEmailJ, _ := json.Marshal(reqBodyInvalidEmail) 67 | reqBodyInvalidPassword := models.SignUpRequest{ 68 | AppId: newAppIdStr, 69 | FirstName: "ahmad", 70 | LastName: "tahani", 71 | DisplayName: "Ahmad", 72 | Email: userEmail, 73 | Password: "12", 74 | } 75 | reqBodyInvalidPasswordJ, _ := json.Marshal(reqBodyInvalidPassword) 76 | reqBodyValid := models.SignUpRequest{ 77 | AppId: newAppIdStr, 78 | FirstName: "ahmad", 79 | LastName: "tahani", 80 | DisplayName: "Ahmad", 81 | Email: userEmail, 82 | Password: userPassword, 83 | } 84 | reqBodyValidJ, _ := json.Marshal(reqBodyValid) 85 | path := "/auth/signup" 86 | method := echo.POST 87 | cases := []struct { 88 | req engine.Request 89 | res *test.ResponseRecorder 90 | expectedError error 91 | }{ 92 | { 93 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyInvalidAppIdJ)), 94 | res: test.NewResponseRecorder(), 95 | expectedError: specialerror.ErrNotValidClientInformation, 96 | }, 97 | { 98 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyNotHaveSomeFieldJ)), 99 | res: test.NewResponseRecorder(), 100 | expectedError: specialerror.ErrSomeFieldAreNotValid, 101 | }, 102 | { 103 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyInvalidEmailJ)), 104 | res: test.NewResponseRecorder(), 105 | expectedError: specialerror.ErrSomeFieldAreNotValid, 106 | }, 107 | { 108 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyInvalidPasswordJ)), 109 | res: test.NewResponseRecorder(), 110 | expectedError: specialerror.ErrSomeFieldAreNotValid, 111 | }, 112 | { 113 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyValidJ)), 114 | res: test.NewResponseRecorder(), 115 | expectedError: nil, 116 | }, 117 | } 118 | for _, c := range cases { 119 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 120 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 121 | if err := userController.SignUpNewUser(context); err != c.expectedError { 122 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 123 | } 124 | if c.expectedError == nil { 125 | //it's mean have successfully sign up in to system 126 | if err := json.NewDecoder(c.res.Body).Decode(&authResponse); err != nil { 127 | t.Error("can not get authentication response in sign up request !") 128 | } 129 | } 130 | } 131 | } 132 | 133 | func TestRefreshAccessToken(t *testing.T) { 134 | //get copy db session 135 | session := testingProvider.Session.Copy() 136 | defer session.Close() 137 | userController := NewUserController(session, testhelper.DB_TEST_NAME) 138 | //define different cases 139 | refreshToke1 := models.RefreshTokenRequest{ 140 | AppId: newAppIdStr, 141 | RefreshToken: authResponse.RefreshToken, 142 | } 143 | refreshToke1J, _ := json.Marshal(refreshToke1) 144 | refreshToken2 := models.RefreshTokenRequest{ 145 | AppId: "aksdflhjas8", 146 | RefreshToken: authResponse.RefreshToken, 147 | } 148 | refreshToken2J, _ := json.Marshal(refreshToken2) 149 | refreshToken3 := models.RefreshTokenRequest{ 150 | AppId: newAppIdStr, 151 | } 152 | refreshToken3J, _ := json.Marshal(refreshToken3) 153 | refreshToken4 := models.RefreshTokenRequest{ 154 | AppId: newAppIdStr, 155 | RefreshToken: "asdasfly89asfdj4hp", 156 | } 157 | refreshToken4J, _ := json.Marshal(refreshToken4) 158 | path := "/auth/refreshtoken" 159 | method := echo.POST 160 | cases := []struct { 161 | req engine.Request 162 | res *test.ResponseRecorder 163 | expectedError error 164 | }{ 165 | { 166 | req: test.NewRequest(method, path, bytes.NewReader(refreshToke1J)), 167 | res: test.NewResponseRecorder(), 168 | expectedError: nil, 169 | }, 170 | { 171 | req: test.NewRequest(method, path, bytes.NewReader(refreshToken2J)), 172 | res: test.NewResponseRecorder(), 173 | expectedError: specialerror.ErrNotValidClientInformation, 174 | }, 175 | { 176 | req: test.NewRequest(method, path, bytes.NewReader(refreshToken3J)), 177 | res: test.NewResponseRecorder(), 178 | expectedError: specialerror.ErrSomeFieldAreNotValid, 179 | }, 180 | { 181 | req: test.NewRequest(method, path, bytes.NewReader(refreshToken4J)), 182 | res: test.NewResponseRecorder(), 183 | expectedError: specialerror.ErrRefreshTokenIsNotValid, 184 | }, 185 | } 186 | for _, c := range cases { 187 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 188 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 189 | if err := userController.RefreshAccessToken(context); err != c.expectedError { 190 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 191 | } 192 | if c.expectedError == nil { 193 | //it's mean have successfully sign in to system 194 | if err := json.NewDecoder(c.res.Body).Decode(&authResponse); err != nil { 195 | t.Error("can not get authentication response when refresh token is not valid !") 196 | } 197 | } 198 | } 199 | } 200 | 201 | func TestSignIn(t *testing.T) { 202 | //get copy of db session 203 | session := testingProvider.Session.Copy() 204 | defer session.Close() 205 | userController := NewUserController(session, testhelper.DB_TEST_NAME) 206 | reqBodyInvalidAppId := models.SignInRequest{ 207 | AppId: "0981234", 208 | Email: userEmail, 209 | Password: userPassword, 210 | } 211 | reqBodyInvalidAppIdJ, _ := json.Marshal(reqBodyInvalidAppId) 212 | reqBodyInvalidEmailAddress := models.SignInRequest{ 213 | AppId: newAppIdStr, 214 | Email: "ahmad.tahani@@@.com", 215 | Password: userPassword, 216 | } 217 | reqBodyInvalidEmailAddressJ, _ := json.Marshal(reqBodyInvalidEmailAddress) 218 | reqBodyInvalidCredential := models.SignInRequest{ 219 | AppId: newAppIdStr, 220 | Email: userEmail, 221 | Password: "091823qwerlimasdop", 222 | } 223 | reqBodyInvalidCredentialJ, _ := json.Marshal(reqBodyInvalidCredential) 224 | reqBodyValidRequest := models.SignInRequest{ 225 | AppId: newAppIdStr, 226 | Email: userEmail, 227 | Password: userPassword, 228 | } 229 | reqBodyValidRequestJ, _ := json.Marshal(reqBodyValidRequest) 230 | path := "/auth/singin" 231 | method := echo.POST 232 | //define different cases 233 | cases := []struct { 234 | req engine.Request 235 | res *test.ResponseRecorder 236 | expectedError error 237 | }{ 238 | { 239 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyInvalidAppIdJ)), 240 | res: test.NewResponseRecorder(), 241 | expectedError: specialerror.ErrNotValidClientInformation, 242 | }, 243 | { 244 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyInvalidEmailAddressJ)), 245 | res: test.NewResponseRecorder(), 246 | expectedError: specialerror.ErrSomeFieldAreNotValid, 247 | }, 248 | { 249 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyInvalidCredentialJ)), 250 | res: test.NewResponseRecorder(), 251 | expectedError: specialerror.ErrNotValidCredentialInfo, 252 | }, 253 | { 254 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyValidRequestJ)), 255 | res: test.NewResponseRecorder(), 256 | expectedError: nil, 257 | }, 258 | } 259 | for _, c := range cases { 260 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 261 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 262 | if err := userController.SignIn(context); err != c.expectedError { 263 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 264 | } 265 | if c.expectedError == nil { 266 | //it's mean have successfully sign up in to system 267 | if err := json.NewDecoder(c.res.Body).Decode(&authResponse); err != nil { 268 | t.Error("can not get authentication response in sign in request !") 269 | } 270 | } 271 | } 272 | } 273 | 274 | func TestJWTAuthenticationMiddleware(t *testing.T) { 275 | //get copy of db session 276 | session := testingProvider.Session.Copy() 277 | defer session.Close() 278 | //define jwt as handler since we test middleware alone 279 | jwt := JWTAuthenticationMiddleware(session, testhelper.DB_TEST_NAME)(func(c echo.Context) error { 280 | return c.String(http.StatusOK, "test") 281 | }) 282 | //define different case 283 | //define different cases 284 | cases := []struct { 285 | req engine.Request 286 | res *test.ResponseRecorder 287 | accessToken string 288 | expectedError error 289 | }{ 290 | { 291 | req: test.NewRequest(echo.GET, "/", nil), 292 | res: test.NewResponseRecorder(), 293 | accessToken: fmt.Sprintf("%s %s", BEARER_AUTHENTICATION_TYPE, authResponse.AccessToken), 294 | expectedError: nil, 295 | }, 296 | { 297 | req: test.NewRequest(echo.GET, "/", nil), 298 | res: test.NewResponseRecorder(), 299 | accessToken: authResponse.AccessToken, 300 | expectedError: specialerror.ErrUnauthorized, 301 | }, 302 | { 303 | req: test.NewRequest(echo.GET, "/", nil), 304 | res: test.NewResponseRecorder(), 305 | accessToken: fmt.Sprintf("%s %s", BEARER_AUTHENTICATION_TYPE, "invalidaccessToken"), 306 | expectedError: specialerror.ErrUnauthorized, 307 | }, 308 | } 309 | for _, c := range cases { 310 | c.req.Header().Set(echo.HeaderAuthorization, c.accessToken) 311 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 312 | if err := jwt(context); err != c.expectedError { 313 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 314 | } 315 | } 316 | } 317 | 318 | func TestAuthorizeUserByRolesMiddleware(t *testing.T) { 319 | //get copy of db session 320 | session := testingProvider.Session.Copy() 321 | defer session.Close() 322 | //define authorize role as handler since we test middleware alone 323 | authorizeRole := AuthorizeUserByRolesMiddleware([]string{"admin", "user"})(func(c echo.Context) error { 324 | return c.String(http.StatusOK, "test") 325 | }) 326 | //define different cases 327 | cases := []struct { 328 | req engine.Request 329 | res *test.ResponseRecorder 330 | roles []string 331 | expectedError error 332 | }{ 333 | { 334 | req: test.NewRequest(echo.GET, "/", nil), 335 | res: test.NewResponseRecorder(), 336 | roles: []string{"admin", "user"}, 337 | expectedError: nil, 338 | }, 339 | { 340 | req: test.NewRequest(echo.GET, "/", nil), 341 | res: test.NewResponseRecorder(), 342 | roles: []string{"admin"}, 343 | expectedError: specialerror.ErrCanNotAccessToTheseResource, 344 | }, 345 | { 346 | req: test.NewRequest(echo.GET, "/", nil), 347 | res: test.NewResponseRecorder(), 348 | roles: []string{"user"}, 349 | expectedError: specialerror.ErrCanNotAccessToTheseResource, 350 | }, 351 | } 352 | for _, c := range cases { 353 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 354 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 355 | context.Set(ROLES_KEY, c.roles) 356 | if err := authorizeRole(context); err != c.expectedError { 357 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 358 | } 359 | } 360 | } 361 | 362 | func TestUpdateUserProfile(t *testing.T) { 363 | //copy session of db 364 | session := testingProvider.Session.Copy() 365 | defer session.Close() 366 | //get the user_id from JWT token 367 | to, err := jwt.Parse(authResponse.AccessToken, func(token *jwt.Token) (interface{}, error) { 368 | //always check the signing method 369 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 370 | return nil, errors.New("can't parse the access token !") 371 | } 372 | //return the key for validation 373 | return []byte(JWT_SIGNING_KEY_PHRASE), nil 374 | }) 375 | if err != nil || to == nil || !to.Valid { 376 | t.Errorf("the access token is not valid or can't validate the access token !") 377 | } 378 | userId, ok := to.Claims["uid"].(string) 379 | if !ok { 380 | t.Errorf("can't get user_id from claims !") 381 | } 382 | userController := NewUserController(session, testhelper.DB_TEST_NAME) 383 | //define different cases 384 | reqBodyValid := models.User{ 385 | FirstName: "ahmad :)", 386 | LastName: "tahani new ", 387 | DisplayName: ":) :)", 388 | Email: userEmail, 389 | } 390 | reqBodyValidJ, _ := json.Marshal(reqBodyValid) 391 | reqBodyInvalidEmailAddress := models.User{ 392 | FirstName: "ahmad", 393 | LastName: "tahani", 394 | DisplayName: "Ahmad", 395 | Email: "me@@@gmail.com", 396 | } 397 | reqBodyInvalidEmailAddressJ, _ := json.Marshal(reqBodyInvalidEmailAddress) 398 | reqBodyInvalidFields := models.User{ 399 | FirstName: "ahmad", 400 | LastName: "tahani", 401 | //don't have displayName and email address 402 | } 403 | reqBodyInvalidFieldsJ, _ := json.Marshal(reqBodyInvalidFields) 404 | path := "/api/user/profile" 405 | method := echo.PUT 406 | cases := []struct { 407 | req engine.Request 408 | res *test.ResponseRecorder 409 | expectedError error 410 | }{ 411 | { 412 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyValidJ)), 413 | res: test.NewResponseRecorder(), 414 | expectedError: nil, 415 | }, 416 | { 417 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyInvalidEmailAddressJ)), 418 | res: test.NewResponseRecorder(), 419 | expectedError: specialerror.ErrSomeFieldAreNotValid, 420 | }, 421 | { 422 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyInvalidFieldsJ)), 423 | res: test.NewResponseRecorder(), 424 | expectedError: specialerror.ErrSomeFieldAreNotValid, 425 | }, 426 | } 427 | for _, c := range cases { 428 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 429 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 430 | //set the user_id for context 431 | context.Set(USER_ID_KEY, bson.ObjectIdHex(userId)) 432 | if err := userController.UpdateUserProfile(context); err != c.expectedError { 433 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 434 | } 435 | } 436 | } 437 | 438 | func TestChangeUserPassword(t *testing.T) { 439 | //copy db session 440 | session := testingProvider.Session.Copy() 441 | defer session.Close() 442 | //get the user_id from JWT token 443 | to, err := jwt.Parse(authResponse.AccessToken, func(token *jwt.Token) (interface{}, error) { 444 | //always check the signing method 445 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 446 | return nil, errors.New("can't parse the access token !") 447 | } 448 | //return the key for validation 449 | return []byte(JWT_SIGNING_KEY_PHRASE), nil 450 | }) 451 | if err != nil || to == nil || !to.Valid { 452 | t.Errorf("the access token is not valid or can't validate the access token !") 453 | } 454 | userId, ok := to.Claims["uid"].(string) 455 | if !ok { 456 | t.Errorf("can't get user_id from claims !") 457 | } 458 | userController := NewUserController(session, testhelper.DB_TEST_NAME) 459 | //define different cases 460 | reqBodyValid := models.ChangePasswordRequestModel{ 461 | OldPassword: userPassword, 462 | Password: newPassword, 463 | } 464 | reqBodyValidJ, _ := json.Marshal(reqBodyValid) 465 | reqBodyInvalidOldPassword := models.ChangePasswordRequestModel{ 466 | OldPassword: "123098123klqwerlku89023", 467 | Password: newPassword, 468 | } 469 | reqBodyInvalidOldPasswordJ, _ := json.Marshal(reqBodyInvalidOldPassword) 470 | reqBodyInvalidPassword := models.ChangePasswordRequestModel{ 471 | OldPassword: userPassword, 472 | Password: "", 473 | } 474 | reqBodyInvalidPasswordJ, _ := json.Marshal(reqBodyInvalidPassword) 475 | path := "/api/user/password" 476 | method := echo.POST 477 | cases := []struct { 478 | req engine.Request 479 | res *test.ResponseRecorder 480 | expectedError error 481 | }{ 482 | { 483 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyValidJ)), 484 | res: test.NewResponseRecorder(), 485 | expectedError: nil, 486 | }, 487 | { 488 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyInvalidOldPasswordJ)), 489 | res: test.NewResponseRecorder(), 490 | expectedError: specialerror.ErrNotValidCredentialInfo, 491 | }, 492 | { 493 | req: test.NewRequest(method, path, bytes.NewReader(reqBodyInvalidPasswordJ)), 494 | res: test.NewResponseRecorder(), 495 | expectedError: specialerror.ErrSomeFieldAreNotValid, 496 | }, 497 | } 498 | for _, c := range cases { 499 | c.req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 500 | context := echo.NewContext(c.req, c.res, testingProvider.Echo) 501 | //set the user_id for context 502 | context.Set(USER_ID_KEY, bson.ObjectIdHex(userId)) 503 | if err := userController.ChangeUserPassword(context); err != c.expectedError { 504 | t.Errorf("Error should %q \t but get %q", c.expectedError, err) 505 | } 506 | } 507 | } 508 | 509 | //create new client in db just for test 510 | func createNewClientInDB(s *mgo.Session, e *echo.Echo) (*models.Client, error) { 511 | //copy db session 512 | session := s.Copy() 513 | defer session.Close() 514 | //add new client via function inside the manage_by_admin.go 515 | clientController := client.NewClientController(session, testhelper.DB_TEST_NAME) 516 | req := test.NewRequest(echo.POST, "/api/manage/client", bytes.NewBuffer([]byte(`{"name":"new client just for test"}`))) 517 | req.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) 518 | res := test.NewResponseRecorder() 519 | context := echo.NewContext(req, res, e) 520 | if err := clientController.CreateNewClient(context); err != nil { 521 | return nil, err 522 | } 523 | //decode the body 524 | client := models.Client{} 525 | if err := json.NewDecoder(res.Body).Decode(&client); err != nil { 526 | return nil, err 527 | } 528 | //return new client information 529 | return &client, nil 530 | } 531 | 532 | func TestMain(m *testing.M) { 533 | //start of testing 534 | testingProvider = testhelper.TestingProvider{} 535 | testingProvider.StartTesting() 536 | //create the new client to signUp 537 | if cli, err := createNewClientInDB(testingProvider.Session, testingProvider.Echo); err != nil { 538 | fmt.Println("error happend in creating client !\n%s", err) 539 | } else { 540 | newAppIdStr = string(cli.AppId.Hex()) 541 | } 542 | ret := m.Run() 543 | os.Exit(ret) 544 | } 545 | --------------------------------------------------------------------------------