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

1
example-project/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
private.pem

View File

@ -105,7 +105,7 @@ Running `make down` will properly stop and terminate the Docker Compose session.
## About The Project ## About The Project
The service provides record keeping for someone running a multi-family garage sale. Authenticated users can maintain a list of products for sale. The service provides record keeping for someone running a multi-family garage sale. Authenticated users can maintain a list of projects for sale.
<!--The service uses the following models:--> <!--The service uses the following models:-->

View File

@ -1,137 +0,0 @@
// This program performs administrative tasks for the garage sale service.
//
// Run it with --cmd keygen or --cmd useradd
package main
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"log"
"os"
"time"
"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"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
"github.com/kelseyhightower/envconfig"
"github.com/pkg/errors"
)
func main() {
// =========================================================================
// Logging
log := log.New(os.Stdout, "sales-admin : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
// =========================================================================
// Configuration
var cfg struct {
CMD string `envconfig:"CMD"`
DB struct {
DialTimeout time.Duration `default:"5s" envconfig:"DIAL_TIMEOUT"`
Host string `default:"localhost:27017/gotraining" envconfig:"HOST"`
}
Auth struct {
PrivateKeyFile string `default:"private.pem" envconfig:"PRIVATE_KEY_FILE"`
}
User struct {
Email string
Password string
}
}
if err := envconfig.Process("SALES", &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.
}
var err error
switch cfg.CMD {
case "keygen":
err = keygen(cfg.Auth.PrivateKeyFile)
case "useradd":
err = useradd(cfg.DB.Host, cfg.DB.DialTimeout, cfg.User.Email, cfg.User.Password)
default:
err = errors.New("Must provide --cmd keygen or --cmd useradd")
}
if err != nil {
log.Fatal(err)
}
}
// keygen creates an x509 private key for signing auth tokens.
func keygen(path string) error {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return errors.Wrap(err, "generating keys")
}
file, err := os.Create(path)
if err != nil {
return errors.Wrap(err, "creating private file")
}
block := pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
}
if err := pem.Encode(file, &block); err != nil {
return errors.Wrap(err, "encoding to private file")
}
if err := file.Close(); err != nil {
return errors.Wrap(err, "closing private file")
}
return nil
}
func useradd(dbHost string, dbTimeout time.Duration, email, pass string) error {
dbConn, err := db.New(dbHost, dbTimeout)
if err != nil {
return err
}
defer dbConn.Close()
if email == "" {
return errors.New("Must provide --user_email")
}
if pass == "" {
return errors.New("Must provide --user_password or set the env var SALES_USER_PASSWORD")
}
ctx := context.Background()
newU := user.NewUser{
Email: email,
Password: pass,
PasswordConfirm: pass,
Roles: []string{auth.RoleAdmin, auth.RoleUser},
}
usr, err := user.Create(ctx, dbConn, &newU, time.Now())
if err != nil {
return err
}
fmt.Printf("User created with id: %v\n", usr.ID.Hex())
return nil
}

View File

