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

Basic example cleanup

Rename sales-api to web-api and remove sales-admin
This commit is contained in:
Lee Brown
2019-05-16 18:05:39 -04:00
parent e6453bae45
commit b40d389579
25 changed files with 331 additions and 578 deletions

View File

@ -0,0 +1,38 @@
package handlers
import (
"context"
"net/http"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"go.opencensus.io/trace"
)
// Check provides support for orchestration health checks.
type Check struct {
MasterDB *db.DB
// ADD OTHER STATE LIKE THE LOGGER IF NEEDED.
}
// Health validates the service is healthy and ready to accept requests.
func (c *Check) Health(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.Check.Health")
defer span.End()
dbConn := c.MasterDB.Copy()
defer dbConn.Close()
if err := dbConn.StatusCheck(ctx); err != nil {
return err
}
status := struct {
Status string `json:"status"`
}{
Status: "ok",
}
return web.Respond(ctx, w, status, http.StatusOK)
}

View File

@ -0,0 +1,140 @@
package handlers
import (
"context"
"net/http"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/project"
"github.com/pkg/errors"
"go.opencensus.io/trace"
)
// Project represents the Project API method handler set.
type Project struct {
MasterDB *db.DB
// ADD OTHER STATE LIKE THE LOGGER IF NEEDED.
}
// List returns all the existing projects in the system.
func (p *Project) List(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.Project.List")
defer span.End()
dbConn := p.MasterDB.Copy()
defer dbConn.Close()
projects, err := project.List(ctx, dbConn)
if err != nil {
return err
}
return web.Respond(ctx, w, projects, http.StatusOK)
}
// Retrieve returns the specified project from the system.
func (p *Project) Retrieve(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.Project.Retrieve")
defer span.End()
dbConn := p.MasterDB.Copy()
defer dbConn.Close()
prod, err := project.Retrieve(ctx, dbConn, params["id"])
if err != nil {
switch err {
case project.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case project.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
default:
return errors.Wrapf(err, "ID: %s", params["id"])
}
}
return web.Respond(ctx, w, prod, http.StatusOK)
}
// Create inserts a new project into the system.
func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.Project.Create")
defer span.End()
dbConn := p.MasterDB.Copy()
defer dbConn.Close()
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
var np project.NewProject
if err := web.Decode(r, &np); err != nil {
return errors.Wrap(err, "")
}
nUsr, err := project.Create(ctx, dbConn, &np, v.Now)
if err != nil {
return errors.Wrapf(err, "Project: %+v", &np)
}
return web.Respond(ctx, w, nUsr, http.StatusCreated)
}
// Update updates the specified project in the system.
func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.Project.Update")
defer span.End()
dbConn := p.MasterDB.Copy()
defer dbConn.Close()
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
var up project.UpdateProject
if err := web.Decode(r, &up); err != nil {
return errors.Wrap(err, "")
}
err := project.Update(ctx, dbConn, params["id"], up, v.Now)
if err != nil {
switch err {
case project.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case project.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
default:
return errors.Wrapf(err, "ID: %s Update: %+v", params["id"], up)
}
}
return web.Respond(ctx, w, nil, http.StatusNoContent)
}
// Delete removes the specified project from the system.
func (p *Project) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.Project.Delete")
defer span.End()
dbConn := p.MasterDB.Copy()
defer dbConn.Close()
err := project.Delete(ctx, dbConn, params["id"])
if err != nil {
switch err {
case project.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case project.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
default:
return errors.Wrapf(err, "Id: %s", params["id"])
}
}
return web.Respond(ctx, w, nil, http.StatusNoContent)
}

View File

