1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-23 00:37:48 +02:00

fixed internal package unittests

This commit is contained in:
Lee Brown
2019-06-24 22:41:21 -08:00
parent ca8670eadf
commit 8994ee4d1a
33 changed files with 1285 additions and 343 deletions

View File

@ -41,7 +41,7 @@ func main() {
Database string `default:"shared" envconfig:"DATABASE"`
Driver string `default:"postgres" envconfig:"DRIVER"`
Timezone string `default:"utc" envconfig:"TIMEZONE"`
DisableTLS bool `default:"false" envconfig:"DISABLE_TLS"`
DisableTLS bool `default:"true" envconfig:"DISABLE_TLS"`
}
}

View File

@ -24,3 +24,53 @@ To build using the docker file, need to be in the project root directory. `Docke
```bash
docker build -f cmd/web-api/Dockerfile -t saas-web-api .
```
## API Documentation
Documentation is generated using [swag](https://github.com/swaggo/swag)
Download swag by using:
```bash
go get -u github.com/swaggo/swag/cmd/swag
```
Run `swag init` in the service's root folder which contains the main.go file. This will parse your comments and generate the required files (docs folder and docs/docs.go).
```bash
swag init
```
### Trouble shooting
If you run into errors running `swag init` try the following:
#### cannot find package
Try to install the packages to your $GOPATH.
```bash
GO111MODULE=off go get github.com/leodido/go-urn
GO111MODULE=off go get github.com/lib/pq/oid
GO111MODULE=off go get github.com/lib/pq/scram
GO111MODULE=off go get github.com/tinylib/msgp/msgp
GO111MODULE=off go get gopkg.in/DataDog/dd-trace-go.v1/ddtrace
```
#### error writing go.mod
Need to update pkg directory permissions.
Full error:
```bash
error writing go.mod: open /Users/leebrown/go/pkg/mod/github.com/lib/pq@v1.1.1/go.mod691440060.tmp: permission denied
```
Ensure the `pkg` directory used for go module cache has the correct permissions.
```bash
sudo chown -R $(whoami):staff ${HOME}/go/pkg
sudo chmod -R 755 ${HOME}/go/pkg
```

View File

@ -1,6 +1,6 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag at
// 2019-06-24 15:42:25.999684 -0800 AKDT m=+0.030714022
// 2019-06-24 20:15:37.524606 -0800 AKDT m=+13.872100491
package docs
@ -16,10 +16,10 @@ var doc = `{
"info": {
"description": "This is a sample server celler server.",
"title": "SaaS Example API",
"termsOfService": "http://geeksinthewoods.com/terms",
"termsOfService": "/terms",
"contact": {
"name": "API Support",
"url": "https://gitlab.com/geeks-accelerator/oss/saas-starter-kit",
"url": "/support",
"email": "support@geeksinthewoods.com"
},
"license": {
@ -30,20 +30,140 @@ var doc = `{
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {},
"paths": {
"/accounts/{id}": {
"get": {
"description": "get string by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Read returns the specified account from the system.",
"operationId": "get-string-by-int",
"parameters": [
{
"type": "integer",
"description": "Account ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/account.Account"
},
"headers": {
"Token": {
"type": "string",
"description": "qwerty"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
}
}
}
}
},
"definitions": {
"account.Account": {
"type": "object",
"properties": {
"address1": {
"type": "string"
},
"address2": {
"type": "string"
},
"archived_at": {
"type": "string"
},
"billing_user_id": {
"type": "string"
},
"city": {
"type": "string"
},
"country": {
"type": "string"
},
"created_at": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"region": {
"type": "string"
},
"signup_user_id": {
"type": "string"
},
"status": {
"type": "AccountStatus"
},
"timezone": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"zipcode": {
"type": "string"
}
}
},
"web.Error": {
"type": "object",
"properties": {
"err": {
"type": "error"
},
"fields": {
"type": "array",
"items": {
"type": "FieldError"
}
},
"status": {
"type": "integer"
}
}
}
},
"securityDefinitions": {
"ApiKeyAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
},
"BasicAuth": {
"type": "basic"
},
"OAuth2Password": {
"type": "oauth2",
"flow": "password",
"tokenUrl": "https://example.com/v1/oauth/token"
"tokenUrl": "/v1/oauth/token"
}
}
}`

View File

@ -3,10 +3,10 @@
"info": {
"description": "This is a sample server celler server.",
"title": "SaaS Example API",
"termsOfService": "http://geeksinthewoods.com/terms",
"termsOfService": "/terms",
"contact": {
"name": "API Support",
"url": "https://gitlab.com/geeks-accelerator/oss/saas-starter-kit",
"url": "/support",
"email": "support@geeksinthewoods.com"
},
"license": {
@ -17,20 +17,140 @@
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {},
"paths": {
"/accounts/{id}": {
"get": {
"description": "get string by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Read returns the specified account from the system.",
"operationId": "get-string-by-int",
"parameters": [
{
"type": "integer",
"description": "Account ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/account.Account"
},
"headers": {
"Token": {
"type": "string",
"description": "qwerty"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
}
}
}
}
},
"definitions": {
"account.Account": {
"type": "object",
"properties": {
"address1": {
"type": "string"
},
"address2": {
"type": "string"
},
"archived_at": {
"type": "string"
},
"billing_user_id": {
"type": "string"
},
"city": {
"type": "string"
},
"country": {
"type": "string"
},
"created_at": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"region": {
"type": "string"
},
"signup_user_id": {
"type": "string"
},
"status": {
"type": "AccountStatus"
},
"timezone": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"zipcode": {
"type": "string"
}
}
},
"web.Error": {
"type": "object",
"properties": {
"err": {
"type": "error"
},
"fields": {
"type": "array",
"items": {
"type": "FieldError"
}
},
"status": {
"type": "integer"
}
}
}
},
"securityDefinitions": {
"ApiKeyAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
},
"BasicAuth": {
"type": "basic"
},
"OAuth2Password": {
"type": "oauth2",
"flow": "password",
"tokenUrl": "https://example.com/v1/oauth/token"
"tokenUrl": "/v1/oauth/token"
}
}
}

View File

@ -1,27 +1,106 @@
basePath: '{{.BasePath}}'
definitions:
account.Account:
properties:
address1:
type: string
address2:
type: string
archived_at:
type: string
billing_user_id:
type: string
city:
type: string
country:
type: string
created_at:
type: string
id:
type: string
name:
type: string
region:
type: string
signup_user_id:
type: string
status:
type: AccountStatus
timezone:
type: string
updated_at:
type: string
zipcode:
type: string
type: object
web.Error:
properties:
err:
type: error
fields:
items:
type: FieldError
type: array
status:
type: integer
type: object
host: '{{.Host}}'
info:
contact:
email: support@geeksinthewoods.com
name: API Support
url: https://gitlab.com/geeks-accelerator/oss/saas-starter-kit
url: /support
description: This is a sample server celler server.
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
termsOfService: http://geeksinthewoods.com/terms
termsOfService: /terms
title: SaaS Example API
version: '{{.Version}}'
paths: {}
paths:
/accounts/{id}:
get:
consumes:
- application/json
description: get string by ID
operationId: get-string-by-int
parameters:
- description: Account ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
headers:
Token:
description: qwerty
type: string
schema:
$ref: '#/definitions/account.Account'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/web.Error'
type: object
"403":
description: Forbidden
schema:
$ref: '#/definitions/web.Error'
type: object
"404":
description: Not Found
schema:
$ref: '#/definitions/web.Error'
type: object
summary: Read returns the specified account from the system.
securityDefinitions:
ApiKeyAuth:
in: header
name: Authorization
type: apiKey
BasicAuth:
type: basic
OAuth2Password:
flow: password
tokenUrl: https://example.com/v1/oauth/token
tokenUrl: /v1/oauth/token
type: oauth2
swagger: "2.0"

View File

@ -39,7 +39,19 @@ func (a *Account) Find(ctx context.Context, w http.ResponseWriter, r *http.Reque
return web.RespondJson(ctx, w, res, http.StatusOK)
}
// Read returns the specified account from the system.
// Read godoc
// @Summary Read returns the specified account from the system.
// @Description get string by ID
// @ID get-string-by-int
// @Accept json
// @Produce json
// @Param id path int true "Account ID"
// @Success 200 {object} account.Account
// @Header 200 {string} Token "qwerty"
// @Failure 400 {object} web.Error
// @Failure 403 {object} web.Error
// @Failure 404 {object} web.Error
// @Router /accounts/{id} [get]
func (a *Account) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {

View File

@ -40,7 +40,7 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red
app.Handle("PATCH", "/v1/users/switch-account/:accountId", u.SwitchAccount, mid.Authenticate(authenticator))
// This route is not authenticated
app.Handle("GET", "/v1/oauth/token", u.Token)
app.Handle("POST", "/v1/oauth/token", u.Token)
// Register account endpoints.
a := Account{
@ -65,8 +65,10 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red
app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
// Register swagger documentation.
app.Handle("GET", "/swagger/", saasSwagger.WrapHandler, mid.Authenticate(authenticator))
app.Handle("GET", "/swagger/*", saasSwagger.WrapHandler, mid.Authenticate(authenticator))
// TODO: Add authentication. Current authenticator requires an Authorization header
// which breaks the browser experience.
app.Handle("GET", "/swagger/", saasSwagger.WrapHandler)
app.Handle("GET", "/swagger/*", saasSwagger.WrapHandler)
return app
}

View File

@ -42,23 +42,17 @@ var service = "WEB_API"
// @title SaaS Example API
// @description This is a sample server celler server.
// @termsOfService http://geeksinthewoods.com/terms
// @termsOfService http://example.com/terms
// @contact.name API Support
// @contact.email support@geeksinthewoods.com
// @contact.url https://gitlab.com/geeks-accelerator/oss/saas-starter-kit
// @contact.url http://example.com/support
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @securityDefinitions.basic BasicAuth
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
// @securitydefinitions.oauth2.password OAuth2Password
// @tokenUrl https://example.com/v1/oauth/token
// @tokenUrl /v1/oauth/token
func main() {
@ -101,7 +95,7 @@ func main() {
Database string `default:"shared" envconfig:"DATABASE"`
Driver string `default:"postgres" envconfig:"DRIVER"`
Timezone string `default:"utc" envconfig:"TIMEZONE"`
DisableTLS bool `default:"false" envconfig:"DISABLE_TLS"`
DisableTLS bool `default:"true" envconfig:"DISABLE_TLS"`
}
Trace struct {
Host string `default:"127.0.0.1" envconfig:"DD_TRACE_AGENT_HOSTNAME"`

View File

@ -0,0 +1,4 @@
export WEB_API_DB_HOST=127.0.0.1:5433
export WEB_API_DB_USER=postgres
export WEB_API_DB_PASS=postgres
export WEB_API_DB_DISABLE_TLS=true

View File

@ -94,7 +94,7 @@ func main() {
Database string `default:"shared" envconfig:"DATABASE"`
Driver string `default:"postgres" envconfig:"DRIVER"`
Timezone string `default:"utc" envconfig:"TIMEZONE"`
DisableTLS bool `default:"false" envconfig:"DISABLE_TLS"`
DisableTLS bool `default:"true" envconfig:"DISABLE_TLS"`
}
Trace struct {
Host string `default:"127.0.0.1" envconfig:"DD_TRACE_AGENT_HOSTNAME"`

View File

@ -0,0 +1,4 @@
export WEB_APP_DB_HOST=127.0.0.1:5433
export WEB_APP_DB_USER=postgres
export WEB_APP_DB_PASS=postgres
export WEB_APP_DB_DISABLE_TLS=true

View File

@ -13,14 +13,14 @@ require (
github.com/go-playground/locales v0.12.1
github.com/go-playground/universal-translator v0.16.0
github.com/go-redis/redis v6.15.2+incompatible
github.com/google/go-cmp v0.2.0
github.com/google/go-cmp v0.3.0
github.com/gorilla/schema v1.1.0
github.com/huandu/go-sqlbuilder v1.4.0
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365
github.com/jmoiron/sqlx v1.2.0
github.com/kelseyhightower/envconfig v1.3.0
github.com/leodido/go-urn v1.1.0 // indirect
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01
github.com/leodido/go-urn v1.1.0
github.com/lib/pq v1.1.1
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/onsi/ginkgo v1.8.0 // indirect
@ -39,9 +39,9 @@ require (
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 // indirect
golang.org/x/tools v0.0.0-20190624190245-7f2218787638 // indirect
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4 // indirect
google.golang.org/appengine v1.6.0 // indirect
gopkg.in/DataDog/dd-trace-go.v1 v1.14.0
gopkg.in/DataDog/dd-trace-go.v1 v1.15.0
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v9 v9.29.0
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce

View File

@ -53,6 +53,8 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
@ -79,6 +81,8 @@ github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01 h1:EPw7R3OAyxHBCyl0oqh3lUZqS5lu3KSxzzGasE0opXQ=
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -153,11 +157,15 @@ golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624190245-7f2218787638 h1:uIfBkD8gLczr4XDgYpt/qJYds2YJwZRNw4zs7wSnNhk=
golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4 h1:1mMox4TgefDwqluYCv677yNXwlfTkija4owZve/jr78=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw=
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/DataDog/dd-trace-go.v1 v1.14.0 h1:p/8j8WV6HC+6c99FMWIPrPPs+PiXU/ShrBxHbO8S8V0=
gopkg.in/DataDog/dd-trace-go.v1 v1.14.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
gopkg.in/DataDog/dd-trace-go.v1 v1.15.0 h1:2LhklnAJsRSelbnBrrE5QuRleRDkmOh2JWxOtIX6yec=
gopkg.in/DataDog/dd-trace-go.v1 v1.15.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -232,7 +232,7 @@ func find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, query *sqlbu
}
// Validation an name is unique excluding the current account ID.
func uniqueName(ctx context.Context, dbConn *sqlx.DB, name, accountId string) (bool, error) {
func UniqueName(ctx context.Context, dbConn *sqlx.DB, name, accountId string) (bool, error) {
query := sqlbuilder.NewSelectBuilder().Select("id").From(accountTableName)
query.Where(query.And(
query.Equal("name", name),
@ -264,7 +264,7 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Accoun
v := validator.New()
// Validation email address is unique in the database.
uniq, err := uniqueName(ctx, dbConn, req.Name, "")
uniq, err := UniqueName(ctx, dbConn, req.Name, "")
if err != nil {
return nil, err
}
@ -372,7 +372,7 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Accoun
// Validation name is unique in the database.
if req.Name != nil {
uniq, err := uniqueName(ctx, dbConn, *req.Name, req.ID)
uniq, err := UniqueName(ctx, dbConn, *req.Name, req.ID)
if err != nil {
return err
}
@ -583,22 +583,14 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, accountID
return err
}
// Build the delete SQL statement.
query := sqlbuilder.NewDeleteBuilder()
query.DeleteFrom(accountTableName)
query.Where(query.Equal("id", req.ID))
// Execute the query with the provided context.
sql, args := query.Build()
sql = dbConn.Rebind(sql)
_, err = dbConn.ExecContext(ctx, sql, args...)
// Start a new transaction to handle rollbacks on error.
tx, err := dbConn.Begin()
if err != nil {
err = errors.Wrapf(err, "query - %s", query.String())
err = errors.WithMessagef(err, "delete account %s failed", req.ID)
return err
return errors.WithStack(err)
}
// Delete all the associated user accounts
// Delete all the associated user accounts.
// Required to execute first to avoid foreign key constraints.
{
// Build the delete SQL statement.
query := sqlbuilder.NewDeleteBuilder()
@ -610,13 +602,54 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, accountID
// Execute the query with the provided context.
sql, args := query.Build()
sql = dbConn.Rebind(sql)
_, err = dbConn.ExecContext(ctx, sql, args...)
_, err = tx.ExecContext(ctx, sql, args...)
if err != nil {
tx.Rollback()
err = errors.Wrapf(err, "query - %s", query.String())
err = errors.WithMessagef(err, "delete users for account %s failed", req.ID)
return err
}
}
// Build the delete SQL statement.
query := sqlbuilder.NewDeleteBuilder()
query.DeleteFrom(accountTableName)
query.Where(query.Equal("id", req.ID))
// Execute the query with the provided context.
sql, args := query.Build()
sql = dbConn.Rebind(sql)
_, err = tx.ExecContext(ctx, sql, args...)
if err != nil {
tx.Rollback()
err = errors.Wrapf(err, "query - %s", query.String())
err = errors.WithMessagef(err, "delete account %s failed", req.ID)
return err
}
err = tx.Commit()
if err != nil {
return errors.WithStack(err)
}
return nil
}
// MockAccount returns a fake Account for testing.
func MockAccount(ctx context.Context, dbConn *sqlx.DB, now time.Time) (*Account, error) {
s := AccountStatus_Active
req := AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
City: "Valdez",
Region: "AK",
Country: "USA",
Zipcode: "99686",
Status: &s,
}
return Create(ctx, auth.Claims{}, dbConn, req, now)
}

View File

@ -1,13 +1,13 @@
package account
import (
"github.com/lib/pq"
"math/rand"
"os"
"strings"
"testing"
"time"
"github.com/lib/pq"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
"github.com/dgrijalva/jwt-go"
@ -142,25 +142,25 @@ func TestCreateValidation(t *testing.T) {
var accountTests = []struct {
name string
req CreateAccountRequest
expected func(req CreateAccountRequest, res *Account) *Account
req AccountCreateRequest
expected func(req AccountCreateRequest, res *Account) *Account
error error
}{
{"Required Fields",
CreateAccountRequest{},
func(req CreateAccountRequest, res *Account) *Account {
AccountCreateRequest{},
func(req AccountCreateRequest, res *Account) *Account {
return nil
},
errors.New("Key: 'CreateAccountRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" +
"Key: 'CreateAccountRequest.Address1' Error:Field validation for 'Address1' failed on the 'required' tag\n" +
"Key: 'CreateAccountRequest.City' Error:Field validation for 'City' failed on the 'required' tag\n" +
"Key: 'CreateAccountRequest.Region' Error:Field validation for 'Region' failed on the 'required' tag\n" +
"Key: 'CreateAccountRequest.Country' Error:Field validation for 'Country' failed on the 'required' tag\n" +
"Key: 'CreateAccountRequest.Zipcode' Error:Field validation for 'Zipcode' failed on the 'required' tag"),
errors.New("Key: 'AccountCreateRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" +
"Key: 'AccountCreateRequest.Address1' Error:Field validation for 'Address1' failed on the 'required' tag\n" +
"Key: 'AccountCreateRequest.City' Error:Field validation for 'City' failed on the 'required' tag\n" +
"Key: 'AccountCreateRequest.Region' Error:Field validation for 'Region' failed on the 'required' tag\n" +
"Key: 'AccountCreateRequest.Country' Error:Field validation for 'Country' failed on the 'required' tag\n" +
"Key: 'AccountCreateRequest.Zipcode' Error:Field validation for 'Zipcode' failed on the 'required' tag"),
},
{"Default Timezone & Status",
CreateAccountRequest{
AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -169,7 +169,7 @@ func TestCreateValidation(t *testing.T) {
Country: "USA",
Zipcode: "99686",
},
func(req CreateAccountRequest, res *Account) *Account {
func(req AccountCreateRequest, res *Account) *Account {
return &Account{
Name: req.Name,
Address1: req.Address1,
@ -191,7 +191,7 @@ func TestCreateValidation(t *testing.T) {
nil,
},
{"Valid Status",
CreateAccountRequest{
AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -201,10 +201,10 @@ func TestCreateValidation(t *testing.T) {
Zipcode: "99686",
Status: &invalidStatus,
},
func(req CreateAccountRequest, res *Account) *Account {
func(req AccountCreateRequest, res *Account) *Account {
return nil
},
errors.New("Key: 'CreateAccountRequest.Status' Error:Field validation for 'Status' failed on the 'oneof' tag"),
errors.New("Key: 'AccountCreateRequest.Status' Error:Field validation for 'Status' failed on the 'oneof' tag"),
},
}
@ -262,7 +262,7 @@ func TestCreateValidationNameUnique(t *testing.T) {
{
ctx := tests.Context()
req1 := CreateAccountRequest{
req1 := AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -277,7 +277,7 @@ func TestCreateValidationNameUnique(t *testing.T) {
t.Fatalf("\t%s\tCreate failed.", tests.Failed)
}
req2 := CreateAccountRequest{
req2 := AccountCreateRequest{
Name: account1.Name,
Address1: "103 East Main St",
Address2: "Unit 546",
@ -286,7 +286,7 @@ func TestCreateValidationNameUnique(t *testing.T) {
Country: "USA",
Zipcode: "99686",
}
expectedErr := errors.New("Key: 'CreateAccountRequest.Name' Error:Field validation for 'Name' failed on the 'unique' tag")
expectedErr := errors.New("Key: 'AccountCreateRequest.Name' Error:Field validation for 'Name' failed on the 'unique' tag")
_, err = Create(ctx, auth.Claims{}, test.MasterDB, req2, now)
if err == nil {
t.Logf("\t\tWant: %+v", expectedErr)
@ -310,13 +310,13 @@ func TestCreateClaims(t *testing.T) {
var accountTests = []struct {
name string
claims auth.Claims
req CreateAccountRequest
req AccountCreateRequest
error error
}{
// Internal request, should bypass ACL.
{"EmptyClaims",
auth.Claims{},
CreateAccountRequest{
AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -336,7 +336,7 @@ func TestCreateClaims(t *testing.T) {
Audience: "acc1",
},
},
CreateAccountRequest{
AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -356,7 +356,7 @@ func TestCreateClaims(t *testing.T) {
Audience: "acc1",
},
},
CreateAccountRequest{
AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -396,24 +396,24 @@ func TestUpdateValidation(t *testing.T) {
// TODO: actually create the account so can test the output of findbyId
type accountTest struct {
name string
req UpdateAccountRequest
req AccountUpdateRequest
error error
}
var accountTests = []accountTest{
{"Required Fields",
UpdateAccountRequest{},
errors.New("Key: 'UpdateAccountRequest.ID' Error:Field validation for 'ID' failed on the 'required' tag"),
AccountUpdateRequest{},
errors.New("Key: 'AccountUpdateRequest.ID' Error:Field validation for 'ID' failed on the 'required' tag"),
},
}
invalidStatus := AccountStatus("xxxxxx")
accountTests = append(accountTests, accountTest{"Valid Status",
UpdateAccountRequest{
AccountUpdateRequest{
ID: uuid.NewRandom().String(),
Status: &invalidStatus,
},
errors.New("Key: 'UpdateAccountRequest.Status' Error:Field validation for 'Status' failed on the 'oneof' tag"),
errors.New("Key: 'AccountUpdateRequest.Status' Error:Field validation for 'Status' failed on the 'oneof' tag"),
})
now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)
@ -459,7 +459,7 @@ func TestUpdateValidationNameUnique(t *testing.T) {
{
ctx := tests.Context()
req1 := CreateAccountRequest{
req1 := AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -474,7 +474,7 @@ func TestUpdateValidationNameUnique(t *testing.T) {
t.Fatalf("\t%s\tCreate failed.", tests.Failed)
}
req2 := CreateAccountRequest{
req2 := AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -490,11 +490,11 @@ func TestUpdateValidationNameUnique(t *testing.T) {
}
// Try to set the email for account 1 on account 2
updateReq := UpdateAccountRequest{
updateReq := AccountUpdateRequest{
ID: account2.ID,
Name: &account1.Name,
}
expectedErr := errors.New("Key: 'UpdateAccountRequest.Name' Error:Field validation for 'Name' failed on the 'unique' tag")
expectedErr := errors.New("Key: 'AccountUpdateRequest.Name' Error:Field validation for 'Name' failed on the 'unique' tag")
err = Update(ctx, auth.Claims{}, test.MasterDB, updateReq, now)
if err == nil {
t.Logf("\t\tWant: %+v", expectedErr)
@ -518,10 +518,10 @@ func TestCrud(t *testing.T) {
type accountTest struct {
name string
claims func(*Account, string) auth.Claims
create CreateAccountRequest
update func(*Account) UpdateAccountRequest
create AccountCreateRequest
update func(*Account) AccountUpdateRequest
updateErr error
expected func(*Account, UpdateAccountRequest) *Account
expected func(*Account, AccountUpdateRequest) *Account
findErr error
}
@ -532,7 +532,7 @@ func TestCrud(t *testing.T) {
func(account *Account, userId string) auth.Claims {
return auth.Claims{}
},
CreateAccountRequest{
AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -541,15 +541,15 @@ func TestCrud(t *testing.T) {
Country: "USA",
Zipcode: "99686",
},
func(account *Account) UpdateAccountRequest {
func(account *Account) AccountUpdateRequest {
name := uuid.NewRandom().String()
return UpdateAccountRequest{
return AccountUpdateRequest{
ID: account.ID,
Name: &name,
}
},
nil,
func(account *Account, req UpdateAccountRequest) *Account {
func(account *Account, req AccountUpdateRequest) *Account {
return &Account{
Name: *req.Name,
// Copy this fields from the created account.
@ -583,7 +583,7 @@ func TestCrud(t *testing.T) {
},
}
},
CreateAccountRequest{
AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -592,15 +592,15 @@ func TestCrud(t *testing.T) {
Country: "USA",
Zipcode: "99686",
},
func(account *Account) UpdateAccountRequest {
func(account *Account) AccountUpdateRequest {
name := uuid.NewRandom().String()
return UpdateAccountRequest{
return AccountUpdateRequest{
ID: account.ID,
Name: &name,
}
},
ErrForbidden,
func(account *Account, req UpdateAccountRequest) *Account {
func(account *Account, req AccountUpdateRequest) *Account {
return account
},
ErrNotFound,
@ -617,7 +617,7 @@ func TestCrud(t *testing.T) {
},
}
},
CreateAccountRequest{
AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -626,15 +626,15 @@ func TestCrud(t *testing.T) {
Country: "USA",
Zipcode: "99686",
},
func(account *Account) UpdateAccountRequest {
func(account *Account) AccountUpdateRequest {
name := uuid.NewRandom().String()
return UpdateAccountRequest{
return AccountUpdateRequest{
ID: account.ID,
Name: &name,
}
},
nil,
func(account *Account, req UpdateAccountRequest) *Account {
func(account *Account, req AccountUpdateRequest) *Account {
return &Account{
Name: *req.Name,
// Copy this fields from the created account.
@ -668,7 +668,7 @@ func TestCrud(t *testing.T) {
},
}
},
CreateAccountRequest{
AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -677,15 +677,15 @@ func TestCrud(t *testing.T) {
Country: "USA",
Zipcode: "99686",
},
func(account *Account) UpdateAccountRequest {
func(account *Account) AccountUpdateRequest {
name := uuid.NewRandom().String()
return UpdateAccountRequest{
return AccountUpdateRequest{
ID: account.ID,
Name: &name,
}
},
ErrForbidden,
func(account *Account, req UpdateAccountRequest) *Account {
func(account *Account, req AccountUpdateRequest) *Account {
return nil
},
ErrNotFound,
@ -702,7 +702,7 @@ func TestCrud(t *testing.T) {
},
}
},
CreateAccountRequest{
AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -711,15 +711,15 @@ func TestCrud(t *testing.T) {
Country: "USA",
Zipcode: "99686",
},
func(account *Account) UpdateAccountRequest {
func(account *Account) AccountUpdateRequest {
name := uuid.NewRandom().String()
return UpdateAccountRequest{
return AccountUpdateRequest{
ID: account.ID,
Name: &name,
}
},
nil,
func(account *Account, req UpdateAccountRequest) *Account {
func(account *Account, req AccountUpdateRequest) *Account {
return &Account{
Name: *req.Name,
// Copy this fields from the created account.
@ -846,7 +846,7 @@ func TestFind(t *testing.T) {
var accounts []*Account
for i := 0; i <= 4; i++ {
account, err := Create(tests.Context(), auth.Claims{}, test.MasterDB, CreateAccountRequest{
account, err := Create(tests.Context(), auth.Claims{}, test.MasterDB, AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
@ -990,12 +990,37 @@ func mockUserAccount(accountId, userId string, now time.Time, roles ...string) e
roleArr = append(roleArr, r)
}
err := mockUser(userId, now)
if err != nil {
return err
}
// Build the insert SQL statement.
query := sqlbuilder.NewInsertBuilder()
query.InsertInto(userAccountTableName)
query.Cols("id", "user_id", "account_id", "roles", "created_at", "updated_at")
query.Values(uuid.NewRandom().String(), userId, accountId, roleArr, now, now)
// Execute the query with the provided context.
sql, args := query.Build()
sql = test.MasterDB.Rebind(sql)
_, err = test.MasterDB.ExecContext(tests.Context(), sql, args...)
if err != nil {
err = errors.Wrapf(err, "query - %s", query.String())
return err
}
return nil
}
func mockUser(userId string, now time.Time) error {
// Build the insert SQL statement.
query := sqlbuilder.NewInsertBuilder()
query.InsertInto("users")
query.Cols("id", "email", "password_hash", "password_salt", "created_at", "updated_at")
query.Values(userId, uuid.NewRandom().String(), "-", "-", now, now)
// Execute the query with the provided context.
sql, args := query.Build()
sql = test.MasterDB.Rebind(sql)

View File

@ -92,7 +92,7 @@ func SaasWrapHandler(confs ...func(c *Config)) web.Handler {
if err != nil {
return web.NewRequestError(err, http.StatusInternalServerError)
}
return web.RespondJson(ctx, w, doc, http.StatusOK)
return web.RespondJson(ctx, w, []byte(doc), http.StatusOK)
default:
if strings.HasSuffix(path, ".html") {
w.Header().Set("Content-Type", "text/html; charset=utf-8")

View File

@ -97,7 +97,7 @@ func (a *Authenticator) GenerateToken(claims Claims) (string, error) {
tkn := jwt.NewWithClaims(method, claims)
tkn.Header["kid"] = a.keyID
str, err := tkn.SignedString(a.privateKey)
str, err := tkn.SignedString(a.privateKey.PrivateKey)
if err != nil {
return "", errors.Wrap(err, "signing token")
}

View File

@ -20,13 +20,77 @@ func TestMain(m *testing.M) {
}
func testMain(m *testing.M) int {
tests.DisableDb = true
test = tests.New()
defer test.TearDown()
return m.Run()
}
func TestAuthenticator(t *testing.T) {
// TestAuthenticatorFile validates File storage.
func TestAuthenticatorFile(t *testing.T) {
var authTests = []struct {
name string
now time.Time
keyExpiration time.Duration
error error
}{
{"NoKeyExpiration", time.Now(), time.Duration(0), nil},
{"KeyExpirationOk", time.Now(), time.Duration(time.Second * 3600), nil},
{"KeyExpirationDisabled", time.Now().Add(time.Second * 3600 * 3), time.Duration(time.Second * 3600), nil},
}
// Generate the token.
signedClaims := auth.Claims{
Roles: []string{auth.RoleAdmin},
}
t.Log("Given the need to validate initiating a new Authenticator using File storage by key expiration.")
{
for i, tt := range authTests {
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name)
{
a, err := auth.NewAuthenticatorFile("", tt.now, tt.keyExpiration)
if err != tt.error {
t.Log("\t\tGot :", err)
t.Log("\t\tWant:", tt.error)
t.Fatalf("\t%s\tNewAuthenticatorFile failed.", tests.Failed)
}
tknStr, err := a.GenerateToken(signedClaims)
if err != nil {
t.Log("\t\tGot :", err)
t.Fatalf("\t%s\tGenerateToken failed.", tests.Failed)
}
parsedClaims, err := a.ParseClaims(tknStr)
if err != nil {
t.Log("\t\tGot :", err)
t.Fatalf("\t%s\tParseClaims failed.", tests.Failed)
}
// Assert expected claims.
if exp, got := len(signedClaims.Roles), len(parsedClaims.Roles); exp != got {
t.Log("\t\tGot :", got)
t.Log("\t\tWant:", exp)
t.Fatalf("\t%s\tShould got the same number of roles.", tests.Failed)
}
if exp, got := signedClaims.Roles[0], parsedClaims.Roles[0]; exp != got {
t.Log("\t\tGot :", got)
t.Log("\t\tWant:", exp)
t.Fatalf("\t%s\tShould got the same role name.", tests.Failed)
}
t.Logf("\t%s\tNewAuthenticatorFile ok.", tests.Success)
}
}
}
}
// TestAuthenticatorAws validates AWS storage.
func TestAuthenticatorAws(t *testing.T) {
awsSecretID := "jwt-key" + uuid.NewRandom().String()
@ -58,16 +122,16 @@ func TestAuthenticator(t *testing.T) {
Roles: []string{auth.RoleAdmin},
}
t.Log("Given the need to validate initiating a new Authenticator by key expiration.")
t.Log("Given the need to validate initiating a new Authenticator using AWS storage by key expiration.")
{
for i, tt := range authTests {
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name)
{
a, err := auth.NewAuthenticator(test.AwsSession, tt.awsSecretID, tt.now, tt.keyExpiration)
a, err := auth.NewAuthenticatorAws(test.AwsSession, tt.awsSecretID, tt.now, tt.keyExpiration)
if err != tt.error {
t.Log("\t\tGot :", err)
t.Log("\t\tWant:", tt.error)
t.Fatalf("\t%s\tNewAuthenticator failed.", tests.Failed)
t.Fatalf("\t%s\tNewAuthenticatorAws failed.", tests.Failed)
}
tknStr, err := a.GenerateToken(signedClaims)
@ -94,7 +158,7 @@ func TestAuthenticator(t *testing.T) {
t.Fatalf("\t%s\tShould got the same role name.", tests.Failed)
}
t.Logf("\t%s\tNewAuthenticator ok.", tests.Success)
t.Logf("\t%s\tNewAuthenticatorAws ok.", tests.Success)
}
}
}

View File

@ -13,7 +13,7 @@ import (
const algorithm = "RS256"
// keyGen creates an x509 private key for signing auth tokens.
func keyGen() ([]byte, error) {
func KeyGen() ([]byte, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return []byte{}, errors.Wrap(err, "generating keys")

View File

@ -107,7 +107,7 @@ func NewStorageFile(localDir string, now time.Time, keyExpiration time.Duration)
}
// Values used to format filename.
filePrefix := "auth_"
filePrefix := "sassauth_"
fileExt := ".privatekey"
files, err := ioutil.ReadDir(localDir)
@ -171,14 +171,14 @@ func NewStorageFile(localDir string, now time.Time, keyExpiration time.Duration)
// If there are no keys or the current key needs to be rotated, generate a new key.
if len(keyContents) == 0 || curKeyId == "" {
privateKey, err := keyGen()
privateKey, err := KeyGen()
if err != nil {
return nil, errors.Wrap(err, "failed to generate new private key")
}
kID := uuid.NewRandom().String()
fname := fmt.Sprintf("%s_%d_%s%s", filePrefix, now.UTC().Unix(), kID, fileExt)
fname := fmt.Sprintf("%s%d_%s%s", filePrefix, now.UTC().Unix(), kID, fileExt)
filePath := filepath.Join(localDir, fname)

View File

@ -186,7 +186,7 @@ func NewStorageAws(awsSession *session.Session, awsSecretID string, now time.Tim
// refreshed on instance launch. Could store keys in a kv store and update that value
// when new keys are generated
if len(keyContents) == 0 || curKeyId == "" {
privateKey, err := keyGen()
privateKey, err := KeyGen()
if err != nil {
return nil, errors.Wrap(err, "failed to generate new private key")
}

View File

@ -31,6 +31,9 @@ type Test struct {
AwsSession *session.Session
}
// Flag used to disable setting up database.
var DisableDb bool
// New is the entry point for tests.
func New() *Test {
@ -44,10 +47,17 @@ func New() *Test {
awsSession := session.Must(session.NewSession())
// ============================================================
// Startup Postgres container
container, err := docker.StartPostgres(log)
var (
masterDB *sqlx.DB
container *docker.Container
)
if !DisableDb {
var err error
container, err = docker.StartPostgres(log)
if err != nil {
log.Fatalln(err)
}
@ -61,7 +71,6 @@ func New() *Test {
// Start Postgres
log.Println("main : Started : Initialize Postgres")
var masterDB *sqlx.DB
for i := 0; i <= 20; i++ {
masterDB, err = sqlx.Open("postgres", dbHost)
if err != nil {
@ -89,6 +98,7 @@ func New() *Test {
log.Fatalf("main : Migrate : %v", err)
}
log.Printf("main : Migrate : Completed")
}
return &Test{log, masterDB, container, awsSession}
}
@ -96,10 +106,15 @@ func New() *Test {
// TearDown is used for shutting down tests. Calling this should be
// done in a defer immediately after calling New.
func (t *Test) TearDown() {
if t.MasterDB != nil {
t.MasterDB.Close()
}
if t.container != nil {
if err := docker.StopPostgres(t.Log, t.container); err != nil {
t.Log.Println(err)
}
}
}
// Recover is used to prevent panics from allowing the test to cleanup.

View File

@ -65,8 +65,8 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
zipcode varchar(20) NOT NULL DEFAULT '',
status account_status_t NOT NULL DEFAULT 'active',
timezone varchar(128) NOT NULL DEFAULT 'America/Anchorage',
signup_user_id char(36) DEFAULT NULL REFERENCES users(id),
billing_user_id char(36) DEFAULT NULL REFERENCES users(id),
signup_user_id char(36) DEFAULT NULL REFERENCES users(id) ON DELETE SET NULL,
billing_user_id char(36) DEFAULT NULL REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
archived_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
@ -93,7 +93,7 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
},
// create new table user_accounts
{
ID: "20190522-01c",
ID: "20190522-01d",
Migrate: func(tx *sql.Tx) error {
q1 := `CREATE TYPE user_account_role_t as enum('admin', 'user')`
if _, err := tx.Exec(q1); err != nil {
@ -107,8 +107,8 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
q3 := `CREATE TABLE IF NOT EXISTS users_accounts (
id char(36) NOT NULL,
account_id char(36) NOT NULL REFERENCES accounts(id),
user_id char(36) NOT NULL REFERENCES users(id),
account_id char(36) NOT NULL REFERENCES accounts(id) ON DELETE NO ACTION,
user_id char(36) NOT NULL REFERENCES users(id) ON DELETE NO ACTION,
roles user_account_role_t[] NOT NULL,
status user_account_status_t NOT NULL DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
@ -153,7 +153,7 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
q2 := `CREATE TABLE IF NOT EXISTS projects (
id char(36) NOT NULL,
account_id char(36) NOT NULL REFERENCES accounts(id),
account_id char(36) NOT NULL REFERENCES accounts(id) ON DELETE SET NULL,
name varchar(255) NOT NULL,
status project_status_t NOT NULL DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE NOT NULL,

View File

@ -0,0 +1,18 @@
package signup
import (
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
)
// SignupRequest contains information needed perform signup.
type SignupRequest struct {
Account account.AccountCreateRequest `json:"account" validate:"required"`
User user.UserCreateRequest `json:"user" validate:"required"`
}
// SignupResponse contains information needed perform signup.
type SignupResponse struct {
Account *account.Account `json:"account"`
User *user.User `json:"user"`
}

View File

@ -0,0 +1,98 @@
package signup
import (
"context"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user_account"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"github.com/jmoiron/sqlx"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/go-playground/validator.v9"
)
// Signup performs the steps needed to create a new account, new user and then associate
// both records with a new user_account entry.
func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req SignupRequest, now time.Time) (*SignupResponse, error) {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.signup.Signup")
defer span.Finish()
// Default account status to active for signup if now set.
if req.Account.Status == nil {
s := account.AccountStatus_Active
req.Account.Status = &s
}
v := validator.New()
// Validate the user email address is unique in the database.
uniqEmail, err := user.UniqueEmail(ctx, dbConn, req.User.Email, "")
if err != nil {
return nil, err
}
// Validate the account name is unique in the database.
uniqName, err := account.UniqueName(ctx, dbConn, req.Account.Name, "")
if err != nil {
return nil, err
}
f := func(fl validator.FieldLevel) bool {
if fl.Field().String() == "invalid" {
return false
}
var uniq bool
switch (fl.FieldName()) {
case "Name":
uniq = uniqName
case "Email":
uniq = uniqEmail
}
return uniq
}
v.RegisterValidation("unique", f)
// Validate the request.
err = v.Struct(req)
if err != nil {
return nil, err
}
var resp SignupResponse
// Execute user creation.
resp.User, err = user.Create(ctx, claims, dbConn, req.User, now)
if err != nil {
return nil, err
}
// Set the signup and billing user IDs for reference.
req.Account.SignupUserID = &resp.User.ID
req.Account.BillingUserID = &resp.User.ID
// Execute account creation.
resp.Account, err = account.Create(ctx, claims, dbConn, req.Account, now)
if err != nil {
return nil, err
}
// Associate the created user with the new account. The first user for the account will
// always have the role of admin.
ua := user_account.CreateUserAccountRequest{
UserID: resp.User.ID,
AccountID: resp.Account.ID,
Roles: []user_account.UserAccountRole{user_account.UserAccountRole_Admin},
//Status: Use default value
}
_, err = user_account.Create(ctx, claims, dbConn, ua, now)
if err != nil {
return nil, err
}
return &resp, nil
}

View File

@ -0,0 +1,172 @@
package signup
import (
"os"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
"github.com/pborman/uuid"
"github.com/pkg/errors"
)
var test *tests.Test
// TestMain is the entry point for testing.
func TestMain(m *testing.M) {
os.Exit(testMain(m))
}
func testMain(m *testing.M) int {
test = tests.New()
defer test.TearDown()
return m.Run()
}
// TestSignupValidation ensures all the validation tags work on Signup
func TestSignupValidation(t *testing.T) {
var userTests = []struct {
name string
req SignupRequest
expected func(req SignupRequest, res *SignupResponse) *SignupResponse
error error
}{
{"Required Fields",
SignupRequest{},
func(req SignupRequest, res *SignupResponse) *SignupResponse {
return nil
},
errors.New("Key: 'SignupRequest.Account.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" +
"Key: 'SignupRequest.Account.Address1' Error:Field validation for 'Address1' failed on the 'required' tag\n" +
"Key: 'SignupRequest.Account.City' Error:Field validation for 'City' failed on the 'required' tag\n" +
"Key: 'SignupRequest.Account.Region' Error:Field validation for 'Region' failed on the 'required' tag\n" +
"Key: 'SignupRequest.Account.Country' Error:Field validation for 'Country' failed on the 'required' tag\n" +
"Key: 'SignupRequest.Account.Zipcode' Error:Field validation for 'Zipcode' failed on the 'required' tag\n" +
"Key: 'SignupRequest.User.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" +
"Key: 'SignupRequest.User.Email' Error:Field validation for 'Email' failed on the 'required' tag\n" +
"Key: 'SignupRequest.User.Password' Error:Field validation for 'Password' failed on the 'required' tag"),
},
}
now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)
t.Log("Given the need ensure all validation tags are working for signup.")
{
for i, tt := range userTests {
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name)
{
ctx := tests.Context()
res, err := Signup(ctx, auth.Claims{}, test.MasterDB, tt.req, now)
if err != tt.error {
// TODO: need a better way to handle validation errors as they are
// of type interface validator.ValidationErrorsTranslations
var errStr string
if err != nil {
errStr = err.Error()
}
var expectStr string
if tt.error != nil {
expectStr = tt.error.Error()
}
if errStr != expectStr {
t.Logf("\t\tGot : %+v", err)
t.Logf("\t\tWant: %+v", tt.error)
t.Fatalf("\t%s\tSignup failed.", tests.Failed)
}
}
// If there was an error that was expected, then don't go any further
if tt.error != nil {
t.Logf("\t%s\tSignup ok.", tests.Success)
continue
}
expected := tt.expected(tt.req, res)
if diff := cmp.Diff(res, expected); diff != "" {
t.Fatalf("\t%s\tExpected result should match. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tSignup ok.", tests.Success)
}
}
}
}
// TestSignupFull validates Signup and ensures the created user can login.
func TestSignupFull(t *testing.T) {
req := SignupRequest{
Account: account.AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
City: "Valdez",
Region: "AK",
Country: "USA",
Zipcode: "99686",
},
User: user.UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier",
},
}
ctx := tests.Context()
now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)
tknGen := &user.MockTokenGenerator{}
t.Log("Given the need to ensure signup works.")
{
res, err := Signup(ctx, auth.Claims{}, test.MasterDB, req, now)
if err != nil {
t.Logf("\t\tGot error : %+v", err)
t.Fatalf("\t%s\tSignup failed.", tests.Failed)
}
if res.User == nil || res.User.ID == "" {
t.Fatalf("\t%s\tResponse user is empty.", tests.Failed)
}
if res.Account == nil || res.Account.ID == "" {
t.Fatalf("\t%s\tResponse account is empty.", tests.Failed)
}
if res.Account.SignupUserID.String == "" {
t.Fatalf("\t%s\tResponse account signup user ID is empty.", tests.Failed)
} else if res.Account.SignupUserID.String != res.User.ID {
t.Logf("\t\tGot : %+v", res.Account.SignupUserID.String)
t.Logf("\t\tWant: %+v", res.User.ID)
t.Fatalf("\t%s\tSigup user ID does not match created user ID.", tests.Failed)
}
if res.Account.BillingUserID.String == "" {
t.Fatalf("\t%s\tResponse account billing user ID is empty.", tests.Failed)
} else if res.Account.BillingUserID.String != res.User.ID {
t.Logf("\t\tGot : %+v", res.Account.BillingUserID.String)
t.Logf("\t\tWant: %+v", res.User.ID)
t.Fatalf("\t%s\tBilling user ID does not match created user ID.", tests.Failed)
}
t.Logf("\t%s\tSignup ok.", tests.Success)
// Verify that the user can be authenticated with the updated password.
_, err = user.Authenticate(ctx, test.MasterDB, tknGen, res.User.Email, req.User.Password, time.Hour, now)
if err != nil {
t.Log("\t\tGot :", err)
t.Fatalf("\t%s\tAuthenticate failed.", tests.Failed)
}
t.Logf("\t%s\tAuthenticate ok.", tests.Success)
}
}

View File

@ -2,6 +2,8 @@ package user
import (
"context"
"crypto/rsa"
"github.com/dgrijalva/jwt-go"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
@ -224,3 +226,68 @@ func generateToken(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator,
return Token{Token: tkn, claims: claims}, nil
}
// mockTokenGenerator is used for testing that Authenticate calls its provided
// token generator in a specific way.
type MockTokenGenerator struct {
// Private key generated by GenerateToken that is need for ParseClaims
key *rsa.PrivateKey
// algorithm is the method used to generate the private key.
algorithm string
}
// GenerateToken implements the TokenGenerator interface. It returns a "token"
// that includes some information about the claims it was passed.
func (g *MockTokenGenerator) GenerateToken(claims auth.Claims) (string, error) {
privateKey, err := auth.KeyGen()
if err != nil {
return "", err
}
g.key, err = jwt.ParseRSAPrivateKeyFromPEM(privateKey)
if err != nil {
return "", err
}
g.algorithm = "RS256"
method := jwt.GetSigningMethod(g.algorithm)
tkn := jwt.NewWithClaims(method, claims)
tkn.Header["kid"] = "1"
str, err := tkn.SignedString(g.key)
if err != nil {
return "", err
}
return str, nil
}
// ParseClaims recreates the Claims that were used to generate a token. It
// verifies that the token was signed using our key.
func (g *MockTokenGenerator) ParseClaims(tknStr string) (auth.Claims, error) {
parser := jwt.Parser{
ValidMethods: []string{g.algorithm},
}
if g.key == nil {
return auth.Claims{}, errors.New("Private key is empty.")
}
f := func(t *jwt.Token) (interface{}, error) {
return g.key.Public().(*rsa.PublicKey), nil
}
var claims auth.Claims
tkn, err := parser.ParseWithClaims(tknStr, &claims, f)
if err != nil {
return auth.Claims{}, errors.Wrap(err, "parsing token")
}
if !tkn.Valid {
return auth.Claims{}, errors.New("Invalid token")
}
return claims, nil
}

View File

@ -1,82 +1,16 @@
package user
import (
"crypto/rsa"
"testing"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
"github.com/dgrijalva/jwt-go"
"github.com/google/go-cmp/cmp"
"github.com/pborman/uuid"
"github.com/pkg/errors"
)
// mockTokenGenerator is used for testing that Authenticate calls its provided
// token generator in a specific way.
type mockTokenGenerator struct {
// Private key generated by GenerateToken that is need for ParseClaims
key *rsa.PrivateKey
// algorithm is the method used to generate the private key.
algorithm string
}
// GenerateToken implements the TokenGenerator interface. It returns a "token"
// that includes some information about the claims it was passed.
func (g *mockTokenGenerator) GenerateToken(claims auth.Claims) (string, error) {
privateKey, err := auth.Keygen()
if err != nil {
return "", err
}
g.key, err = jwt.ParseRSAPrivateKeyFromPEM(privateKey)
if err != nil {
return "", err
}
g.algorithm = "RS256"
method := jwt.GetSigningMethod(g.algorithm)
tkn := jwt.NewWithClaims(method, claims)
tkn.Header["kid"] = "1"
str, err := tkn.SignedString(g.key)
if err != nil {
return "", err
}
return str, nil
}
// ParseClaims recreates the Claims that were used to generate a token. It
// verifies that the token was signed using our key.
func (g *mockTokenGenerator) ParseClaims(tknStr string) (auth.Claims, error) {
parser := jwt.Parser{
ValidMethods: []string{g.algorithm},
}
if g.key == nil {
return auth.Claims{}, errors.New("Private key is empty.")
}
f := func(t *jwt.Token) (interface{}, error) {
return g.key.Public().(*rsa.PublicKey), nil
}
var claims auth.Claims
tkn, err := parser.ParseWithClaims(tknStr, &claims, f)
if err != nil {
return auth.Claims{}, errors.Wrap(err, "parsing token")
}
if !tkn.Valid {
return auth.Claims{}, errors.New("Invalid token")
}
return claims, nil
}
// TestAuthenticate validates the behavior around authenticating users.
func TestAuthenticate(t *testing.T) {
defer tests.Recover(t)
@ -87,7 +21,7 @@ func TestAuthenticate(t *testing.T) {
{
ctx := tests.Context()
tknGen := &mockTokenGenerator{}
tknGen := &MockTokenGenerator{}
// Auth tokens are valid for an our and is verified against current time.
// Issue the token one hour ago.
@ -104,7 +38,7 @@ func TestAuthenticate(t *testing.T) {
// Create a new user for testing.
initPass := uuid.NewRandom().String()
user, err := Create(ctx, auth.Claims{}, test.MasterDB, CreateUserRequest{
user, err := Create(ctx, auth.Claims{}, test.MasterDB, UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: initPass,
@ -132,7 +66,7 @@ func TestAuthenticate(t *testing.T) {
t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)
}
// Create a second new random account.
// Create a second new random account. Need to ensure
account2Id := uuid.NewRandom().String()
err = mockAccount(account2Id, user.CreatedAt)
if err != nil {
@ -140,9 +74,11 @@ func TestAuthenticate(t *testing.T) {
t.Fatalf("\t%s\tCreate account failed.", tests.Failed)
}
// Associate secoend new account with user user.
// Associate second new account with user user. Need to ensure that now
// is always greater than the first user_account entry created so it will
// be returned consistently back in the same order, last.
account2Role := auth.RoleUser
err = mockUserAccount(user.ID, account2Id, user.CreatedAt, account2Role)
err = mockUserAccount(user.ID, account2Id, user.CreatedAt.Add(time.Second), account2Role)
if err != nil {
t.Log("\t\tGot :", err)
t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)

View File

@ -232,7 +232,7 @@ func find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, query *sqlbu
}
// Validation an email address is unique excluding the current user ID.
func uniqueEmail(ctx context.Context, dbConn *sqlx.DB, email, userId string) (bool, error) {
func UniqueEmail(ctx context.Context, dbConn *sqlx.DB, email, userId string) (bool, error) {
query := sqlbuilder.NewSelectBuilder().Select("id").From(userTableName)
query.Where(query.And(
query.Equal("email", email),
@ -264,7 +264,7 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserCr
v := validator.New()
// Validation email address is unique in the database.
uniq, err := uniqueEmail(ctx, dbConn, req.Email, "")
uniq, err := UniqueEmail(ctx, dbConn, req.Email, "")
if err != nil {
return nil, err
}
@ -376,7 +376,7 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserUp
// Validation email address is unique in the database.
if req.Email != nil {
uniq, err := uniqueEmail(ctx, dbConn, *req.Email, req.ID)
uniq, err := UniqueEmail(ctx, dbConn, *req.Email, req.ID)
if err != nil {
return err
}
@ -614,22 +614,14 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userID str
return err
}
// Build the delete SQL statement.
query := sqlbuilder.NewDeleteBuilder()
query.DeleteFrom(userTableName)
query.Where(query.Equal("id", req.ID))
// Execute the query with the provided context.
sql, args := query.Build()
sql = dbConn.Rebind(sql)
_, err = dbConn.ExecContext(ctx, sql, args...)
// Start a new transaction to handle rollbacks on error.
tx, err := dbConn.Begin()
if err != nil {
err = errors.Wrapf(err, "query - %s", query.String())
err = errors.WithMessagef(err, "delete user %s failed", req.ID)
return err
return errors.WithStack(err)
}
// Delete all the associated user accounts
// Delete all the associated user accounts.
// Required to execute first to avoid foreign key constraints.
{
// Build the delete SQL statement.
query := sqlbuilder.NewDeleteBuilder()
@ -641,13 +633,37 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userID str
// Execute the query with the provided context.
sql, args := query.Build()
sql = dbConn.Rebind(sql)
_, err = dbConn.ExecContext(ctx, sql, args...)
_, err = tx.ExecContext(ctx, sql, args...)
if err != nil {
tx.Rollback()
err = errors.Wrapf(err, "query - %s", query.String())
err = errors.WithMessagef(err, "delete accounts for user %s failed", req.ID)
return err
}
}
// Build the delete SQL statement.
query := sqlbuilder.NewDeleteBuilder()
query.DeleteFrom(userTableName)
query.Where(query.Equal("id", req.ID))
// Execute the query with the provided context.
sql, args := query.Build()
sql = dbConn.Rebind(sql)
_, err = tx.ExecContext(ctx, sql, args...)
if err != nil {
tx.Rollback()
err = errors.Wrapf(err, "query - %s", query.String())
err = errors.WithMessagef(err, "delete user %s failed", req.ID)
return err
}
err = tx.Commit()
if err != nil {
return errors.WithStack(err)
}
return nil
}

View File

@ -140,51 +140,51 @@ func TestCreateValidation(t *testing.T) {
var userTests = []struct {
name string
req CreateUserRequest
expected func(req CreateUserRequest, res *User) *User
req UserCreateRequest
expected func(req UserCreateRequest, res *User) *User
error error
}{
{"Required Fields",
CreateUserRequest{},
func(req CreateUserRequest, res *User) *User {
UserCreateRequest{},
func(req UserCreateRequest, res *User) *User {
return nil
},
errors.New("Key: 'CreateUserRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" +
"Key: 'CreateUserRequest.Email' Error:Field validation for 'Email' failed on the 'required' tag\n" +
"Key: 'CreateUserRequest.Password' Error:Field validation for 'Password' failed on the 'required' tag"),
errors.New("Key: 'UserCreateRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" +
"Key: 'UserCreateRequest.Email' Error:Field validation for 'Email' failed on the 'required' tag\n" +
"Key: 'UserCreateRequest.Password' Error:Field validation for 'Password' failed on the 'required' tag"),
},
{"Valid Email",
CreateUserRequest{
UserCreateRequest{
Name: "Lee Brown",
Email: "xxxxxxxxxx",
Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier",
},
func(req CreateUserRequest, res *User) *User {
func(req UserCreateRequest, res *User) *User {
return nil
},
errors.New("Key: 'CreateUserRequest.Email' Error:Field validation for 'Email' failed on the 'email' tag"),
errors.New("Key: 'UserCreateRequest.Email' Error:Field validation for 'Email' failed on the 'email' tag"),
},
{"Passwords Match",
CreateUserRequest{
UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
PasswordConfirm: "W0rkL1fe#",
},
func(req CreateUserRequest, res *User) *User {
func(req UserCreateRequest, res *User) *User {
return nil
},
errors.New("Key: 'CreateUserRequest.PasswordConfirm' Error:Field validation for 'PasswordConfirm' failed on the 'eqfield' tag"),
errors.New("Key: 'UserCreateRequest.PasswordConfirm' Error:Field validation for 'PasswordConfirm' failed on the 'eqfield' tag"),
},
{"Default Timezone",
CreateUserRequest{
UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier",
},
func(req CreateUserRequest, res *User) *User {
func(req UserCreateRequest, res *User) *User {
return &User{
Name: req.Name,
Email: req.Email,
@ -258,7 +258,7 @@ func TestCreateValidationEmailUnique(t *testing.T) {
{
ctx := tests.Context()
req1 := CreateUserRequest{
req1 := UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
@ -270,13 +270,13 @@ func TestCreateValidationEmailUnique(t *testing.T) {
t.Fatalf("\t%s\tCreate failed.", tests.Failed)
}
req2 := CreateUserRequest{
req2 := UserCreateRequest{
Name: "Lucas Brown",
Email: user1.Email,
Password: "W0rkL1fe#",
PasswordConfirm: "W0rkL1fe#",
}
expectedErr := errors.New("Key: 'CreateUserRequest.Email' Error:Field validation for 'Email' failed on the 'unique' tag")
expectedErr := errors.New("Key: 'UserCreateRequest.Email' Error:Field validation for 'Email' failed on the 'unique' tag")
_, err = Create(ctx, auth.Claims{}, test.MasterDB, req2, now)
if err == nil {
t.Logf("\t\tWant: %+v", expectedErr)
@ -300,13 +300,13 @@ func TestCreateClaims(t *testing.T) {
var userTests = []struct {
name string
claims auth.Claims
req CreateUserRequest
req UserCreateRequest
error error
}{
// Internal request, should bypass ACL.
{"EmptyClaims",
auth.Claims{},
CreateUserRequest{
UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
@ -323,7 +323,7 @@ func TestCreateClaims(t *testing.T) {
Audience: "acc1",
},
},
CreateUserRequest{
UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
@ -340,7 +340,7 @@ func TestCreateClaims(t *testing.T) {
Audience: "acc1",
},
},
CreateUserRequest{
UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
@ -377,24 +377,24 @@ func TestUpdateValidation(t *testing.T) {
// TODO: actually create the user so can test the output of findbyId
type userTest struct {
name string
req UpdateUserRequest
req UserUpdateRequest
error error
}
var userTests = []userTest{
{"Required Fields",
UpdateUserRequest{},
errors.New("Key: 'UpdateUserRequest.ID' Error:Field validation for 'ID' failed on the 'required' tag"),
UserUpdateRequest{},
errors.New("Key: 'UserUpdateRequest.ID' Error:Field validation for 'ID' failed on the 'required' tag"),
},
}
invalidEmail := "xxxxxxxxxx"
userTests = append(userTests, userTest{"Valid Email",
UpdateUserRequest{
UserUpdateRequest{
ID: uuid.NewRandom().String(),
Email: &invalidEmail,
},
errors.New("Key: 'UpdateUserRequest.Email' Error:Field validation for 'Email' failed on the 'email' tag"),
errors.New("Key: 'UserUpdateRequest.Email' Error:Field validation for 'Email' failed on the 'email' tag"),
})
now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)
@ -440,7 +440,7 @@ func TestUpdateValidationEmailUnique(t *testing.T) {
{
ctx := tests.Context()
req1 := CreateUserRequest{
req1 := UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
@ -452,7 +452,7 @@ func TestUpdateValidationEmailUnique(t *testing.T) {
t.Fatalf("\t%s\tCreate failed.", tests.Failed)
}
req2 := CreateUserRequest{
req2 := UserCreateRequest{
Name: "Lucas Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "W0rkL1fe#",
@ -465,11 +465,11 @@ func TestUpdateValidationEmailUnique(t *testing.T) {
}
// Try to set the email for user 1 on user 2
updateReq := UpdateUserRequest{
updateReq := UserUpdateRequest{
ID: user2.ID,
Email: &user1.Email,
}
expectedErr := errors.New("Key: 'UpdateUserRequest.Email' Error:Field validation for 'Email' failed on the 'unique' tag")
expectedErr := errors.New("Key: 'UserUpdateRequest.Email' Error:Field validation for 'Email' failed on the 'unique' tag")
err = Update(ctx, auth.Claims{}, test.MasterDB, updateReq, now)
if err == nil {
t.Logf("\t\tWant: %+v", expectedErr)
@ -495,11 +495,11 @@ func TestUpdatePassword(t *testing.T) {
now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)
tknGen := &mockTokenGenerator{}
tknGen := &MockTokenGenerator{}
// Create a new user for testing.
initPass := uuid.NewRandom().String()
user, err := Create(ctx, auth.Claims{}, test.MasterDB, CreateUserRequest{
user, err := Create(ctx, auth.Claims{}, test.MasterDB, UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: initPass,
@ -533,9 +533,9 @@ func TestUpdatePassword(t *testing.T) {
}
// Ensure validation is working by trying UpdatePassword with an empty request.
expectedErr := errors.New("Key: 'UpdatePasswordRequest.ID' Error:Field validation for 'ID' failed on the 'required' tag\n" +
"Key: 'UpdatePasswordRequest.Password' Error:Field validation for 'Password' failed on the 'required' tag")
err = UpdatePassword(ctx, auth.Claims{}, test.MasterDB, UpdatePasswordRequest{}, now)
expectedErr := errors.New("Key: 'UserUpdatePasswordRequest.ID' Error:Field validation for 'ID' failed on the 'required' tag\n" +
"Key: 'UserUpdatePasswordRequest.Password' Error:Field validation for 'Password' failed on the 'required' tag")
err = UpdatePassword(ctx, auth.Claims{}, test.MasterDB, UserUpdatePasswordRequest{}, now)
if err == nil {
t.Logf("\t\tWant: %+v", expectedErr)
t.Fatalf("\t%s\tUpdate failed.", tests.Failed)
@ -548,7 +548,7 @@ func TestUpdatePassword(t *testing.T) {
// Update the users password.
newPass := uuid.NewRandom().String()
err = UpdatePassword(ctx, auth.Claims{}, test.MasterDB, UpdatePasswordRequest{
err = UpdatePassword(ctx, auth.Claims{}, test.MasterDB, UserUpdatePasswordRequest{
ID: user.ID,
Password: newPass,
PasswordConfirm: newPass,
@ -576,10 +576,10 @@ func TestCrud(t *testing.T) {
type userTest struct {
name string
claims func(*User, string) auth.Claims
create CreateUserRequest
update func(*User) UpdateUserRequest
create UserCreateRequest
update func(*User) UserUpdateRequest
updateErr error
expected func(*User, UpdateUserRequest) *User
expected func(*User, UserUpdateRequest) *User
findErr error
}
@ -590,21 +590,21 @@ func TestCrud(t *testing.T) {
func(user *User, accountId string) auth.Claims {
return auth.Claims{}
},
CreateUserRequest{
UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier",
},
func(user *User) UpdateUserRequest {
func(user *User) UserUpdateRequest {
email := uuid.NewRandom().String() + "@geeksinthewoods.com"
return UpdateUserRequest{
return UserUpdateRequest{
ID: user.ID,
Email: &email,
}
},
nil,
func(user *User, req UpdateUserRequest) *User {
func(user *User, req UserUpdateRequest) *User {
return &User{
Email: *req.Email,
// Copy this fields from the created user.
@ -633,21 +633,21 @@ func TestCrud(t *testing.T) {
},
}
},
CreateUserRequest{
UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier",
},
func(user *User) UpdateUserRequest {
func(user *User) UserUpdateRequest {
email := uuid.NewRandom().String() + "@geeksinthewoods.com"
return UpdateUserRequest{
return UserUpdateRequest{
ID: user.ID,
Email: &email,
}
},
ErrForbidden,
func(user *User, req UpdateUserRequest) *User {
func(user *User, req UserUpdateRequest) *User {
return user
},
ErrNotFound,
@ -664,21 +664,21 @@ func TestCrud(t *testing.T) {
},
}
},
CreateUserRequest{
UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier",
},
func(user *User) UpdateUserRequest {
func(user *User) UserUpdateRequest {
email := uuid.NewRandom().String() + "@geeksinthewoods.com"
return UpdateUserRequest{
return UserUpdateRequest{
ID: user.ID,
Email: &email,
}
},
nil,
func(user *User, req UpdateUserRequest) *User {
func(user *User, req UserUpdateRequest) *User {
return &User{
Email: *req.Email,
// Copy this fields from the created user.
@ -707,21 +707,21 @@ func TestCrud(t *testing.T) {
},
}
},
CreateUserRequest{
UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier",
},
func(user *User) UpdateUserRequest {
func(user *User) UserUpdateRequest {
email := uuid.NewRandom().String() + "@geeksinthewoods.com"
return UpdateUserRequest{
return UserUpdateRequest{
ID: user.ID,
Email: &email,
}
},
ErrForbidden,
func(user *User, req UpdateUserRequest) *User {
func(user *User, req UserUpdateRequest) *User {
return nil
},
ErrNotFound,
@ -738,21 +738,21 @@ func TestCrud(t *testing.T) {
},
}
},
CreateUserRequest{
UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier",
},
func(user *User) UpdateUserRequest {
func(user *User) UserUpdateRequest {
email := uuid.NewRandom().String() + "@geeksinthewoods.com"
return UpdateUserRequest{
return UserUpdateRequest{
ID: user.ID,
Email: &email,
}
},
nil,
func(user *User, req UpdateUserRequest) *User {
func(user *User, req UserUpdateRequest) *User {
return &User{
Email: *req.Email,
// Copy this fields from the created user.
@ -784,15 +784,22 @@ func TestCrud(t *testing.T) {
user, err := Create(tests.Context(), auth.Claims{}, test.MasterDB, tt.create, now)
if err != nil {
t.Log("\t\tGot :", err)
t.Fatalf("\t%s\tCreate failed.", tests.Failed)
t.Fatalf("\t%s\tCreate user failed.", tests.Failed)
}
// Create a new random account and associate that with the user.
// Create a random account for the new user.
accountId := uuid.NewRandom().String()
err = mockAccount(accountId, user.CreatedAt)
if err != nil {
t.Log("\t\tGot :", err)
t.Fatalf("\t%s\tCreate account failed.", tests.Failed)
}
// Associate the account with the new test user.
err = mockUserAccount(user.ID, accountId, user.CreatedAt, auth.RoleAdmin)
if err != nil {
t.Log("\t\tGot :", err)
t.Fatalf("\t%s\tAdd user account failed.", tests.Failed)
t.Fatalf("\t%s\tCreate user account failed.", tests.Failed)
}
// Update the user.
@ -874,7 +881,7 @@ func TestFind(t *testing.T) {
var users []*User
for i := 0; i <= 4; i++ {
user, err := Create(tests.Context(), auth.Claims{}, test.MasterDB, CreateUserRequest{
user, err := Create(tests.Context(), auth.Claims{}, test.MasterDB, UserCreateRequest{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",

View File

@ -1,4 +1,4 @@
package user
package user_account
import (
"database/sql/driver"

View File

@ -1,4 +1,4 @@
package user
package user_account
import (
"context"

View File

@ -1,4 +1,4 @@
package user
package user_account
import (
"github.com/lib/pq"
@ -212,6 +212,20 @@ func TestCreateValidation(t *testing.T) {
{
ctx := tests.Context()
// Generate a new random user.
err := mockUser(tt.req.UserID, now)
if err != nil {
t.Logf("\t\tGot : %+v", err)
t.Fatalf("\t%s\tMock user failed.", tests.Failed)
}
// Generate a new random account.
err = mockAccount(tt.req.AccountID, now)
if err != nil {
t.Logf("\t\tGot : %+v", err)
t.Fatalf("\t%s\tMock account failed.", tests.Failed)
}
res, err := Create(ctx, auth.Claims{}, test.MasterDB, tt.req, now)
if err != tt.error {
// TODO: need a better way to handle validation errors as they are
@ -258,9 +272,25 @@ func TestCreateExistingEntry(t *testing.T) {
{
ctx := tests.Context()
// Generate a new random user.
userID := uuid.NewRandom().String()
err := mockUser(userID, now)
if err != nil {
t.Logf("\t\tGot : %+v", err)
t.Fatalf("\t%s\tMock user failed.", tests.Failed)
}
// Generate a new random account.
accountID := uuid.NewRandom().String()
err = mockAccount(accountID, now)
if err != nil {
t.Logf("\t\tGot : %+v", err)
t.Fatalf("\t%s\tMock account failed.", tests.Failed)
}
req1 := CreateUserAccountRequest{
UserID: uuid.NewRandom().String(),
AccountID: uuid.NewRandom().String(),
UserID: userID,
AccountID: accountID,
Roles: []UserAccountRole{UserAccountRole_User},
}
ua1, err := Create(ctx, auth.Claims{}, test.MasterDB, req1, now)
@ -503,9 +533,23 @@ func TestCrud(t *testing.T) {
for i, tt := range accountTests {
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name)
{
// Create a new random account and associate that with the user.
// Generate a new random user.
userID := uuid.NewRandom().String()
err := mockUser(userID, now)
if err != nil {
t.Logf("\t\tGot : %+v", err)
t.Fatalf("\t%s\tMock user failed.", tests.Failed)
}
// Generate a new random account.
accountID := uuid.NewRandom().String()
err = mockAccount(accountID, now)
if err != nil {
t.Logf("\t\tGot : %+v", err)
t.Fatalf("\t%s\tMock account failed.", tests.Failed)
}
// Associate that with the user.
createReq := CreateUserAccountRequest{
UserID: userID,
AccountID: accountID,
@ -656,9 +700,23 @@ func TestFind(t *testing.T) {
var userAccounts []*UserAccount
for i := 0; i <= 4; i++ {
// Create a new random account and associate that with the user.
// Generate a new random user.
userID := uuid.NewRandom().String()
err := mockUser(userID, now)
if err != nil {
t.Logf("\t\tGot : %+v", err)
t.Fatalf("\t%s\tCreate user failed.", tests.Failed)
}
// Generate a new random account.
accountID := uuid.NewRandom().String()
err = mockAccount(accountID, now)
if err != nil {
t.Logf("\t\tGot : %+v", err)
t.Fatalf("\t%s\tCreate account failed.", tests.Failed)
}
// Execute Create that will associate the user with the account.
ua, err := Create(tests.Context(), auth.Claims{}, test.MasterDB, CreateUserAccountRequest{
UserID: userID,
AccountID: accountID,
@ -784,3 +842,43 @@ func TestFind(t *testing.T) {
}
}
}
func mockAccount(accountId string, now time.Time) error {
// Build the insert SQL statement.
query := sqlbuilder.NewInsertBuilder()
query.InsertInto("accounts")
query.Cols("id", "name", "created_at", "updated_at")
query.Values(accountId, uuid.NewRandom().String(), now, now)
// Execute the query with the provided context.
sql, args := query.Build()
sql = test.MasterDB.Rebind(sql)
_, err := test.MasterDB.ExecContext(tests.Context(), sql, args...)
if err != nil {
err = errors.Wrapf(err, "query - %s", query.String())
return err
}
return nil
}
func mockUser(userId string, now time.Time) error {
// Build the insert SQL statement.
query := sqlbuilder.NewInsertBuilder()
query.InsertInto("users")
query.Cols("id", "email", "password_hash", "password_salt", "created_at", "updated_at")
query.Values(userId, uuid.NewRandom().String(), "-", "-", now, now)
// Execute the query with the provided context.
sql, args := query.Build()
sql = test.MasterDB.Rebind(sql)
_, err := test.MasterDB.ExecContext(tests.Context(), sql, args...)
if err != nil {
err = errors.Wrapf(err, "query - %s", query.String())
return err
}
return nil
}