@ -42,7 +42,7 @@ func main() {
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"` ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
} }
Collect struct { Collect struct {
From string `default:"http://sales-api:4000/debug/vars" envconfig:"FROM"` From string `default:"http://web-api:4000/debug/vars" envconfig:"FROM"`
} }
Publish struct { Publish struct {
To string `default:"console" envconfig:"TO"` To string `default:"console" envconfig:"TO"`

View File

@ -71,7 +71,7 @@ func (z *Zipkin) Publish(ctx context.Context, w http.ResponseWriter, r *http.Req
// send uses HTTP to send the data to the tracing sidecar for processing. // send uses HTTP to send the data to the tracing sidecar for processing.
func (z *Zipkin) send(sendBatch []trace.SpanData) error { func (z *Zipkin) send(sendBatch []trace.SpanData) error {
le, err := newEndpoint("sales-api", z.localHost) le, err := newEndpoint("web-api", z.localHost)
if err != nil { if err != nil {
return err return err
} }

View File

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

View File

@ -37,15 +37,15 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *db.DB, authenticato
// This route is not authenticated // This route is not authenticated
app.Handle("GET", "/v1/users/token", u.Token) app.Handle("GET", "/v1/users/token", u.Token)
// Register product and sale endpoints. // Register project and sale endpoints.
p := Product{ p := Project{
MasterDB: masterDB, MasterDB: masterDB,
} }
app.Handle("GET", "/v1/products", p.List, mid.Authenticate(authenticator)) app.Handle("GET", "/v1/projects", p.List, mid.Authenticate(authenticator))
app.Handle("POST", "/v1/products", p.Create, mid.Authenticate(authenticator)) app.Handle("POST", "/v1/projects", p.Create, mid.Authenticate(authenticator))
app.Handle("GET", "/v1/products/:id", p.Retrieve, mid.Authenticate(authenticator)) app.Handle("GET", "/v1/projects/:id", p.Retrieve, mid.Authenticate(authenticator))
app.Handle("PUT", "/v1/products/:id", p.Update, mid.Authenticate(authenticator)) app.Handle("PUT", "/v1/projects/:id", p.Update, mid.Authenticate(authenticator))
app.Handle("DELETE", "/v1/products/:id", p.Delete, mid.Authenticate(authenticator)) app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.Authenticate(authenticator))
return app return app
} }

View File

@ -14,7 +14,7 @@ import (
"syscall" "syscall"
"time" "time"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/sales-api/handlers" "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/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
@ -45,7 +45,7 @@ func main() {
// ========================================================================= // =========================================================================
// Logging // Logging
log := log.New(os.Stdout, "SALES : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) log := log.New(os.Stdout, "WEB_APP : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
// ========================================================================= // =========================================================================
// Configuration // Configuration
@ -75,7 +75,7 @@ func main() {
} }
} }
if err := envconfig.Process("SALES", &cfg); err != nil { if err := envconfig.Process("WEB_APP", &cfg); err != nil {
log.Fatalf("main : Parsing Config : %v", err) log.Fatalf("main : Parsing Config : %v", err)
} }

View File

@ -10,38 +10,38 @@ import (
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" "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/platform/web"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/product" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/project"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/bson"
) )
// TestProducts is the entry point for the products // TestProjects is the entry point for the projects
func TestProducts(t *testing.T) { func TestProjects(t *testing.T) {
defer tests.Recover(t) defer tests.Recover(t)
t.Run("getProducts200Empty", getProducts200Empty) t.Run("getProjects200Empty", getProjects200Empty)
t.Run("postProduct400", postProduct400) t.Run("postProject400", postProject400)
t.Run("postProduct401", postProduct401) t.Run("postProject401", postProject401)
t.Run("getProduct404", getProduct404) t.Run("getProject404", getProject404)
t.Run("getProduct400", getProduct400) t.Run("getProject400", getProject400)
t.Run("deleteProduct404", deleteProduct404) t.Run("deleteProject404", deleteProject404)
t.Run("putProduct404", putProduct404) t.Run("putProject404", putProject404)
t.Run("crudProducts", crudProduct) t.Run("crudProjects", crudProject)
} }
// getProducts200Empty validates an empty products list can be retrieved with the endpoint. // getProjects200Empty validates an empty projects list can be retrieved with the endpoint.
func getProducts200Empty(t *testing.T) { func getProjects200Empty(t *testing.T) {
r := httptest.NewRequest("GET", "/v1/products", nil) r := httptest.NewRequest("GET", "/v1/projects", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization) r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r) a.ServeHTTP(w, r)
t.Log("Given the need to fetch an empty list of products with the products endpoint.") t.Log("Given the need to fetch an empty list of projects with the projects endpoint.")
{ {
t.Log("\tTest 0:\tWhen fetching an empty product list.") t.Log("\tTest 0:\tWhen fetching an empty project list.")
{ {
if w.Code != http.StatusOK { 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.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code)
@ -60,19 +60,19 @@ func getProducts200Empty(t *testing.T) {
} }
} }
// postProduct400 validates a product can't be created with the endpoint // postProject400 validates a project can't be created with the endpoint
// unless a valid product document is submitted. // unless a valid project document is submitted.
func postProduct400(t *testing.T) { func postProject400(t *testing.T) {
r := httptest.NewRequest("POST", "/v1/products", strings.NewReader(`{}`)) r := httptest.NewRequest("POST", "/v1/projects", strings.NewReader(`{}`))
w := httptest.NewRecorder() w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization) r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r) a.ServeHTTP(w, r)
t.Log("Given the need to validate a new product can't be created with an invalid document.") 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 product value.") t.Log("\tTest 0:\tWhen using an incomplete project value.")
{ {
if w.Code != http.StatusBadRequest { 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.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code)
@ -110,10 +110,10 @@ func postProduct400(t *testing.T) {
} }
} }
// postProduct401 validates a product can't be created with the endpoint // postProject401 validates a project can't be created with the endpoint
// unless the user is authenticated // unless the user is authenticated
func postProduct401(t *testing.T) { func postProject401(t *testing.T) {
np := product.NewProduct{ np := project.NewProject{
Name: "Comic Books", Name: "Comic Books",
Cost: 25, Cost: 25,
Quantity: 60, Quantity: 60,
@ -124,16 +124,16 @@ func postProduct401(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
r := httptest.NewRequest("POST", "/v1/products", bytes.NewBuffer(body)) r := httptest.NewRequest("POST", "/v1/projects", bytes.NewBuffer(body))
w := httptest.NewRecorder() w := httptest.NewRecorder()
// Not setting an authorization header // Not setting an authorization header
a.ServeHTTP(w, r) a.ServeHTTP(w, r)
t.Log("Given the need to validate a new product can't be created with an invalid document.") 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 product value.") t.Log("\tTest 0:\tWhen using an incomplete project value.")
{ {
if w.Code != http.StatusUnauthorized { 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.Fatalf("\t%s\tShould receive a status code of 401 for the response : %v", tests.Failed, w.Code)
@ -143,20 +143,20 @@ func postProduct401(t *testing.T) {
} }
} }
// getProduct400 validates a product request for a malformed id. // getProject400 validates a project request for a malformed id.
func getProduct400(t *testing.T) { func getProject400(t *testing.T) {
id := "12345" id := "12345"
r := httptest.NewRequest("GET", "/v1/products/"+id, nil) r := httptest.NewRequest("GET", "/v1/projects/"+id, nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization) r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r) a.ServeHTTP(w, r)
t.Log("Given the need to validate getting a product with a malformed id.") t.Log("Given the need to validate getting a project with a malformed id.")
{ {
t.Logf("\tTest 0:\tWhen using the new product %s.", id) t.Logf("\tTest 0:\tWhen using the new project %s.", id)
{ {
if w.Code != http.StatusBadRequest { 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.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code)
@ -175,20 +175,20 @@ func getProduct400(t *testing.T) {
} }
} }
// getProduct404 validates a product request for a product that does not exist with the endpoint. // getProject404 validates a project request for a project that does not exist with the endpoint.
func getProduct404(t *testing.T) { func getProject404(t *testing.T) {
id := bson.NewObjectId().Hex() id := bson.NewObjectId().Hex()
r := httptest.NewRequest("GET", "/v1/products/"+id, nil) r := httptest.NewRequest("GET", "/v1/projects/"+id, nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization) r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r) a.ServeHTTP(w, r)
t.Log("Given the need to validate getting a product with an unknown id.") t.Log("Given the need to validate getting a project with an unknown id.")
{ {
t.Logf("\tTest 0:\tWhen using the new product %s.", id) t.Logf("\tTest 0:\tWhen using the new project %s.", id)
{ {
if w.Code != http.StatusNotFound { 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.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
@ -207,20 +207,20 @@ func getProduct404(t *testing.T) {
} }
} }
// deleteProduct404 validates deleting a product that does not exist. // deleteProject404 validates deleting a project that does not exist.
func deleteProduct404(t *testing.T) { func deleteProject404(t *testing.T) {
id := bson.NewObjectId().Hex() id := bson.NewObjectId().Hex()
r := httptest.NewRequest("DELETE", "/v1/products/"+id, nil) r := httptest.NewRequest("DELETE", "/v1/projects/"+id, nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization) r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r) a.ServeHTTP(w, r)
t.Log("Given the need to validate deleting a product that does not exist.") t.Log("Given the need to validate deleting a project that does not exist.")
{ {
t.Logf("\tTest 0:\tWhen using the new product %s.", id) t.Logf("\tTest 0:\tWhen using the new project %s.", id)
{ {
if w.Code != http.StatusNotFound { 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.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
@ -239,9 +239,9 @@ func deleteProduct404(t *testing.T) {
} }
} }
// putProduct404 validates updating a product that does not exist. // putProject404 validates updating a project that does not exist.
func putProduct404(t *testing.T) { func putProject404(t *testing.T) {
up := product.UpdateProduct{ up := project.UpdateProject{
Name: tests.StringPointer("Nonexistent"), Name: tests.StringPointer("Nonexistent"),
} }
@ -252,16 +252,16 @@ func putProduct404(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
r := httptest.NewRequest("PUT", "/v1/products/"+id, bytes.NewBuffer(body)) r := httptest.NewRequest("PUT", "/v1/projects/"+id, bytes.NewBuffer(body))
w := httptest.NewRecorder() w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization) r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r) a.ServeHTTP(w, r)
t.Log("Given the need to validate updating a product that does not exist.") t.Log("Given the need to validate updating a project that does not exist.")
{ {
t.Logf("\tTest 0:\tWhen using the new product %s.", id) t.Logf("\tTest 0:\tWhen using the new project %s.", id)
{ {
if w.Code != http.StatusNotFound { 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.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code)
@ -280,18 +280,18 @@ func putProduct404(t *testing.T) {
} }
} }
// crudProduct performs a complete test of CRUD against the api. // crudProject performs a complete test of CRUD against the api.
func crudProduct(t *testing.T) { func crudProject(t *testing.T) {
p := postProduct201(t) p := postProject201(t)
defer deleteProduct204(t, p.ID.Hex()) defer deleteProject204(t, p.ID.Hex())
getProduct200(t, p.ID.Hex()) getProject200(t, p.ID.Hex())
putProduct204(t, p.ID.Hex()) putProject204(t, p.ID.Hex())
} }
// postProduct201 validates a product can be created with the endpoint. // postProject201 validates a project can be created with the endpoint.
func postProduct201(t *testing.T) product.Product { func postProject201(t *testing.T) project.Project {
np := product.NewProduct{ np := project.NewProject{
Name: "Comic Books", Name: "Comic Books",
Cost: 25, Cost: 25,
Quantity: 60, Quantity: 60,
@ -302,7 +302,7 @@ func postProduct201(t *testing.T) product.Product {
t.Fatal(err) t.Fatal(err)
} }
r := httptest.NewRequest("POST", "/v1/products", bytes.NewBuffer(body)) r := httptest.NewRequest("POST", "/v1/projects", bytes.NewBuffer(body))
w := httptest.NewRecorder() w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization) r.Header.Set("Authorization", userAuthorization)
@ -310,11 +310,11 @@ func postProduct201(t *testing.T) product.Product {
a.ServeHTTP(w, r) a.ServeHTTP(w, r)
// p is the value we will return. // p is the value we will return.
var p product.Product var p project.Project
t.Log("Given the need to create a new product with the products endpoint.") t.Log("Given the need to create a new project with the projects endpoint.")
{ {
t.Log("\tTest 0:\tWhen using the declared product value.") t.Log("\tTest 0:\tWhen using the declared project value.")
{ {
if w.Code != http.StatusCreated { 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.Fatalf("\t%s\tShould receive a status code of 201 for the response : %v", tests.Failed, w.Code)
@ -342,18 +342,18 @@ func postProduct201(t *testing.T) product.Product {
return p return p
} }
// deleteProduct200 validates deleting a product that does exist. // deleteProject200 validates deleting a project that does exist.
func deleteProduct204(t *testing.T, id string) { func deleteProject204(t *testing.T, id string) {
r := httptest.NewRequest("DELETE", "/v1/products/"+id, nil) r := httptest.NewRequest("DELETE", "/v1/projects/"+id, nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization) r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r) a.ServeHTTP(w, r)
t.Log("Given the need to validate deleting a product that does exist.") t.Log("Given the need to validate deleting a project that does exist.")
{ {
t.Logf("\tTest 0:\tWhen using the new product %s.", id) t.Logf("\tTest 0:\tWhen using the new project %s.", id)
{ {
if w.Code != http.StatusNoContent { 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.Fatalf("\t%s\tShould receive a status code of 204 for the response : %v", tests.Failed, w.Code)
@ -363,25 +363,25 @@ func deleteProduct204(t *testing.T, id string) {
} }
} }
// getProduct200 validates a product request for an existing id. // getProject200 validates a project request for an existing id.
func getProduct200(t *testing.T, id string) { func getProject200(t *testing.T, id string) {
r := httptest.NewRequest("GET", "/v1/products/"+id, nil) r := httptest.NewRequest("GET", "/v1/projects/"+id, nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization) r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r) a.ServeHTTP(w, r)
t.Log("Given the need to validate getting a product that exists.") t.Log("Given the need to validate getting a project that exists.")
{ {
t.Logf("\tTest 0:\tWhen using the new product %s.", id) t.Logf("\tTest 0:\tWhen using the new project %s.", id)
{ {
if w.Code != http.StatusOK { 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.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) t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success)
var p product.Product var p project.Project
if err := json.NewDecoder(w.Body).Decode(&p); err != nil { 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) t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
} }
@ -402,26 +402,26 @@ func getProduct200(t *testing.T, id string) {
} }
} }
// putProduct204 validates updating a product that does exist. // putProject204 validates updating a project that does exist.
func putProduct204(t *testing.T, id string) { func putProject204(t *testing.T, id string) {
body := `{"name": "Graphic Novels", "cost": 100}` body := `{"name": "Graphic Novels", "cost": 100}`
r := httptest.NewRequest("PUT", "/v1/products/"+id, strings.NewReader(body)) r := httptest.NewRequest("PUT", "/v1/projects/"+id, strings.NewReader(body))
w := httptest.NewRecorder() w := httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization) r.Header.Set("Authorization", userAuthorization)
a.ServeHTTP(w, r) a.ServeHTTP(w, r)
t.Log("Given the need to update a product with the products endpoint.") t.Log("Given the need to update a project with the projects endpoint.")
{ {
t.Log("\tTest 0:\tWhen using the modified product value.") t.Log("\tTest 0:\tWhen using the modified project value.")
{ {
if w.Code != http.StatusNoContent { 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.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) t.Logf("\t%s\tShould receive a status code of 204 for the response.", tests.Success)
r = httptest.NewRequest("GET", "/v1/products/"+id, nil) r = httptest.NewRequest("GET", "/v1/projects/"+id, nil)
w = httptest.NewRecorder() w = httptest.NewRecorder()
r.Header.Set("Authorization", userAuthorization) r.Header.Set("Authorization", userAuthorization)
@ -433,7 +433,7 @@ func putProduct204(t *testing.T, id string) {
} }
t.Logf("\t%s\tShould receive a status code of 200 for the retrieve.", tests.Success) t.Logf("\t%s\tShould receive a status code of 200 for the retrieve.", tests.Success)
var ru product.Product var ru project.Project
if err := json.NewDecoder(w.Body).Decode(&ru); err != nil { 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) t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err)
} }

View File

@ -8,7 +8,7 @@ import (
"testing" "testing"
"time" "time"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/sales-api/handlers" "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/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"

View File

@ -21,17 +21,17 @@ services:
command: --bind_ip 0.0.0.0 command: --bind_ip 0.0.0.0
# This is the core CRUD based service. # This is the core CRUD based service.
sales-api: web-api:
container_name: sales-api container_name: web-api
networks: networks:
- shared-network - shared-network
image: gcr.io/sales-api/sales-api-amd64:1.0 image: gcr.io/web-api/web-api-amd64:1.0
ports: ports:
- 3000:3000 # CRUD API - 3000:3000 # CRUD API
- 4000:4000 # DEBUG API - 4000:4000 # DEBUG API
environment: environment:
- SALES_AUTH_KEY_ID=1 - WEB_APP_AUTH_KEY_ID=1
# - SALES_DB_HOST=got:got2015@ds039441.mongolab.com:39441/gotraining # - WEB_APP_DB_HOST=got:got2015@ds039441.mongolab.com:39441/gotraining
# - GODEBUG=gctrace=1 # - GODEBUG=gctrace=1
# This sidecar publishes metrics to the console by default. # This sidecar publishes metrics to the console by default.
@ -39,7 +39,7 @@ services:
container_name: metrics container_name: metrics
networks: networks:
- shared-network - shared-network
image: gcr.io/sales-api/metrics-amd64:1.0 image: gcr.io/web-api/metrics-amd64:1.0
ports: ports:
- 3001:3001 # EXPVAR API - 3001:3001 # EXPVAR API
- 4001:4001 # DEBUG API - 4001:4001 # DEBUG API
@ -49,12 +49,12 @@ services:
container_name: tracer container_name: tracer
networks: networks:
- shared-network - shared-network
image: gcr.io/sales-api/tracer-amd64:1.0 image: gcr.io/web-api/tracer-amd64:1.0
ports: ports:
- 3002:3002 # TRACER API - 3002:3002 # TRACER API
- 4002:4002 # DEBUG API - 4002:4002 # DEBUG API
# environment: # environment:
# - SALES_ZIPKIN_HOST=http://zipkin:9411/api/v2/spans # - WEB_APP_ZIPKIN_HOST=http://zipkin:9411/api/v2/spans
# This sidecar allows for the viewing of traces. # This sidecar allows for the viewing of traces.
zipkin: zipkin:

View File

@ -1,24 +0,0 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mongo
spec:
replicas: 1
strategy: {}
template:
metadata:
name: mongo
labels:
database: mongo
spec:
containers:
- name: mongo
image: mongo:3-jessie
args:
- --bind_ip
- 0.0.0.0
ports:
- name: mongo
containerPort: 27017
resources: {}
status: {}

View File

@ -1,56 +0,0 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: sales-api
spec:
replicas: 1
strategy: {}
template:
metadata:
name: sales-api
labels:
service: sales-api
spec:
containers:
- name: zipkin
image: openzipkin/zipkin:2.11
ports:
- name: zipkin
containerPort: 9411
resources: {}
- name: sales-api
image: gcr.io/sales-api/sales-api-amd64:1.0
env:
- name: SALES_TRACE_HOST
value: "http://localhost:3002/v1/publish"
- name: SALES_AUTH_KEY_ID
value: "1"
ports:
- name: sales-api
containerPort: 3000
- name: debug
containerPort: 4000
resources: {}
- name: metrics
image: gcr.io/sales-api/metrics-amd64:1.0
env:
- name: METRICS_COLLECT_FROM
value: "http://localhost:4000/debug/vars"
ports:
- name: metrics
containerPort: 3001
- name: debug
containerPort: 4001
resources: {}
- name: tracer
image: gcr.io/sales-api/tracer-amd64:1.0
env:
- name: TRACER_ZIPKIN_HOST
value: "http://localhost:9411/api/v2/spans"
ports:
- name: tracer
containerPort: 3002
- name: debug
containerPort: 4002
resources: {}
status: {}

View File

@ -1,11 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: mongo
spec:
selector:
database: mongo
ports:
- name: "db"
port: 27017
targetPort: 27017

View File

@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: sales-api
spec:
selector:
service: sales-api
ports:
- name: "zipkin"
port: 9411
targetPort: 9411
- name: "sales-api"
port: 3000
targetPort: 3000
- name: "metrics"
port: 3001
targetPort: 3001
status:
loadBalancer: {}

View File

@ -20,7 +20,7 @@ import (
type KeyFunc func(keyID string) (*rsa.PublicKey, error) type KeyFunc func(keyID string) (*rsa.PublicKey, error)
// NewSingleKeyFunc is a simple implementation of KeyFunc that only ever // NewSingleKeyFunc is a simple implementation of KeyFunc that only ever
// supports one key. This is easy for development but in production should be // supports one key. This is easy for development but in projection should be
// replaced with a caching layer that calls a JWKS endpoint. // replaced with a caching layer that calls a JWKS endpoint.
func NewSingleKeyFunc(id string, key *rsa.PublicKey) KeyFunc { func NewSingleKeyFunc(id string, key *rsa.PublicKey) KeyFunc {
return func(kid string) (*rsa.PublicKey, error) { return func(kid string) (*rsa.PublicKey, error) {

View File

@ -84,7 +84,7 @@ func TestApply(t *testing.T) {
Host string `default:"mongo:27017/gotraining" flag:"h"` Host string `default:"mongo:27017/gotraining" flag:"h"`
Insecure bool `flag:"i"` Insecure bool `flag:"i"`
} }
osArgs := []string{"./sales-api", "-i", "-a", "0.0.1.1:5000", "--web_batchsize", "300", "--dialtimeout", "10s"} osArgs := []string{"./web-api", "-i", "-a", "0.0.1.1:5000", "--web_batchsize", "300", "--dialtimeout", "10s"}
expected := `{"Web":{"APIHost":"0.0.1.1:5000","BatchSize":300,"ReadTimeout":0},"DialTimeout":10000000000,"Host":"","Insecure":true}` expected := `{"Web":{"APIHost":"0.0.1.1:5000","BatchSize":300,"ReadTimeout":0},"DialTimeout":10000000000,"Host":"","Insecure":true}`
t.Log("Given the need to validate we can apply overrides a struct value.") t.Log("Given the need to validate we can apply overrides a struct value.")

View File

@ -1,129 +0,0 @@
package product_test
import (
"os"
"testing"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/product"
"github.com/google/go-cmp/cmp"
"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()
}
// TestProduct validates the full set of CRUD operations on Product values.
func TestProduct(t *testing.T) {
defer tests.Recover(t)
t.Log("Given the need to work with Product records.")
{
t.Log("\tWhen handling a single Product.")
{
ctx := tests.Context()
dbConn := test.MasterDB.Copy()
defer dbConn.Close()
np := product.NewProduct{
Name: "Comic Books",
Cost: 25,
Quantity: 60,
}
p, err := product.Create(ctx, dbConn, &np, time.Now().UTC())
if err != nil {
t.Fatalf("\t%s\tShould be able to create a product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to create a product.", tests.Success)
savedP, err := product.Retrieve(ctx, dbConn, p.ID.Hex())
if err != nil {
t.Fatalf("\t%s\tShould be able to retrieve product by ID: %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to retrieve product by ID.", tests.Success)
if diff := cmp.Diff(p, savedP); diff != "" {
t.Fatalf("\t%s\tShould get back the same product. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get back the same product.", tests.Success)
upd := product.UpdateProduct{
Name: tests.StringPointer("Comics"),
Cost: tests.IntPointer(50),
Quantity: tests.IntPointer(40),
}
if err := product.Update(ctx, dbConn, p.ID.Hex(), upd, time.Now().UTC()); err != nil {
t.Fatalf("\t%s\tShould be able to update product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to update product.", tests.Success)
savedP, err = product.Retrieve(ctx, dbConn, p.ID.Hex())
if err != nil {
t.Fatalf("\t%s\tShould be able to retrieve updated product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to retrieve updated product.", tests.Success)
// Build a product matching what we expect to see. We just use the
// modified time from the database.
want := &product.Product{
ID: p.ID,
Name: *upd.Name,
Cost: *upd.Cost,
Quantity: *upd.Quantity,
DateCreated: p.DateCreated,
DateModified: savedP.DateModified,
}
if diff := cmp.Diff(want, savedP); diff != "" {
t.Fatalf("\t%s\tShould get back the same product. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get back the same product.", tests.Success)
upd = product.UpdateProduct{
Name: tests.StringPointer("Graphic Novels"),
}
if err := product.Update(ctx, dbConn, p.ID.Hex(), upd, time.Now().UTC()); err != nil {
t.Fatalf("\t%s\tShould be able to update just some fields of product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to update just some fields of product.", tests.Success)
savedP, err = product.Retrieve(ctx, dbConn, p.ID.Hex())
if err != nil {
t.Fatalf("\t%s\tShould be able to retrieve updated product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to retrieve updated product.", tests.Success)
if savedP.Name != *upd.Name {
t.Fatalf("\t%s\tShould be able to see updated Name field : got %q want %q.", tests.Failed, savedP.Name, *upd.Name)
} else {
t.Logf("\t%s\tShould be able to see updated Name field.", tests.Success)
}
if err := product.Delete(ctx, dbConn, p.ID.Hex()); err != nil {
t.Fatalf("\t%s\tShould be able to delete product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to delete product.", tests.Success)
savedP, err = product.Retrieve(ctx, dbConn, p.ID.Hex())
if errors.Cause(err) != product.ErrNotFound {
t.Fatalf("\t%s\tShould NOT be able to retrieve deleted product : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould NOT be able to retrieve deleted product.", tests.Success)
}
}
}

View File

@ -1,4 +1,4 @@
package product package project
import ( import (
"time" "time"
@ -6,37 +6,37 @@ import (
"gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/bson"
) )
// Product is an item we sell. // Project is an item we sell.
type Product struct { type Project struct {
ID bson.ObjectId `bson:"_id" json:"id"` // Unique identifier. ID bson.ObjectId `bson:"_id" json:"id"` // Unique identifier.
Name string `bson:"name" json:"name"` // Display name of the product. Name string `bson:"name" json:"name"` // Display name of the project.
Cost int `bson:"cost" json:"cost"` // Price for one item in cents. Cost int `bson:"cost" json:"cost"` // Price for one item in cents.
Quantity int `bson:"quantity" json:"quantity"` // Original number of items available. Quantity int `bson:"quantity" json:"quantity"` // Original number of items available.
DateCreated time.Time `bson:"date_created" json:"date_created"` // When the product was added. DateCreated time.Time `bson:"date_created" json:"date_created"` // When the project was added.
DateModified time.Time `bson:"date_modified" json:"date_modified"` // When the product record was lost modified. DateModified time.Time `bson:"date_modified" json:"date_modified"` // When the project record was lost modified.
} }
// NewProduct is what we require from clients when adding a Product. // NewProject is what we require from clients when adding a Project.
type NewProduct struct { type NewProject struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Cost int `json:"cost" validate:"required,gte=0"` Cost int `json:"cost" validate:"required,gte=0"`
Quantity int `json:"quantity" validate:"required,gte=1"` Quantity int `json:"quantity" validate:"required,gte=1"`
} }
// UpdateProduct defines what information may be provided to modify an // UpdateProject defines what information may be provided to modify an
// existing Product. All fields are optional so clients can send just the // existing Project. All fields are optional so clients can send just the
// fields they want changed. It uses pointer fields so we can differentiate // fields they want changed. It uses pointer fields so we can differentiate
// between a field that was not provided and a field that was provided as // between a field that was not provided and a field that was provided as
// explicitly blank. Normally we do not want to use pointers to basic types but // explicitly blank. Normally we do not want to use pointers to basic types but
// we make exceptions around marshalling/unmarshalling. // we make exceptions around marshalling/unmarshalling.
type UpdateProduct struct { type UpdateProject struct {
Name *string `json:"name"` Name *string `json:"name"`
Cost *int `json:"cost" validate:"omitempty,gte=0"` Cost *int `json:"cost" validate:"omitempty,gte=0"`
Quantity *int `json:"quantity" validate:"omitempty,gte=1"` Quantity *int `json:"quantity" validate:"omitempty,gte=1"`
} }
// Sale represents a transaction where we sold some quantity of a // Sale represents a transaction where we sold some quantity of a
// Product. // Project.
type Sale struct{} type Sale struct{}
// NewSale defines what we require when creating a Sale record. // NewSale defines what we require when creating a Sale record.

View File

@ -1,4 +1,4 @@
package product package project
import ( import (
"context" "context"
@ -12,7 +12,7 @@ import (
"gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/bson"
) )
const productsCollection = "products" const projectsCollection = "projects"
var ( var (
// ErrNotFound abstracts the mgo not found error. // ErrNotFound abstracts the mgo not found error.
@ -22,26 +22,26 @@ var (
ErrInvalidID = errors.New("ID is not in its proper form") ErrInvalidID = errors.New("ID is not in its proper form")
) )
// List retrieves a list of existing products from the database. // List retrieves a list of existing projects from the database.
func List(ctx context.Context, dbConn *db.DB) ([]Product, error) { func List(ctx context.Context, dbConn *db.DB) ([]Project, error) {
ctx, span := trace.StartSpan(ctx, "internal.product.List") ctx, span := trace.StartSpan(ctx, "internal.project.List")
defer span.End() defer span.End()
p := []Product{} p := []Project{}
f := func(collection *mgo.Collection) error { f := func(collection *mgo.Collection) error {
return collection.Find(nil).All(&p) return collection.Find(nil).All(&p)
} }
if err := dbConn.Execute(ctx, productsCollection, f); err != nil { if err := dbConn.Execute(ctx, projectsCollection, f); err != nil {
return nil, errors.Wrap(err, "db.products.find()") return nil, errors.Wrap(err, "db.projects.find()")
} }
return p, nil return p, nil
} }
// Retrieve gets the specified product from the database. // Retrieve gets the specified project from the database.
func Retrieve(ctx context.Context, dbConn *db.DB, id string) (*Product, error) { func Retrieve(ctx context.Context, dbConn *db.DB, id string) (*Project, error) {
ctx, span := trace.StartSpan(ctx, "internal.product.Retrieve") ctx, span := trace.StartSpan(ctx, "internal.project.Retrieve")
defer span.End() defer span.End()
if !bson.IsObjectIdHex(id) { if !bson.IsObjectIdHex(id) {
@ -50,30 +50,30 @@ func Retrieve(ctx context.Context, dbConn *db.DB, id string) (*Product, error) {
q := bson.M{"_id": bson.ObjectIdHex(id)} q := bson.M{"_id": bson.ObjectIdHex(id)}
var p *Product var p *Project
f := func(collection *mgo.Collection) error { f := func(collection *mgo.Collection) error {
return collection.Find(q).One(&p) return collection.Find(q).One(&p)
} }
if err := dbConn.Execute(ctx, productsCollection, f); err != nil { if err := dbConn.Execute(ctx, projectsCollection, f); err != nil {
if err == mgo.ErrNotFound { if err == mgo.ErrNotFound {
return nil, ErrNotFound return nil, ErrNotFound
} }
return nil, errors.Wrap(err, fmt.Sprintf("db.products.find(%s)", db.Query(q))) return nil, errors.Wrap(err, fmt.Sprintf("db.projects.find(%s)", db.Query(q)))
} }
return p, nil return p, nil
} }
// Create inserts a new product into the database. // Create inserts a new project into the database.
func Create(ctx context.Context, dbConn *db.DB, cp *NewProduct, now time.Time) (*Product, error) { func Create(ctx context.Context, dbConn *db.DB, cp *NewProject, now time.Time) (*Project, error) {
ctx, span := trace.StartSpan(ctx, "internal.product.Create") ctx, span := trace.StartSpan(ctx, "internal.project.Create")
defer span.End() defer span.End()
// Mongo truncates times to milliseconds when storing. We and do the same // Mongo truncates times to milliseconds when storing. We and do the same
// here so the value we return is consistent with what we store. // here so the value we return is consistent with what we store.
now = now.Truncate(time.Millisecond) now = now.Truncate(time.Millisecond)
p := Product{ p := Project{
ID: bson.NewObjectId(), ID: bson.NewObjectId(),
Name: cp.Name, Name: cp.Name,
Cost: cp.Cost, Cost: cp.Cost,
@ -85,16 +85,16 @@ func Create(ctx context.Context, dbConn *db.DB, cp *NewProduct, now time.Time) (
f := func(collection *mgo.Collection) error { f := func(collection *mgo.Collection) error {
return collection.Insert(&p) return collection.Insert(&p)
} }
if err := dbConn.Execute(ctx, productsCollection, f); err != nil { if err := dbConn.Execute(ctx, projectsCollection, f); err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("db.products.insert(%s)", db.Query(&p))) return nil, errors.Wrap(err, fmt.Sprintf("db.projects.insert(%s)", db.Query(&p)))
} }
return &p, nil return &p, nil
} }
// Update replaces a product document in the database. // Update replaces a project document in the database.
func Update(ctx context.Context, dbConn *db.DB, id string, upd UpdateProduct, now time.Time) error { func Update(ctx context.Context, dbConn *db.DB, id string, upd UpdateProject, now time.Time) error {
ctx, span := trace.StartSpan(ctx, "internal.product.Update") ctx, span := trace.StartSpan(ctx, "internal.project.Update")
defer span.End() defer span.End()
if !bson.IsObjectIdHex(id) { if !bson.IsObjectIdHex(id) {
@ -126,7 +126,7 @@ func Update(ctx context.Context, dbConn *db.DB, id string, upd UpdateProduct, no
f := func(collection *mgo.Collection) error { f := func(collection *mgo.Collection) error {
return collection.Update(q, m) return collection.Update(q, m)
} }
if err := dbConn.Execute(ctx, productsCollection, f); err != nil { if err := dbConn.Execute(ctx, projectsCollection, f); err != nil {
if err == mgo.ErrNotFound { if err == mgo.ErrNotFound {
return ErrNotFound return ErrNotFound
} }
@ -136,9 +136,9 @@ func Update(ctx context.Context, dbConn *db.DB, id string, upd UpdateProduct, no
return nil return nil
} }
// Delete removes a product from the database. // Delete removes a project from the database.
func Delete(ctx context.Context, dbConn *db.DB, id string) error { func Delete(ctx context.Context, dbConn *db.DB, id string) error {
ctx, span := trace.StartSpan(ctx, "internal.product.Delete") ctx, span := trace.StartSpan(ctx, "internal.project.Delete")
defer span.End() defer span.End()
if !bson.IsObjectIdHex(id) { if !bson.IsObjectIdHex(id) {
@ -150,11 +150,11 @@ func Delete(ctx context.Context, dbConn *db.DB, id string) error {
f := func(collection *mgo.Collection) error { f := func(collection *mgo.Collection) error {
return collection.Remove(q) return collection.Remove(q)
} }
if err := dbConn.Execute(ctx, productsCollection, f); err != nil { if err := dbConn.Execute(ctx, projectsCollection, f); err != nil {
if err == mgo.ErrNotFound { if err == mgo.ErrNotFound {
return ErrNotFound return ErrNotFound
} }
return errors.Wrap(err, fmt.Sprintf("db.products.remove(%v)", q)) return errors.Wrap(err, fmt.Sprintf("db.projects.remove(%v)", q))
} }
return nil return nil

View File

@ -0,0 +1,129 @@
package project_test
import (
"os"
"testing"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/project"
"github.com/google/go-cmp/cmp"
"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()
}
// TestProject validates the full set of CRUD operations on Project values.
func TestProject(t *testing.T) {
defer tests.Recover(t)
t.Log("Given the need to work with Project records.")
{
t.Log("\tWhen handling a single Project.")
{
ctx := tests.Context()
dbConn := test.MasterDB.Copy()
defer dbConn.Close()
np := project.NewProject{
Name: "Comic Books",
Cost: 25,
Quantity: 60,
}
p, err := project.Create(ctx, dbConn, &np, time.Now().UTC())
if err != nil {
t.Fatalf("\t%s\tShould be able to create a project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to create a project.", tests.Success)
savedP, err := project.Retrieve(ctx, dbConn, p.ID.Hex())
if err != nil {
t.Fatalf("\t%s\tShould be able to retrieve project by ID: %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to retrieve project by ID.", tests.Success)
if diff := cmp.Diff(p, savedP); diff != "" {
t.Fatalf("\t%s\tShould get back the same project. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get back the same project.", tests.Success)
upd := project.UpdateProject{
Name: tests.StringPointer("Comics"),
Cost: tests.IntPointer(50),
Quantity: tests.IntPointer(40),
}
if err := project.Update(ctx, dbConn, p.ID.Hex(), upd, time.Now().UTC()); err != nil {
t.Fatalf("\t%s\tShould be able to update project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to update project.", tests.Success)
savedP, err = project.Retrieve(ctx, dbConn, p.ID.Hex())
if err != nil {
t.Fatalf("\t%s\tShould be able to retrieve updated project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to retrieve updated project.", tests.Success)
// Build a project matching what we expect to see. We just use the
// modified time from the database.
want := &project.Project{
ID: p.ID,
Name: *upd.Name,
Cost: *upd.Cost,
Quantity: *upd.Quantity,
DateCreated: p.DateCreated,
DateModified: savedP.DateModified,
}
if diff := cmp.Diff(want, savedP); diff != "" {
t.Fatalf("\t%s\tShould get back the same project. Diff:\n%s", tests.Failed, diff)
}
t.Logf("\t%s\tShould get back the same project.", tests.Success)
upd = project.UpdateProject{
Name: tests.StringPointer("Graphic Novels"),
}
if err := project.Update(ctx, dbConn, p.ID.Hex(), upd, time.Now().UTC()); err != nil {
t.Fatalf("\t%s\tShould be able to update just some fields of project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to update just some fields of project.", tests.Success)
savedP, err = project.Retrieve(ctx, dbConn, p.ID.Hex())
if err != nil {
t.Fatalf("\t%s\tShould be able to retrieve updated project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to retrieve updated project.", tests.Success)
if savedP.Name != *upd.Name {
t.Fatalf("\t%s\tShould be able to see updated Name field : got %q want %q.", tests.Failed, savedP.Name, *upd.Name)
} else {
t.Logf("\t%s\tShould be able to see updated Name field.", tests.Success)
}
if err := project.Delete(ctx, dbConn, p.ID.Hex()); err != nil {
t.Fatalf("\t%s\tShould be able to delete project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould be able to delete project.", tests.Success)
savedP, err = project.Retrieve(ctx, dbConn, p.ID.Hex())
if errors.Cause(err) != project.ErrNotFound {
t.Fatalf("\t%s\tShould NOT be able to retrieve deleted project : %s.", tests.Failed, err)
}
t.Logf("\t%s\tShould NOT be able to retrieve deleted project.", tests.Success)
}
}
}

View File

@ -1,6 +1,6 @@
SHELL := /bin/bash SHELL := /bin/bash
all: keys sales-api metrics tracer all: keys web-api metrics tracer
keys: keys:
go run ./cmd/sales-admin/main.go --cmd keygen go run ./cmd/sales-admin/main.go --cmd keygen
@ -8,10 +8,10 @@ keys:
admin: admin:
go run ./cmd/sales-admin/main.go --cmd useradd --user_email admin@example.com --user_password gophers go run ./cmd/sales-admin/main.go --cmd useradd --user_email admin@example.com --user_password gophers
sales-api: web-api:
docker build \ docker build \
-t gcr.io/sales-api/sales-api-amd64:1.0 \ -t gcr.io/web-api/web-api-amd64:1.0 \
--build-arg PACKAGE_NAME=sales-api \ --build-arg PACKAGE_NAME=web-api \
--build-arg VCS_REF=`git rev-parse HEAD` \ --build-arg VCS_REF=`git rev-parse HEAD` \
--build-arg BUILD_DATE=`date -u +”%Y-%m-%dT%H:%M:%SZ”` \ --build-arg BUILD_DATE=`date -u +”%Y-%m-%dT%H:%M:%SZ”` \
. .
@ -19,7 +19,7 @@ sales-api:
metrics: metrics:
docker build \ docker build \
-t gcr.io/sales-api/metrics-amd64:1.0 \ -t gcr.io/web-api/metrics-amd64:1.0 \
--build-arg PACKAGE_NAME=metrics \ --build-arg PACKAGE_NAME=metrics \
--build-arg PACKAGE_PREFIX=sidecar/ \ --build-arg PACKAGE_PREFIX=sidecar/ \
--build-arg VCS_REF=`git rev-parse HEAD` \ --build-arg VCS_REF=`git rev-parse HEAD` \
@ -28,9 +28,8 @@ metrics:
docker system prune -f docker system prune -f
tracer: tracer:
cd "$$GOPATH/src/geeks-accelerator/oss/saas-starter-kit/example-project"
docker build \ docker build \
-t gcr.io/sales-api/tracer-amd64:1.0 \ -t gcr.io/web-api/tracer-amd64:1.0 \
--build-arg PACKAGE_NAME=tracer \ --build-arg PACKAGE_NAME=tracer \
--build-arg PACKAGE_PREFIX=sidecar/ \ --build-arg PACKAGE_PREFIX=sidecar/ \
--build-arg VCS_REF=`git rev-parse HEAD` \ --build-arg VCS_REF=`git rev-parse HEAD` \
@ -61,27 +60,27 @@ remove-all:
# GKE # GKE
config: config:
@echo Setting environment for sales-api @echo Setting environment for web-api
gcloud config set project sales-api gcloud config set project web-api
gcloud config set compute/zone us-central1-b gcloud config set compute/zone us-central1-b
gcloud auth configure-docker gcloud auth configure-docker
@echo ====================================================================== @echo ======================================================================
project: project:
gcloud projects create sales-api gcloud projects create web-api
gcloud beta billing projects link sales-api --billing-account=$(ACCOUNT_ID) gcloud beta billing projects link web-api --billing-account=$(ACCOUNT_ID)
gcloud services enable container.googleapis.com gcloud services enable container.googleapis.com
@echo ====================================================================== @echo ======================================================================
cluster: cluster:
gcloud container clusters create sales-api-cluster --num-nodes=2 --machine-type=n1-standard-2 gcloud container clusters create web-api-cluster --num-nodes=2 --machine-type=n1-standard-2
gcloud compute instances list gcloud compute instances list
@echo ====================================================================== @echo ======================================================================
upload: upload:
docker push gcr.io/sales-api/sales-api-amd64:1.0 docker push gcr.io/web-api/web-api-amd64:1.0
docker push gcr.io/sales-api/metrics-amd64:1.0 docker push gcr.io/web-api/metrics-amd64:1.0
docker push gcr.io/sales-api/tracer-amd64:1.0 docker push gcr.io/web-api/tracer-amd64:1.0
@echo ====================================================================== @echo ======================================================================
database: database:
@ -90,8 +89,8 @@ database:
@echo ====================================================================== @echo ======================================================================
services: services:
kubectl create -f gke-deploy-sales-api.yaml kubectl create -f gke-deploy-web-api.yaml
kubectl expose -f gke-expose-sales-api.yaml --type=LoadBalancer kubectl expose -f gke-expose-web-api.yaml --type=LoadBalancer
@echo ====================================================================== @echo ======================================================================
shell: shell:
@ -102,15 +101,15 @@ status:
gcloud container clusters list gcloud container clusters list
kubectl get nodes kubectl get nodes
kubectl get pods kubectl get pods
kubectl get services sales-api kubectl get services web-api
@echo ====================================================================== @echo ======================================================================
delete: delete:
kubectl delete services sales-api kubectl delete services web-api
kubectl delete deployment sales-api kubectl delete deployment web-api
gcloud container clusters delete sales-api-cluster gcloud container clusters delete web-api-cluster
gcloud projects delete sales-api gcloud projects delete web-api
docker image remove gcr.io/sales-api/sales-api-amd64:1.0 docker image remove gcr.io/web-api/web-api-amd64:1.0
docker image remove gcr.io/sales-api/metrics-amd64:1.0 docker image remove gcr.io/web-api/metrics-amd64:1.0
docker image remove gcr.io/sales-api/tracer-amd64:1.0 docker image remove gcr.io/web-api/tracer-amd64:1.0
@echo ====================================================================== @echo ======================================================================