@ -0,0 +1,51 @@
package handlers
import (
"log"
"net/http"
"os"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
)
// API returns a handler for a set of routes.
func API(shutdown chan os.Signal, log *log.Logger, masterDB *db.DB, authenticator *auth.Authenticator) http.Handler {
// Construct the web.App which holds all routes as well as common Middleware.
app := web.NewApp(shutdown, log, mid.Logger(log), mid.Errors(log), mid.Metrics(), mid.Panics())
// Register health check endpoint. This route is not authenticated.
check := Check{
MasterDB: masterDB,
}
app.Handle("GET", "/v1/health", check.Health)
// Register user management and authentication endpoints.
u := User{
MasterDB: masterDB,
TokenGenerator: authenticator,
}
app.Handle("GET", "/v1/users", u.List, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("POST", "/v1/users", u.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/users/:id", u.Retrieve, mid.Authenticate(authenticator))
app.Handle("PUT", "/v1/users/:id", u.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/users/:id", u.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
// This route is not authenticated
app.Handle("GET", "/v1/users/token", u.Token)
// Register project and sale endpoints.
p := Project{
MasterDB: masterDB,
}
app.Handle("GET", "/v1/projects", p.List, mid.Authenticate(authenticator))
app.Handle("POST", "/v1/projects", p.Create, mid.Authenticate(authenticator))
app.Handle("GET", "/v1/projects/:id", p.Retrieve, mid.Authenticate(authenticator))
app.Handle("PUT", "/v1/projects/:id", p.Update, mid.Authenticate(authenticator))
app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.Authenticate(authenticator))
return app
}

View File

@ -0,0 +1,186 @@
package handlers
import (
"context"
"net/http"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
"github.com/pkg/errors"
"go.opencensus.io/trace"
)
// User represents the User API method handler set.
type User struct {
MasterDB *db.DB
TokenGenerator user.TokenGenerator
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
}
// List returns all the existing users in the system.
func (u *User) List(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.User.List")
defer span.End()
dbConn := u.MasterDB.Copy()
defer dbConn.Close()
usrs, err := user.List(ctx, dbConn)
if err != nil {
return err
}
return web.Respond(ctx, w, usrs, http.StatusOK)
}
// Retrieve returns the specified user from the system.
func (u *User) Retrieve(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.User.Retrieve")
defer span.End()
dbConn := u.MasterDB.Copy()
defer dbConn.Close()
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
usr, err := user.Retrieve(ctx, claims, dbConn, params["id"])
if err != nil {
switch err {
case user.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case user.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "Id: %s", params["id"])
}
}
return web.Respond(ctx, w, usr, http.StatusOK)
}
// Create inserts a new user into the system.
func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.User.Create")
defer span.End()
dbConn := u.MasterDB.Copy()
defer dbConn.Close()
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
var newU user.NewUser
if err := web.Decode(r, &newU); err != nil {
return errors.Wrap(err, "")
}
usr, err := user.Create(ctx, dbConn, &newU, v.Now)
if err != nil {
return errors.Wrapf(err, "User: %+v", &usr)
}
return web.Respond(ctx, w, usr, http.StatusCreated)
}
// Update updates the specified user in the system.
func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.User.Update")
defer span.End()
dbConn := u.MasterDB.Copy()
defer dbConn.Close()
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
var upd user.UpdateUser
if err := web.Decode(r, &upd); err != nil {
return errors.Wrap(err, "")
}
err := user.Update(ctx, dbConn, params["id"], &upd, v.Now)
if err != nil {
switch err {
case user.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case user.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "Id: %s User: %+v", params["id"], &upd)
}
}
return web.Respond(ctx, w, nil, http.StatusNoContent)
}
// Delete removes the specified user from the system.
func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.User.Delete")
defer span.End()
dbConn := u.MasterDB.Copy()
defer dbConn.Close()
err := user.Delete(ctx, dbConn, params["id"])
if err != nil {
switch err {
case user.ErrInvalidID:
return web.NewRequestError(err, http.StatusBadRequest)
case user.ErrNotFound:
return web.NewRequestError(err, http.StatusNotFound)
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
return errors.Wrapf(err, "Id: %s", params["id"])
}
}
return web.Respond(ctx, w, nil, http.StatusNoContent)
}
// Token handles a request to authenticate a user. It expects a request using
// Basic Auth with a user's email and password. It responds with a JWT.
func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.User.Token")
defer span.End()
dbConn := u.MasterDB.Copy()
defer dbConn.Close()
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
email, pass, ok := r.BasicAuth()
if !ok {
err := errors.New("must provide email and password in Basic auth")
return web.NewRequestError(err, http.StatusUnauthorized)
}
tkn, err := user.Authenticate(ctx, dbConn, u.TokenGenerator, v.Now, email, pass)
if err != nil {
switch err {
case user.ErrAuthenticationFailure:
return web.NewRequestError(err, http.StatusUnauthorized)
default:
return errors.Wrap(err, "authenticating")
}
}
return web.Respond(ctx, w, tkn, http.StatusOK)
}

View File

