1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-15 00:15:15 +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"` Database string `default:"shared" envconfig:"DATABASE"`
Driver string `default:"postgres" envconfig:"DRIVER"` Driver string `default:"postgres" envconfig:"DRIVER"`
Timezone string `default:"utc" envconfig:"TIMEZONE"` 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 ```bash
docker build -f cmd/web-api/Dockerfile -t saas-web-api . 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 // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag at // 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 package docs
@ -16,10 +16,10 @@ var doc = `{
"info": { "info": {
"description": "This is a sample server celler server.", "description": "This is a sample server celler server.",
"title": "SaaS Example API", "title": "SaaS Example API",
"termsOfService": "http://geeksinthewoods.com/terms", "termsOfService": "/terms",
"contact": { "contact": {
"name": "API Support", "name": "API Support",
"url": "https://gitlab.com/geeks-accelerator/oss/saas-starter-kit", "url": "/support",
"email": "support@geeksinthewoods.com" "email": "support@geeksinthewoods.com"
}, },
"license": { "license": {
@ -30,20 +30,140 @@ var doc = `{
}, },
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "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": { "securityDefinitions": {
"ApiKeyAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
},
"BasicAuth": {
"type": "basic"
},
"OAuth2Password": { "OAuth2Password": {
"type": "oauth2", "type": "oauth2",
"flow": "password", "flow": "password",
"tokenUrl": "https://example.com/v1/oauth/token" "tokenUrl": "/v1/oauth/token"
} }
} }
}` }`

View File

@ -3,10 +3,10 @@
"info": { "info": {
"description": "This is a sample server celler server.", "description": "This is a sample server celler server.",
"title": "SaaS Example API", "title": "SaaS Example API",
"termsOfService": "http://geeksinthewoods.com/terms", "termsOfService": "/terms",
"contact": { "contact": {
"name": "API Support", "name": "API Support",
"url": "https://gitlab.com/geeks-accelerator/oss/saas-starter-kit", "url": "/support",
"email": "support@geeksinthewoods.com" "email": "support@geeksinthewoods.com"
}, },
"license": { "license": {
@ -17,20 +17,140 @@
}, },
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "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": { "securityDefinitions": {
"ApiKeyAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
},
"BasicAuth": {
"type": "basic"
},
"OAuth2Password": { "OAuth2Password": {
"type": "oauth2", "type": "oauth2",
"flow": "password", "flow": "password",
"tokenUrl": "https://example.com/v1/oauth/token" "tokenUrl": "/v1/oauth/token"
} }
} }
} }

View File

@ -1,27 +1,106 @@
basePath: '{{.BasePath}}' 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}}' host: '{{.Host}}'
info: info:
contact: contact:
email: support@geeksinthewoods.com email: support@geeksinthewoods.com
name: API Support name: API Support
url: https://gitlab.com/geeks-accelerator/oss/saas-starter-kit url: /support
description: This is a sample server celler server. description: This is a sample server celler server.
license: license:
name: Apache 2.0 name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html url: http://www.apache.org/licenses/LICENSE-2.0.html
termsOfService: http://geeksinthewoods.com/terms termsOfService: /terms
title: SaaS Example API title: SaaS Example API
version: '{{.Version}}' 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: securityDefinitions:
ApiKeyAuth:
in: header
name: Authorization
type: apiKey
BasicAuth:
type: basic
OAuth2Password: OAuth2Password:
flow: password flow: password
tokenUrl: https://example.com/v1/oauth/token tokenUrl: /v1/oauth/token
type: oauth2 type: oauth2
swagger: "2.0" 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) 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 { 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) claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok { 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)) app.Handle("PATCH", "/v1/users/switch-account/:accountId", u.SwitchAccount, mid.Authenticate(authenticator))
// This route is not authenticated // This route is not authenticated
app.Handle("GET", "/v1/oauth/token", u.Token) app.Handle("POST", "/v1/oauth/token", u.Token)
// Register account endpoints. // Register account endpoints.
a := Account{ 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)) app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
// Register swagger documentation. // Register swagger documentation.
app.Handle("GET", "/swagger/", saasSwagger.WrapHandler, mid.Authenticate(authenticator)) // TODO: Add authentication. Current authenticator requires an Authorization header
app.Handle("GET", "/swagger/*", saasSwagger.WrapHandler, mid.Authenticate(authenticator)) // which breaks the browser experience.
app.Handle("GET", "/swagger/", saasSwagger.WrapHandler)
app.Handle("GET", "/swagger/*", saasSwagger.WrapHandler)
return app return app
} }

View File

@ -42,23 +42,17 @@ var service = "WEB_API"
// @title SaaS Example API // @title SaaS Example API
// @description This is a sample server celler server. // @description This is a sample server celler server.
// @termsOfService http://geeksinthewoods.com/terms // @termsOfService http://example.com/terms
// @contact.name API Support // @contact.name API Support
// @contact.email support@geeksinthewoods.com // @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.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @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 // @securitydefinitions.oauth2.password OAuth2Password
// @tokenUrl https://example.com/v1/oauth/token // @tokenUrl /v1/oauth/token
func main() { func main() {
@ -101,7 +95,7 @@ func main() {
Database string `default:"shared" envconfig:"DATABASE"` Database string `default:"shared" envconfig:"DATABASE"`
Driver string `default:"postgres" envconfig:"DRIVER"` Driver string `default:"postgres" envconfig:"DRIVER"`
Timezone string `default:"utc" envconfig:"TIMEZONE"` Timezone string `default:"utc" envconfig:"TIMEZONE"`
DisableTLS bool `default:"false" envconfig:"DISABLE_TLS"` DisableTLS bool `default:"true" envconfig:"DISABLE_TLS"`
} }
Trace struct { Trace struct {
Host string `default:"127.0.0.1" envconfig:"DD_TRACE_AGENT_HOSTNAME"` 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"` Database string `default:"shared" envconfig:"DATABASE"`
Driver string `default:"postgres" envconfig:"DRIVER"` Driver string `default:"postgres" envconfig:"DRIVER"`
Timezone string `default:"utc" envconfig:"TIMEZONE"` Timezone string `default:"utc" envconfig:"TIMEZONE"`
DisableTLS bool `default:"false" envconfig:"DISABLE_TLS"` DisableTLS bool `default:"true" envconfig:"DISABLE_TLS"`
} }
Trace struct { Trace struct {
Host string `default:"127.0.0.1" envconfig:"DD_TRACE_AGENT_HOSTNAME"` 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/locales v0.12.1
github.com/go-playground/universal-translator v0.16.0 github.com/go-playground/universal-translator v0.16.0
github.com/go-redis/redis v6.15.2+incompatible 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/gorilla/schema v1.1.0
github.com/huandu/go-sqlbuilder v1.4.0 github.com/huandu/go-sqlbuilder v1.4.0
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365 github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365
github.com/jmoiron/sqlx v1.2.0 github.com/jmoiron/sqlx v1.2.0
github.com/kelseyhightower/envconfig v1.3.0 github.com/kelseyhightower/envconfig v1.3.0
github.com/leodido/go-urn v1.1.0 // indirect github.com/leodido/go-urn v1.1.0
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01 github.com/lib/pq v1.1.1
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481 // indirect github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/onsi/ginkgo v1.8.0 // indirect 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/crypto v0.0.0-20190621222207-cc06ce4a13d4
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 // 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 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/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v9 v9.29.0 gopkg.in/go-playground/validator.v9 v9.29.0
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce 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/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 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 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/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 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 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 h1:EPw7R3OAyxHBCyl0oqh3lUZqS5lu3KSxzzGasE0opXQ=
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 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= 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-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 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-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.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 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw=
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 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 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.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 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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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. // 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 := sqlbuilder.NewSelectBuilder().Select("id").From(accountTableName)
query.Where(query.And( query.Where(query.And(
query.Equal("name", name), query.Equal("name", name),
@ -264,7 +264,7 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Accoun
v := validator.New() v := validator.New()
// Validation email address is unique in the database. // 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 { if err != nil {
return nil, err 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. // Validation name is unique in the database.
if req.Name != nil { 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 { if err != nil {
return err return err
} }
@ -583,22 +583,14 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, accountID
return err return err
} }
// Build the delete SQL statement. // Start a new transaction to handle rollbacks on error.
query := sqlbuilder.NewDeleteBuilder() tx, err := dbConn.Begin()
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...)
if err != nil { if err != nil {
err = errors.Wrapf(err, "query - %s", query.String()) return errors.WithStack(err)
err = errors.WithMessagef(err, "delete account %s failed", req.ID)
return 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. // Build the delete SQL statement.
query := sqlbuilder.NewDeleteBuilder() 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. // Execute the query with the provided context.
sql, args := query.Build() sql, args := query.Build()
sql = dbConn.Rebind(sql) sql = dbConn.Rebind(sql)
_, err = dbConn.ExecContext(ctx, sql, args...) _, err = tx.ExecContext(ctx, sql, args...)
if err != nil { if err != nil {
tx.Rollback()
err = errors.Wrapf(err, "query - %s", query.String()) err = errors.Wrapf(err, "query - %s", query.String())
err = errors.WithMessagef(err, "delete users for account %s failed", req.ID) err = errors.WithMessagef(err, "delete users for account %s failed", req.ID)
return err 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 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 package account
import ( import (
"github.com/lib/pq"
"math/rand" "math/rand"
"os" "os"
"strings" "strings"
"testing" "testing"
"time" "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/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
@ -142,25 +142,25 @@ func TestCreateValidation(t *testing.T) {
var accountTests = []struct { var accountTests = []struct {
name string name string
req CreateAccountRequest req AccountCreateRequest
expected func(req CreateAccountRequest, res *Account) *Account expected func(req AccountCreateRequest, res *Account) *Account
error error error error
}{ }{
{"Required Fields", {"Required Fields",
CreateAccountRequest{}, AccountCreateRequest{},
func(req CreateAccountRequest, res *Account) *Account { func(req AccountCreateRequest, res *Account) *Account {
return nil return nil
}, },
errors.New("Key: 'CreateAccountRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" + errors.New("Key: 'AccountCreateRequest.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: 'AccountCreateRequest.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: 'AccountCreateRequest.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: 'AccountCreateRequest.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: 'AccountCreateRequest.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"), "Key: 'AccountCreateRequest.Zipcode' Error:Field validation for 'Zipcode' failed on the 'required' tag"),
}, },
{"Default Timezone & Status", {"Default Timezone & Status",
CreateAccountRequest{ AccountCreateRequest{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -169,7 +169,7 @@ func TestCreateValidation(t *testing.T) {
Country: "USA", Country: "USA",
Zipcode: "99686", Zipcode: "99686",
}, },
func(req CreateAccountRequest, res *Account) *Account { func(req AccountCreateRequest, res *Account) *Account {
return &Account{ return &Account{
Name: req.Name, Name: req.Name,
Address1: req.Address1, Address1: req.Address1,
@ -191,7 +191,7 @@ func TestCreateValidation(t *testing.T) {
nil, nil,
}, },
{"Valid Status", {"Valid Status",
CreateAccountRequest{ AccountCreateRequest{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -201,10 +201,10 @@ func TestCreateValidation(t *testing.T) {
Zipcode: "99686", Zipcode: "99686",
Status: &invalidStatus, Status: &invalidStatus,
}, },
func(req CreateAccountRequest, res *Account) *Account { func(req AccountCreateRequest, res *Account) *Account {
return nil 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() ctx := tests.Context()
req1 := CreateAccountRequest{ req1 := AccountCreateRequest{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -277,7 +277,7 @@ func TestCreateValidationNameUnique(t *testing.T) {
t.Fatalf("\t%s\tCreate failed.", tests.Failed) t.Fatalf("\t%s\tCreate failed.", tests.Failed)
} }
req2 := CreateAccountRequest{ req2 := AccountCreateRequest{
Name: account1.Name, Name: account1.Name,
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -286,7 +286,7 @@ func TestCreateValidationNameUnique(t *testing.T) {
Country: "USA", Country: "USA",
Zipcode: "99686", 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) _, err = Create(ctx, auth.Claims{}, test.MasterDB, req2, now)
if err == nil { if err == nil {
t.Logf("\t\tWant: %+v", expectedErr) t.Logf("\t\tWant: %+v", expectedErr)
@ -310,13 +310,13 @@ func TestCreateClaims(t *testing.T) {
var accountTests = []struct { var accountTests = []struct {
name string name string
claims auth.Claims claims auth.Claims
req CreateAccountRequest req AccountCreateRequest
error error error error
}{ }{
// Internal request, should bypass ACL. // Internal request, should bypass ACL.
{"EmptyClaims", {"EmptyClaims",
auth.Claims{}, auth.Claims{},
CreateAccountRequest{ AccountCreateRequest{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -336,7 +336,7 @@ func TestCreateClaims(t *testing.T) {
Audience: "acc1", Audience: "acc1",
}, },
}, },
CreateAccountRequest{ AccountCreateRequest{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -356,7 +356,7 @@ func TestCreateClaims(t *testing.T) {
Audience: "acc1", Audience: "acc1",
}, },
}, },
CreateAccountRequest{ AccountCreateRequest{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -396,24 +396,24 @@ func TestUpdateValidation(t *testing.T) {
// TODO: actually create the account so can test the output of findbyId // TODO: actually create the account so can test the output of findbyId
type accountTest struct { type accountTest struct {
name string name string
req UpdateAccountRequest req AccountUpdateRequest
error error error error
} }
var accountTests = []accountTest{ var accountTests = []accountTest{
{"Required Fields", {"Required Fields",
UpdateAccountRequest{}, AccountUpdateRequest{},
errors.New("Key: 'UpdateAccountRequest.ID' Error:Field validation for 'ID' failed on the 'required' tag"), errors.New("Key: 'AccountUpdateRequest.ID' Error:Field validation for 'ID' failed on the 'required' tag"),
}, },
} }
invalidStatus := AccountStatus("xxxxxx") invalidStatus := AccountStatus("xxxxxx")
accountTests = append(accountTests, accountTest{"Valid Status", accountTests = append(accountTests, accountTest{"Valid Status",
UpdateAccountRequest{ AccountUpdateRequest{
ID: uuid.NewRandom().String(), ID: uuid.NewRandom().String(),
Status: &invalidStatus, 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) 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() ctx := tests.Context()
req1 := CreateAccountRequest{ req1 := AccountCreateRequest{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -474,7 +474,7 @@ func TestUpdateValidationNameUnique(t *testing.T) {
t.Fatalf("\t%s\tCreate failed.", tests.Failed) t.Fatalf("\t%s\tCreate failed.", tests.Failed)
} }
req2 := CreateAccountRequest{ req2 := AccountCreateRequest{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -490,11 +490,11 @@ func TestUpdateValidationNameUnique(t *testing.T) {
} }
// Try to set the email for account 1 on account 2 // Try to set the email for account 1 on account 2
updateReq := UpdateAccountRequest{ updateReq := AccountUpdateRequest{
ID: account2.ID, ID: account2.ID,
Name: &account1.Name, 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) err = Update(ctx, auth.Claims{}, test.MasterDB, updateReq, now)
if err == nil { if err == nil {
t.Logf("\t\tWant: %+v", expectedErr) t.Logf("\t\tWant: %+v", expectedErr)
@ -518,10 +518,10 @@ func TestCrud(t *testing.T) {
type accountTest struct { type accountTest struct {
name string name string
claims func(*Account, string) auth.Claims claims func(*Account, string) auth.Claims
create CreateAccountRequest create AccountCreateRequest
update func(*Account) UpdateAccountRequest update func(*Account) AccountUpdateRequest
updateErr error updateErr error
expected func(*Account, UpdateAccountRequest) *Account expected func(*Account, AccountUpdateRequest) *Account
findErr error findErr error
} }
@ -532,7 +532,7 @@ func TestCrud(t *testing.T) {
func(account *Account, userId string) auth.Claims { func(account *Account, userId string) auth.Claims {
return auth.Claims{} return auth.Claims{}
}, },
CreateAccountRequest{ AccountCreateRequest{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -541,15 +541,15 @@ func TestCrud(t *testing.T) {
Country: "USA", Country: "USA",
Zipcode: "99686", Zipcode: "99686",
}, },
func(account *Account) UpdateAccountRequest { func(account *Account) AccountUpdateRequest {
name := uuid.NewRandom().String() name := uuid.NewRandom().String()
return UpdateAccountRequest{ return AccountUpdateRequest{
ID: account.ID, ID: account.ID,
Name: &name, Name: &name,
} }
}, },
nil, nil,
func(account *Account, req UpdateAccountRequest) *Account { func(account *Account, req AccountUpdateRequest) *Account {
return &Account{ return &Account{
Name: *req.Name, Name: *req.Name,
// Copy this fields from the created account. // Copy this fields from the created account.
@ -583,7 +583,7 @@ func TestCrud(t *testing.T) {
}, },
} }
}, },
CreateAccountRequest{ AccountCreateRequest{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -592,15 +592,15 @@ func TestCrud(t *testing.T) {
Country: "USA", Country: "USA",
Zipcode: "99686", Zipcode: "99686",
}, },
func(account *Account) UpdateAccountRequest { func(account *Account) AccountUpdateRequest {
name := uuid.NewRandom().String() name := uuid.NewRandom().String()
return UpdateAccountRequest{ return AccountUpdateRequest{
ID: account.ID, ID: account.ID,
Name: &name, Name: &name,
} }
}, },
ErrForbidden, ErrForbidden,
func(account *Account, req UpdateAccountRequest) *Account { func(account *Account, req AccountUpdateRequest) *Account {
return account return account
}, },
ErrNotFound, ErrNotFound,
@ -617,7 +617,7 @@ func TestCrud(t *testing.T) {
}, },
} }
}, },
CreateAccountRequest{ AccountCreateRequest{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -626,15 +626,15 @@ func TestCrud(t *testing.T) {
Country: "USA", Country: "USA",
Zipcode: "99686", Zipcode: "99686",
}, },
func(account *Account) UpdateAccountRequest { func(account *Account) AccountUpdateRequest {
name := uuid.NewRandom().String() name := uuid.NewRandom().String()
return UpdateAccountRequest{ return AccountUpdateRequest{
ID: account.ID, ID: account.ID,
Name: &name, Name: &name,
} }
}, },
nil, nil,
func(account *Account, req UpdateAccountRequest) *Account { func(account *Account, req AccountUpdateRequest) *Account {
return &Account{ return &Account{
Name: *req.Name, Name: *req.Name,
// Copy this fields from the created account. // Copy this fields from the created account.
@ -668,7 +668,7 @@ func TestCrud(t *testing.T) {
}, },
} }
}, },
CreateAccountRequest{ AccountCreateRequest{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -677,15 +677,15 @@ func TestCrud(t *testing.T) {
Country: "USA", Country: "USA",
Zipcode: "99686", Zipcode: "99686",
}, },
func(account *Account) UpdateAccountRequest { func(account *Account) AccountUpdateRequest {
name := uuid.NewRandom().String() name := uuid.NewRandom().String()
return UpdateAccountRequest{ return AccountUpdateRequest{
ID: account.ID, ID: account.ID,
Name: &name, Name: &name,
} }
}, },
ErrForbidden, ErrForbidden,
func(account *Account, req UpdateAccountRequest) *Account { func(account *Account, req AccountUpdateRequest) *Account {
return nil return nil
}, },
ErrNotFound, ErrNotFound,
@ -702,7 +702,7 @@ func TestCrud(t *testing.T) {
}, },
} }
}, },
CreateAccountRequest{ AccountCreateRequest{
Name: uuid.NewRandom().String(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -711,15 +711,15 @@ func TestCrud(t *testing.T) {
Country: "USA", Country: "USA",
Zipcode: "99686", Zipcode: "99686",
}, },
func(account *Account) UpdateAccountRequest { func(account *Account) AccountUpdateRequest {
name := uuid.NewRandom().String() name := uuid.NewRandom().String()
return UpdateAccountRequest{ return AccountUpdateRequest{
ID: account.ID, ID: account.ID,
Name: &name, Name: &name,
} }
}, },
nil, nil,
func(account *Account, req UpdateAccountRequest) *Account { func(account *Account, req AccountUpdateRequest) *Account {
return &Account{ return &Account{
Name: *req.Name, Name: *req.Name,
// Copy this fields from the created account. // Copy this fields from the created account.
@ -846,7 +846,7 @@ func TestFind(t *testing.T) {
var accounts []*Account var accounts []*Account
for i := 0; i <= 4; i++ { 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(), Name: uuid.NewRandom().String(),
Address1: "103 East Main St", Address1: "103 East Main St",
Address2: "Unit 546", Address2: "Unit 546",
@ -990,12 +990,37 @@ func mockUserAccount(accountId, userId string, now time.Time, roles ...string) e
roleArr = append(roleArr, r) roleArr = append(roleArr, r)
} }
err := mockUser(userId, now)
if err != nil {
return err
}
// Build the insert SQL statement. // Build the insert SQL statement.
query := sqlbuilder.NewInsertBuilder() query := sqlbuilder.NewInsertBuilder()
query.InsertInto(userAccountTableName) query.InsertInto(userAccountTableName)
query.Cols("id", "user_id", "account_id", "roles", "created_at", "updated_at") query.Cols("id", "user_id", "account_id", "roles", "created_at", "updated_at")
query.Values(uuid.NewRandom().String(), userId, accountId, roleArr, now, now) 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. // Execute the query with the provided context.
sql, args := query.Build() sql, args := query.Build()
sql = test.MasterDB.Rebind(sql) sql = test.MasterDB.Rebind(sql)

View File

@ -92,7 +92,7 @@ func SaasWrapHandler(confs ...func(c *Config)) web.Handler {
if err != nil { if err != nil {
return web.NewRequestError(err, http.StatusInternalServerError) return web.NewRequestError(err, http.StatusInternalServerError)
} }
return web.RespondJson(ctx, w, doc, http.StatusOK) return web.RespondJson(ctx, w, []byte(doc), http.StatusOK)
default: default:
if strings.HasSuffix(path, ".html") { if strings.HasSuffix(path, ".html") {
w.Header().Set("Content-Type", "text/html; charset=utf-8") 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 := jwt.NewWithClaims(method, claims)
tkn.Header["kid"] = a.keyID tkn.Header["kid"] = a.keyID
str, err := tkn.SignedString(a.privateKey) str, err := tkn.SignedString(a.privateKey.PrivateKey)
if err != nil { if err != nil {
return "", errors.Wrap(err, "signing token") return "", errors.Wrap(err, "signing token")
} }

View File

@ -20,13 +20,77 @@ func TestMain(m *testing.M) {
} }
func testMain(m *testing.M) int { func testMain(m *testing.M) int {
tests.DisableDb = true
test = tests.New() test = tests.New()
defer test.TearDown() defer test.TearDown()
return m.Run() 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() awsSecretID := "jwt-key" + uuid.NewRandom().String()
@ -58,16 +122,16 @@ func TestAuthenticator(t *testing.T) {
Roles: []string{auth.RoleAdmin}, 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 { for i, tt := range authTests {
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name) 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 { if err != tt.error {
t.Log("\t\tGot :", err) t.Log("\t\tGot :", err)
t.Log("\t\tWant:", tt.error) 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) 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.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" const algorithm = "RS256"
// keyGen creates an x509 private key for signing auth tokens. // 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) key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil { if err != nil {
return []byte{}, errors.Wrap(err, "generating keys") 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. // Values used to format filename.
filePrefix := "auth_" filePrefix := "sassauth_"
fileExt := ".privatekey" fileExt := ".privatekey"
files, err := ioutil.ReadDir(localDir) 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 there are no keys or the current key needs to be rotated, generate a new key.
if len(keyContents) == 0 || curKeyId == "" { if len(keyContents) == 0 || curKeyId == "" {
privateKey, err := keyGen() privateKey, err := KeyGen()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to generate new private key") return nil, errors.Wrap(err, "failed to generate new private key")
} }
kID := uuid.NewRandom().String() 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) 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 // refreshed on instance launch. Could store keys in a kv store and update that value
// when new keys are generated // when new keys are generated
if len(keyContents) == 0 || curKeyId == "" { if len(keyContents) == 0 || curKeyId == "" {
privateKey, err := keyGen() privateKey, err := KeyGen()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to generate new private key") return nil, errors.Wrap(err, "failed to generate new private key")
} }

View File

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

View File

@ -65,8 +65,8 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
zipcode varchar(20) NOT NULL DEFAULT '', zipcode varchar(20) NOT NULL DEFAULT '',
status account_status_t NOT NULL DEFAULT 'active', status account_status_t NOT NULL DEFAULT 'active',
timezone varchar(128) NOT NULL DEFAULT 'America/Anchorage', timezone varchar(128) NOT NULL DEFAULT 'America/Anchorage',
signup_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), billing_user_id char(36) DEFAULT NULL REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
archived_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 // create new table user_accounts
{ {
ID: "20190522-01c", ID: "20190522-01d",
Migrate: func(tx *sql.Tx) error { Migrate: func(tx *sql.Tx) error {
q1 := `CREATE TYPE user_account_role_t as enum('admin', 'user')` q1 := `CREATE TYPE user_account_role_t as enum('admin', 'user')`
if _, err := tx.Exec(q1); err != nil { 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 ( q3 := `CREATE TABLE IF NOT EXISTS users_accounts (
id char(36) NOT NULL, 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 NO ACTION,
user_id char(36) NOT NULL REFERENCES users(id), user_id char(36) NOT NULL REFERENCES users(id) ON DELETE NO ACTION,
roles user_account_role_t[] NOT NULL, roles user_account_role_t[] NOT NULL,
status user_account_status_t NOT NULL DEFAULT 'active', status user_account_status_t NOT NULL DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE NOT NULL, 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 ( q2 := `CREATE TABLE IF NOT EXISTS projects (
id char(36) NOT NULL, 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, name varchar(255) NOT NULL,
status project_status_t NOT NULL DEFAULT 'active', status project_status_t NOT NULL DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE NOT NULL, 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 ( import (
"context" "context"
"crypto/rsa"
"github.com/dgrijalva/jwt-go"
"time" "time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth" "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 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 package user
import ( import (
"crypto/rsa"
"testing" "testing"
"time" "time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
"github.com/dgrijalva/jwt-go"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/pborman/uuid" "github.com/pborman/uuid"
"github.com/pkg/errors" "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. // TestAuthenticate validates the behavior around authenticating users.
func TestAuthenticate(t *testing.T) { func TestAuthenticate(t *testing.T) {
defer tests.Recover(t) defer tests.Recover(t)
@ -87,7 +21,7 @@ func TestAuthenticate(t *testing.T) {
{ {
ctx := tests.Context() ctx := tests.Context()
tknGen := &mockTokenGenerator{} tknGen := &MockTokenGenerator{}
// Auth tokens are valid for an our and is verified against current time. // Auth tokens are valid for an our and is verified against current time.
// Issue the token one hour ago. // Issue the token one hour ago.
@ -104,7 +38,7 @@ func TestAuthenticate(t *testing.T) {
// Create a new user for testing. // Create a new user for testing.
initPass := uuid.NewRandom().String() 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", Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: initPass, Password: initPass,
@ -132,7 +66,7 @@ func TestAuthenticate(t *testing.T) {
t.Fatalf("\t%s\tCreate user account failed.", tests.Failed) 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() account2Id := uuid.NewRandom().String()
err = mockAccount(account2Id, user.CreatedAt) err = mockAccount(account2Id, user.CreatedAt)
if err != nil { if err != nil {
@ -140,9 +74,11 @@ func TestAuthenticate(t *testing.T) {
t.Fatalf("\t%s\tCreate account failed.", tests.Failed) 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 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 { if err != nil {
t.Log("\t\tGot :", err) t.Log("\t\tGot :", err)
t.Fatalf("\t%s\tCreate user account failed.", tests.Failed) 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. // 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 := sqlbuilder.NewSelectBuilder().Select("id").From(userTableName)
query.Where(query.And( query.Where(query.And(
query.Equal("email", email), query.Equal("email", email),
@ -264,7 +264,7 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserCr
v := validator.New() v := validator.New()
// Validation email address is unique in the database. // 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 { if err != nil {
return nil, err 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. // Validation email address is unique in the database.
if req.Email != nil { 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 { if err != nil {
return err return err
} }
@ -614,22 +614,14 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userID str
return err return err
} }
// Build the delete SQL statement. // Start a new transaction to handle rollbacks on error.
query := sqlbuilder.NewDeleteBuilder() tx, err := dbConn.Begin()
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...)
if err != nil { if err != nil {
err = errors.Wrapf(err, "query - %s", query.String()) return errors.WithStack(err)
err = errors.WithMessagef(err, "delete user %s failed", req.ID)
return 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. // Build the delete SQL statement.
query := sqlbuilder.NewDeleteBuilder() 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. // Execute the query with the provided context.
sql, args := query.Build() sql, args := query.Build()
sql = dbConn.Rebind(sql) sql = dbConn.Rebind(sql)
_, err = dbConn.ExecContext(ctx, sql, args...) _, err = tx.ExecContext(ctx, sql, args...)
if err != nil { if err != nil {
tx.Rollback()
err = errors.Wrapf(err, "query - %s", query.String()) err = errors.Wrapf(err, "query - %s", query.String())
err = errors.WithMessagef(err, "delete accounts for user %s failed", req.ID) err = errors.WithMessagef(err, "delete accounts for user %s failed", req.ID)
return err 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 return nil
} }

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package user package user_account
import ( import (
"github.com/lib/pq" "github.com/lib/pq"
@ -212,6 +212,20 @@ func TestCreateValidation(t *testing.T) {
{ {
ctx := tests.Context() 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) res, err := Create(ctx, auth.Claims{}, test.MasterDB, tt.req, now)
if err != tt.error { if err != tt.error {
// TODO: need a better way to handle validation errors as they are // TODO: need a better way to handle validation errors as they are
@ -258,9 +272,25 @@ func TestCreateExistingEntry(t *testing.T) {
{ {
ctx := tests.Context() 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{ req1 := CreateUserAccountRequest{
UserID: uuid.NewRandom().String(), UserID: userID,
AccountID: uuid.NewRandom().String(), AccountID: accountID,
Roles: []UserAccountRole{UserAccountRole_User}, Roles: []UserAccountRole{UserAccountRole_User},
} }
ua1, err := Create(ctx, auth.Claims{}, test.MasterDB, req1, now) ua1, err := Create(ctx, auth.Claims{}, test.MasterDB, req1, now)
@ -503,9 +533,23 @@ func TestCrud(t *testing.T) {
for i, tt := range accountTests { for i, tt := range accountTests {
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name) 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() 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() 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{ createReq := CreateUserAccountRequest{
UserID: userID, UserID: userID,
AccountID: accountID, AccountID: accountID,
@ -656,9 +700,23 @@ func TestFind(t *testing.T) {
var userAccounts []*UserAccount var userAccounts []*UserAccount
for i := 0; i <= 4; i++ { 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() 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() 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{ ua, err := Create(tests.Context(), auth.Claims{}, test.MasterDB, CreateUserAccountRequest{
UserID: userID, UserID: userID,
AccountID: accountID, 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
}