@ -0,0 +1,228 @@
package main
import (
"context"
"crypto/rsa"
"encoding/json"
"expvar"
"io/ioutil"
"log"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"syscall"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/handlers"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
itrace "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/trace"
jwt "github.com/dgrijalva/jwt-go"
"github.com/kelseyhightower/envconfig"
"go.opencensus.io/trace"
)
/*
ZipKin: http://localhost:9411
AddLoad: hey -m GET -c 10 -n 10000 "http://localhost:3000/v1/users"
expvarmon -ports=":3001" -endpoint="/metrics" -vars="requests,goroutines,errors,mem:memstats.Alloc"
*/
/*
Need to figure out timeouts for http service.
You might want to reset your DB_HOST env var during test tear down.
Service should start even without a DB running yet.
symbols in profiles: https://github.com/golang/go/issues/23376 / https://github.com/google/pprof/pull/366
*/
// build is the git version of this program. It is set using build flags in the makefile.
var build = "develop"
func main() {
// =========================================================================
// Logging
log := log.New(os.Stdout, "WEB_APP : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
// =========================================================================
// Configuration
var cfg struct {
Web struct {
APIHost string `default:"0.0.0.0:3000" envconfig:"API_HOST"`
DebugHost string `default:"0.0.0.0:4000" envconfig:"DEBUG_HOST"`
ReadTimeout time.Duration `default:"5s" envconfig:"READ_TIMEOUT"`
WriteTimeout time.Duration `default:"5s" envconfig:"WRITE_TIMEOUT"`
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
}
DB struct {
DialTimeout time.Duration `default:"5s" envconfig:"DIAL_TIMEOUT"`
Host string `default:"mongo:27017/gotraining" envconfig:"HOST"`
}
Trace struct {
Host string `default:"http://tracer:3002/v1/publish" envconfig:"HOST"`
BatchSize int `default:"1000" envconfig:"BATCH_SIZE"`
SendInterval time.Duration `default:"15s" envconfig:"SEND_INTERVAL"`
SendTimeout time.Duration `default:"500ms" envconfig:"SEND_TIMEOUT"`
}
Auth struct {
KeyID string `envconfig:"KEY_ID"`
PrivateKeyFile string `default:"/app/private.pem" envconfig:"PRIVATE_KEY_FILE"`
Algorithm string `default:"RS256" envconfig:"ALGORITHM"`
}
}
if err := envconfig.Process("WEB_APP", &cfg); err != nil {
log.Fatalf("main : Parsing Config : %v", err)
}
if err := flag.Process(&cfg); err != nil {
if err != flag.ErrHelp {
log.Fatalf("main : Parsing Command Line : %v", err)
}
return // We displayed help.
}
// =========================================================================
// App Starting
// Print the build version for our logs. Also expose it under /debug/vars.
expvar.NewString("build").Set(build)
log.Printf("main : Started : Application Initializing version %q", build)
defer log.Println("main : Completed")
cfgJSON, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
log.Fatalf("main : Marshalling Config to JSON : %v", err)
}
// TODO: Validate what is being written to the logs. We don't
// want to leak credentials or anything that can be a security risk.
log.Printf("main : Config : %v\n", string(cfgJSON))
// =========================================================================
// Find auth keys
keyContents, err := ioutil.ReadFile(cfg.Auth.PrivateKeyFile)
if err != nil {
log.Fatalf("main : Reading auth private key : %v", err)
}
key, err := jwt.ParseRSAPrivateKeyFromPEM(keyContents)
if err != nil {
log.Fatalf("main : Parsing auth private key : %v", err)
}
publicKeyLookup := auth.NewSingleKeyFunc(cfg.Auth.KeyID, key.Public().(*rsa.PublicKey))
authenticator, err := auth.NewAuthenticator(key, cfg.Auth.KeyID, cfg.Auth.Algorithm, publicKeyLookup)
if err != nil {
log.Fatalf("main : Constructing authenticator : %v", err)
}
// =========================================================================
// Start Mongo
log.Println("main : Started : Initialize Mongo")
masterDB, err := db.New(cfg.DB.Host, cfg.DB.DialTimeout)
if err != nil {
log.Fatalf("main : Register DB : %v", err)
}
defer masterDB.Close()
// =========================================================================
// Start Tracing Support
logger := func(format string, v ...interface{}) {
log.Printf(format, v...)
}
log.Printf("main : Tracing Started : %s", cfg.Trace.Host)
exporter, err := itrace.NewExporter(logger, cfg.Trace.Host, cfg.Trace.BatchSize, cfg.Trace.SendInterval, cfg.Trace.SendTimeout)
if err != nil {
log.Fatalf("main : RegiTracingster : ERROR : %v", err)
}
defer func() {
log.Printf("main : Tracing Stopping : %s", cfg.Trace.Host)
batch, err := exporter.Close()
if err != nil {
log.Printf("main : Tracing Stopped : ERROR : Batch[%d] : %v", batch, err)
} else {
log.Printf("main : Tracing Stopped : Flushed Batch[%d]", batch)
}
}()
trace.RegisterExporter(exporter)
trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
// =========================================================================
// Start Debug Service. Not concerned with shutting this down when the
// application is being shutdown.
//
// /debug/vars - Added to the default mux by the expvars package.
// /debug/pprof - Added to the default mux by the net/http/pprof package.
go func() {
log.Printf("main : Debug Listening %s", cfg.Web.DebugHost)
log.Printf("main : Debug Listener closed : %v", http.ListenAndServe(cfg.Web.DebugHost, http.DefaultServeMux))
}()
// =========================================================================
// Start API Service
// Make a channel to listen for an interrupt or terminate signal from the OS.
// Use a buffered channel because the signal package requires it.
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
api := http.Server{
Addr: cfg.Web.APIHost,
Handler: handlers.API(shutdown, log, masterDB, authenticator),
ReadTimeout: cfg.Web.ReadTimeout,
WriteTimeout: cfg.Web.WriteTimeout,
MaxHeaderBytes: 1 << 20,
}
// Make a channel to listen for errors coming from the listener. Use a
// buffered channel so the goroutine can exit if we don't collect this error.
serverErrors := make(chan error, 1)
// Start the service listening for requests.
go func() {
log.Printf("main : API Listening %s", cfg.Web.APIHost)
serverErrors <- api.ListenAndServe()
}()
// =========================================================================
// Shutdown
// Blocking main and waiting for shutdown.
select {
case err := <-serverErrors:
log.Fatalf("main : Error starting server: %v", err)
case sig := <-shutdown:
log.Printf("main : %v : Start shutdown..", sig)
// Create context for Shutdown call.
ctx, cancel := context.WithTimeout(context.Background(), cfg.Web.ShutdownTimeout)
defer cancel()
// Asking listener to shutdown and load shed.
err := api.Shutdown(ctx)
if err != nil {
log.Printf("main : Graceful shutdown did not complete in %v : %v", cfg.Web.ShutdownTimeout, err)
err = api.Close()
}
// Log the status of this shutdown.
switch {
case sig == syscall.SIGSTOP:
log.Fatal("main : Integrity issue caused shutdown")
case err != nil:
log.Fatalf("main : Could not stop server gracefully : %v", err)
}
}
}

View File

@ -0,0 +1,447 @@
package tests
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/project"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"gopkg.in/mgo.v2/bson"
)
// TestProjects is the entry point for the projects
func TestProjects(t *testing.T) {
defer tests.Recover(t)
t.Run("getProjects200Empty", getProjects200Empty)
t.Run("postProject400", postProject400)
t.Run("postProject401", postProject401)
t.Run("getProject404", getProject404)
t.Run("getProject400", getProject400)
t.Run("deleteProject404", deleteProject404)
t.Run("putProject404", putProject404)
t.Run("crudProjects", crudProject)
}
// getProjects200Empty validates an empty projects list can be retrieved with the endpoint.
func getProjects200Empty(t *testing.T) {
r := httptest.NewRequest("GET", "/v1/projects", nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to fetch an empty list of projects with the projects endpoint.")
{
t.Log("\tTest 0:\tWhen fetching an empty project list.")
{
if w.Code != http.StatusOK {
t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success)
recv := w.Body.String()
resp := `[]`
if resp != recv {
t.Log("Got :", recv)
t.Log("Want:", resp)
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
}
// postProject400 validates a project can't be created with the endpoint
// unless a valid project document is submitted.
func postProject400(t *testing.T) {
r := httptest.NewRequest("POST", "/v1/projects", strings.NewReader(`{}`))
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate a new project can't be created with an invalid document.")
{
t.Log("\tTest 0:\tWhen using an incomplete project value.")
{
if w.Code != http.StatusBadRequest {
t.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 400 for the response.", tests.Success)
// Inspect the response.
var got web.ErrorResponse
if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
t.Fatalf("\t%s\tShould be able to unmarshal the response to an error type : %v", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to unmarshal the response to an error type.", tests.Success)
// Define what we want to see.
want := web.ErrorResponse{
Error: "field validation error",
Fields: []web.FieldError{
{Field: "name", Error: "name is a required field"},
{Field: "cost", Error: "cost is a required field"},
{Field: "quantity", Error: "quantity is a required field"},
},
}
// We can't rely on the order of the field errors so they have to be
// sorted. Tell the cmp package how to sort them.
sorter := cmpopts.SortSlices(func(a, b web.FieldError) bool {
return a.Field < b.Field
})
if diff := cmp.Diff(want, got, sorter); diff != "" {
t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
}
// postProject401 validates a project can't be created with the endpoint
// unless the user is authenticated
func postProject401(t *testing.T) {
np := project.NewProject{
Name: "Comic Books",
Cost: 25,
Quantity: 60,
}
body, err := json.Marshal(&np)
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest("POST", "/v1/projects", bytes.NewBuffer(body))
w := httptest.NewRecorder()
// Not setting an authorization header
a.ServeHTTP(w, r)
t.Log("Given the need to validate a new project can't be created with an invalid document.")
{
t.Log("\tTest 0:\tWhen using an incomplete project value.")
{
if w.Code != http.StatusUnauthorized {
t.Fatalf("\t%s\tShould receive a status code of 401 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 401 for the response.", tests.Success)
}
}
}
// getProject400 validates a project request for a malformed id.
func getProject400(t *testing.T) {
id := "12345"
r := httptest.NewRequest("GET", "/v1/projects/"+id, nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate getting a project with a malformed id.")
{
t.Logf("\tTest 0:\tWhen using the new project %s.", id)
{
if w.Code != http.StatusBadRequest {
t.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 400 for the response.", tests.Success)
recv := w.Body.String()
resp := `{"error":"ID is not in its proper form"}`
if resp != recv {
t.Log("Got :", recv)
t.Log("Want:", resp)
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
}
// getProject404 validates a project request for a project that does not exist with the endpoint.
func getProject404(t *testing.T) {
id := bson.NewObjectId().Hex()
r := httptest.NewRequest("GET", "/v1/projects/"+id, nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate getting a project with an unknown id.")
{
t.Logf("\tTest 0:\tWhen using the new project %s.", id)
{
if w.Code != http.StatusNotFound {
t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success)
recv := w.Body.String()
resp := "Entity not found"
if !strings.Contains(recv, resp) {
t.Log("Got :", recv)
t.Log("Want:", resp)
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
}
// deleteProject404 validates deleting a project that does not exist.
func deleteProject404(t *testing.T) {
id := bson.NewObjectId().Hex()
r := httptest.NewRequest("DELETE", "/v1/projects/"+id, nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate deleting a project that does not exist.")
{
t.Logf("\tTest 0:\tWhen using the new project %s.", id)
{
if w.Code != http.StatusNotFound {
t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success)
recv := w.Body.String()
resp := "Entity not found"
if !strings.Contains(recv, resp) {
t.Log("Got :", recv)
t.Log("Want:", resp)
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
}
// putProject404 validates updating a project that does not exist.
func putProject404(t *testing.T) {
up := project.UpdateProject{
Name: tests.StringPointer("Nonexistent"),
}
id := bson.NewObjectId().Hex()
body, err := json.Marshal(&up)
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest("PUT", "/v1/projects/"+id, bytes.NewBuffer(body))
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate updating a project that does not exist.")
{
t.Logf("\tTest 0:\tWhen using the new project %s.", id)
{
if w.Code != http.StatusNotFound {
t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success)
recv := w.Body.String()
resp := "Entity not found"
if !strings.Contains(recv, resp) {
t.Log("Got :", recv)
t.Log("Want:", resp)
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
}
// crudProject performs a complete test of CRUD against the api.
func crudProject(t *testing.T) {
p := postProject201(t)
defer deleteProject204(t, p.ID.Hex())
getProject200(t, p.ID.Hex())
putProject204(t, p.ID.Hex())
}
// postProject201 validates a project can be created with the endpoint.
func postProject201(t *testing.T) project.Project {
np := project.NewProject{
Name: "Comic Books",
Cost: 25,
Quantity: 60,
}
body, err := json.Marshal(&np)
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest("POST", "/v1/projects", bytes.NewBuffer(body))
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
// p is the value we will return.
var p project.Project
t.Log("Given the need to create a new project with the projects endpoint.")
{
t.Log("\tTest 0:\tWhen using the declared project value.")
{
if w.Code != http.StatusCreated {
t.Fatalf("\t%s\tShould receive a status code of 201 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 201 for the response.", tests.Success)
if err := json.NewDecoder(w.Body).Decode(&p); err != nil {
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
}
// Define what we wanted to receive. We will just trust the generated
// fields like ID and Dates so we copy p.
want := p
want.Name = "Comic Books"
want.Cost = 25
want.Quantity = 60
if diff := cmp.Diff(want, p); diff != "" {
t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
return p
}
// deleteProject200 validates deleting a project that does exist.
func deleteProject204(t *testing.T, id string) {
r := httptest.NewRequest("DELETE", "/v1/projects/"+id, nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate deleting a project that does exist.")
{
t.Logf("\tTest 0:\tWhen using the new project %s.", id)
{
if w.Code != http.StatusNoContent {
t.Fatalf("\t%s\tShould receive a status code of 204 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 204 for the response.", tests.Success)
}
}
}
// getProject200 validates a project request for an existing id.
func getProject200(t *testing.T, id string) {
r := httptest.NewRequest("GET", "/v1/projects/"+id, nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate getting a project that exists.")
{
t.Logf("\tTest 0:\tWhen using the new project %s.", id)
{
if w.Code != http.StatusOK {
t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success)
var p project.Project
if err := json.NewDecoder(w.Body).Decode(&p); err != nil {
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
}
// Define what we wanted to receive. We will just trust the generated
// fields like Dates so we copy p.
want := p
want.ID = bson.ObjectIdHex(id)
want.Name = "Comic Books"
want.Cost = 25
want.Quantity = 60
if diff := cmp.Diff(want, p); diff != "" {
t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
}
// putProject204 validates updating a project that does exist.
func putProject204(t *testing.T, id string) {
body := `{"name": "Graphic Novels", "cost": 100}`
r := httptest.NewRequest("PUT", "/v1/projects/"+id, strings.NewReader(body))
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to update a project with the projects endpoint.")
{
t.Log("\tTest 0:\tWhen using the modified project value.")
{
if w.Code != http.StatusNoContent {
t.Fatalf("\t%s\tShould receive a status code of 204 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 204 for the response.", tests.Success)
r = httptest.NewRequest("GET", "/v1/projects/"+id, nil)
w = httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("\t%s\tShould receive a status code of 200 for the retrieve : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 200 for the retrieve.", tests.Success)
var ru project.Project
if err := json.NewDecoder(w.Body).Decode(&ru); err != nil {
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
}
if ru.Name != "Graphic Novels" {
t.Fatalf("\t%s\tShould see an updated Name : got %q want %q", tests.Failed, ru.Name, "Graphic Novels")
}
t.Logf("\t%s\tShould see an updated Name.", tests.Success)
}
}
}

View File

@ -0,0 +1,97 @@
package tests
import (
"crypto/rand"
"crypto/rsa"
"net/http"
"os"
"testing"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/handlers"
"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/user"
)
var a http.Handler
var test *tests.Test
// Information about the users we have created for testing.
var adminAuthorization string
var adminID string
var userAuthorization string
var userID string
// 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()
// Create RSA keys to enable authentication in our service.
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
kid := "4754d86b-7a6d-4df5-9c65-224741361492"
kf := auth.NewSingleKeyFunc(kid, key.Public().(*rsa.PublicKey))
authenticator, err := auth.NewAuthenticator(key, kid, "RS256", kf)
if err != nil {
panic(err)
}
shutdown := make(chan os.Signal, 1)
a = handlers.API(shutdown, test.Log, test.MasterDB, authenticator)
// Create an admin user directly with our business logic. This creates an
// initial user that we will use for admin validated endpoints.
nu := user.NewUser{
Email: "admin@ardanlabs.com",
Name: "Admin User",
Roles: []string{auth.RoleAdmin, auth.RoleUser},
Password: "gophers",
PasswordConfirm: "gophers",
}
admin, err := user.Create(tests.Context(), test.MasterDB, &nu, time.Now())
if err != nil {
panic(err)
}
adminID = admin.ID.Hex()
tkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, time.Now(), nu.Email, nu.Password)
if err != nil {
panic(err)
}
adminAuthorization = "Bearer " + tkn.Token
// Create a regular user to use when calling regular validated endpoints.
nu = user.NewUser{
Email: "user@ardanlabs.com",
Name: "Regular User",
Roles: []string{auth.RoleUser},
Password: "concurrency",
PasswordConfirm: "concurrency",
}
usr, err := user.Create(tests.Context(), test.MasterDB, &nu, time.Now())
if err != nil {
panic(err)
}
userID = usr.ID.Hex()
tkn, err = user.Authenticate(tests.Context(), test.MasterDB, authenticator, time.Now(), nu.Email, nu.Password)
if err != nil {
panic(err)
}
userAuthorization = "Bearer " + tkn.Token
return m.Run()
}

View File

@ -0,0 +1,576 @@
package tests
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"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/web"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"gopkg.in/mgo.v2/bson"
)
// TestUsers is the entry point for testing user management functions.
func TestUsers(t *testing.T) {
defer tests.Recover(t)
t.Run("getToken401", getToken401)
t.Run("getToken200", getToken200)
t.Run("postUser400", postUser400)
t.Run("postUser401", postUser401)
t.Run("postUser403", postUser403)
t.Run("getUser400", getUser400)
t.Run("getUser403", getUser403)
t.Run("getUser404", getUser404)
t.Run("deleteUser404", deleteUser404)
t.Run("putUser404", putUser404)
t.Run("crudUsers", crudUser)
}
// getToken401 ensures an unknown user can't generate a token.
func getToken401(t *testing.T) {
r := httptest.NewRequest("GET", "/v1/users/token", nil)
w := httptest.NewRecorder()
r.SetBasicAuth("unknown@example.com", "some-password")
a.ServeHTTP(w, r)
t.Log("Given the need to deny tokens to unknown users.")
{
t.Log("\tTest 0:\tWhen fetching a token with an unrecognized email.")
{
if w.Code != http.StatusUnauthorized {
t.Fatalf("\t%s\tShould receive a status code of 401 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 401 for the response.", tests.Success)
}
}
}
// getToken200
func getToken200(t *testing.T) {
r := httptest.NewRequest("GET", "/v1/users/token", nil)
w := httptest.NewRecorder()
r.SetBasicAuth("admin@ardanlabs.com", "gophers")
a.ServeHTTP(w, r)
t.Log("Given the need to issues tokens to known users.")
{
t.Log("\tTest 0:\tWhen fetching a token with valid credentials.")
{
if w.Code != http.StatusOK {
t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success)
var got user.Token
if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to unmarshal the response.", tests.Success)
// TODO(jlw) Should we ensure the token is valid?
}
}
}
// postUser400 validates a user can't be created with the endpoint
// unless a valid user document is submitted.
func postUser400(t *testing.T) {
body, err := json.Marshal(&user.NewUser{})
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest("POST", "/v1/users", bytes.NewBuffer(body))
w := httptest.NewRecorder()
r.Header.Set("Authorization", adminAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate a new user can't be created with an invalid document.")
{
t.Log("\tTest 0:\tWhen using an incomplete user value.")
{
if w.Code != http.StatusBadRequest {
t.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 400 for the response.", tests.Success)
// Inspect the response.
var got web.ErrorResponse
if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
t.Fatalf("\t%s\tShould be able to unmarshal the response to an error type : %v", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to unmarshal the response to an error type.", tests.Success)
// Define what we want to see.
want := web.ErrorResponse{
Error: "field validation error",
Fields: []web.FieldError{
{Field: "name", Error: "name is a required field"},
{Field: "email", Error: "email is a required field"},
{Field: "roles", Error: "roles is a required field"},
{Field: "password", Error: "password is a required field"},
},
}
// We can't rely on the order of the field errors so they have to be
// sorted. Tell the cmp package how to sort them.
sorter := cmpopts.SortSlices(func(a, b web.FieldError) bool {
return a.Field < b.Field
})
if diff := cmp.Diff(want, got, sorter); diff != "" {
t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
}
// postUser401 validates a user can't be created unless the calling user is
// authenticated.
func postUser401(t *testing.T) {
body, err := json.Marshal(&user.User{})
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest("POST", "/v1/users", bytes.NewBuffer(body))
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate a new user can't be created with an invalid document.")
{
t.Log("\tTest 0:\tWhen using an incomplete user value.")
{
if w.Code != http.StatusForbidden {
t.Fatalf("\t%s\tShould receive a status code of 403 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 403 for the response.", tests.Success)
}
}
}
// postUser403 validates a user can't be created unless the calling user is
// an admin user. Regular users can't do this.
func postUser403(t *testing.T) {
body, err := json.Marshal(&user.User{})
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest("POST", "/v1/users", bytes.NewBuffer(body))
w := httptest.NewRecorder()
// Not setting the Authorization header
a.ServeHTTP(w, r)
t.Log("Given the need to validate a new user can't be created with an invalid document.")
{
t.Log("\tTest 0:\tWhen using an incomplete user value.")
{
if w.Code != http.StatusUnauthorized {
t.Fatalf("\t%s\tShould receive a status code of 401 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 401 for the response.", tests.Success)
}
}
}
// getUser400 validates a user request for a malformed userid.
func getUser400(t *testing.T) {
id := "12345"
r := httptest.NewRequest("GET", "/v1/users/"+id, nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", adminAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate getting a user with a malformed userid.")
{
t.Logf("\tTest 0:\tWhen using the new user %s.", id)
{
if w.Code != http.StatusBadRequest {
t.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 400 for the response.", tests.Success)
recv := w.Body.String()
resp := `{"error":"ID is not in its proper form"}`
if resp != recv {
t.Log("Got :", recv)
t.Log("Want:", resp)
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
}
// getUser403 validates a regular user can't fetch anyone but themselves
func getUser403(t *testing.T) {
t.Log("Given the need to validate regular users can't fetch other users.")
{
t.Logf("\tTest 0:\tWhen fetching the admin user as a regular user.")
{
r := httptest.NewRequest("GET", "/v1/users/"+adminID, nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
if w.Code != http.StatusForbidden {
t.Fatalf("\t%s\tShould receive a status code of 403 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 403 for the response.", tests.Success)
recv := w.Body.String()
resp := `{"error":"Attempted action is not allowed"}`
if resp != recv {
t.Log("Got :", recv)
t.Log("Want:", resp)
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
t.Logf("\tTest 1:\tWhen fetching the user as a themselves.")
{
r := httptest.NewRequest("GET", "/v1/users/"+userID, nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success)
}
}
}
// getUser404 validates a user request for a user that does not exist with the endpoint.
func getUser404(t *testing.T) {
id := bson.NewObjectId().Hex()
r := httptest.NewRequest("GET", "/v1/users/"+id, nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", adminAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate getting a user with an unknown id.")
{
t.Logf("\tTest 0:\tWhen using the new user %s.", id)
{
if w.Code != http.StatusNotFound {
t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success)
recv := w.Body.String()
resp := "Entity not found"
if !strings.Contains(recv, resp) {
t.Log("Got :", recv)
t.Log("Want:", resp)
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
}
// deleteUser404 validates deleting a user that does not exist.
func deleteUser404(t *testing.T) {
id := bson.NewObjectId().Hex()
r := httptest.NewRequest("DELETE", "/v1/users/"+id, nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", adminAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate deleting a user that does not exist.")
{
t.Logf("\tTest 0:\tWhen using the new user %s.", id)
{
if w.Code != http.StatusNotFound {
t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success)
recv := w.Body.String()
resp := "Entity not found"
if !strings.Contains(recv, resp) {
t.Log("Got :", recv)
t.Log("Want:", resp)
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
}
// putUser404 validates updating a user that does not exist.
func putUser404(t *testing.T) {
u := user.UpdateUser{
Name: tests.StringPointer("Doesn't Exist"),
}
id := bson.NewObjectId().Hex()
body, err := json.Marshal(&u)
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest("PUT", "/v1/users/"+id, bytes.NewBuffer(body))
w := httptest.NewRecorder()
r.Header.Set("Authorization", adminAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate updating a user that does not exist.")
{
t.Logf("\tTest 0:\tWhen using the new user %s.", id)
{
if w.Code != http.StatusNotFound {
t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success)
recv := w.Body.String()
resp := "Entity not found"
if !strings.Contains(recv, resp) {
t.Log("Got :", recv)
t.Log("Want:", resp)
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
}
// crudUser performs a complete test of CRUD against the api.
func crudUser(t *testing.T) {
nu := postUser201(t)
defer deleteUser204(t, nu.ID.Hex())
getUser200(t, nu.ID.Hex())
putUser204(t, nu.ID.Hex())
putUser403(t, nu.ID.Hex())
}
// postUser201 validates a user can be created with the endpoint.
func postUser201(t *testing.T) user.User {
nu := user.NewUser{
Name: "Bill Kennedy",
Email: "bill@ardanlabs.com",
Roles: []string{auth.RoleAdmin},
Password: "gophers",
PasswordConfirm: "gophers",
}
body, err := json.Marshal(&nu)
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest("POST", "/v1/users", bytes.NewBuffer(body))
w := httptest.NewRecorder()
r.Header.Set("Authorization", adminAuthorization)
a.ServeHTTP(w, r)
// u is the value we will return.
var u user.User
t.Log("Given the need to create a new user with the users endpoint.")
{
t.Log("\tTest 0:\tWhen using the declared user value.")
{
if w.Code != http.StatusCreated {
t.Fatalf("\t%s\tShould receive a status code of 201 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 201 for the response.", tests.Success)
if err := json.NewDecoder(w.Body).Decode(&u); err != nil {
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
}
// Define what we wanted to receive. We will just trust the generated
// fields like ID and Dates so we copy u.
want := u
want.Name = "Bill Kennedy"
want.Email = "bill@ardanlabs.com"
want.Roles = []string{auth.RoleAdmin}
if diff := cmp.Diff(want, u); diff != "" {
t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
return u
}
// deleteUser200 validates deleting a user that does exist.
func deleteUser204(t *testing.T, id string) {
r := httptest.NewRequest("DELETE", "/v1/users/"+id, nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", adminAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate deleting a user that does exist.")
{
t.Logf("\tTest 0:\tWhen using the new user %s.", id)
{
if w.Code != http.StatusNoContent {
t.Fatalf("\t%s\tShould receive a status code of 204 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 204 for the response.", tests.Success)
}
}
}
// getUser200 validates a user request for an existing userid.
func getUser200(t *testing.T, id string) {
r := httptest.NewRequest("GET", "/v1/users/"+id, nil)
w := httptest.NewRecorder()
r.Header.Set("Authorization", adminAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to validate getting a user that exsits.")
{
t.Logf("\tTest 0:\tWhen using the new user %s.", id)
{
if w.Code != http.StatusOK {
t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success)
var u user.User
if err := json.NewDecoder(w.Body).Decode(&u); err != nil {
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
}
// Define what we wanted to receive. We will just trust the generated
// fields like Dates so we copy p.
want := u
want.ID = bson.ObjectIdHex(id)
want.Name = "Bill Kennedy"
want.Email = "bill@ardanlabs.com"
want.Roles = []string{auth.RoleAdmin}
if diff := cmp.Diff(want, u); diff != "" {
t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get the expected result.", tests.Success)
}
}
}
// putUser204 validates updating a user that does exist.
func putUser204(t *testing.T, id string) {
body := `{"name": "Jacob Walker"}`
r := httptest.NewRequest("PUT", "/v1/users/"+id, strings.NewReader(body))
w := httptest.NewRecorder()
r.Header.Set("Authorization", adminAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to update a user with the users endpoint.")
{
t.Log("\tTest 0:\tWhen using the modified user value.")
{
if w.Code != http.StatusNoContent {
t.Fatalf("\t%s\tShould receive a status code of 204 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 204 for the response.", tests.Success)
r = httptest.NewRequest("GET", "/v1/users/"+id, nil)
w = httptest.NewRecorder()
r.Header.Set("Authorization", adminAuthorization)
a.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("\t%s\tShould receive a status code of 200 for the retrieve : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 200 for the retrieve.", tests.Success)
var ru user.User
if err := json.NewDecoder(w.Body).Decode(&ru); err != nil {
t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
}
if ru.Name != "Jacob Walker" {
t.Fatalf("\t%s\tShould see an updated Name : got %q want %q", tests.Failed, ru.Name, "Jacob Walker")
}
t.Logf("\t%s\tShould see an updated Name.", tests.Success)
if ru.Email != "bill@ardanlabs.com" {
t.Fatalf("\t%s\tShould not affect other fields like Email : got %q want %q", tests.Failed, ru.Email, "bill@ardanlabs.com")
}
t.Logf("\t%s\tShould not affect other fields like Email.", tests.Success)
}
}
}
// putUser403 validates that a user can't modify users unless they are an admin.
func putUser403(t *testing.T, id string) {
body := `{"name": "Anna Walker"}`
r := httptest.NewRequest("PUT", "/v1/users/"+id, strings.NewReader(body))
w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r)
t.Log("Given the need to update a user with the users endpoint.")
{
t.Log("\tTest 0:\tWhen a non-admin user makes a request")
{
if w.Code != http.StatusForbidden {
t.Fatalf("\t%s\tShould receive a status code of 403 for the response : %v", tests.Failed, w.Code)
}
t.Logf("\t%s\tShould receive a status code of 403 for the response.", tests.Success)
}
}
}