You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-06-15 00:15:15 +02:00
Basic example cleanup
Rename sales-api to web-api and remove sales-admin
This commit is contained in:
1
example-project/.gitignore
vendored
Normal file
1
example-project/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
private.pem
|
@ -105,7 +105,7 @@ Running `make down` will properly stop and terminate the Docker Compose session.
|
||||
|
||||
## 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:-->
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -42,7 +42,7 @@ func main() {
|
||||
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
|
||||
}
|
||||
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 {
|
||||
To string `default:"console" envconfig:"TO"`
|
||||
|
@ -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.
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -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/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"
|
||||
"go.opencensus.io/trace"
|
||||
)
|
||||
|
||||
// Product represents the Product API method handler set.
|
||||
type Product struct {
|
||||
// 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 products in the system.
|
||||
func (p *Product) List(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.Product.List")
|
||||
// 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()
|
||||
|
||||
products, err := product.List(ctx, dbConn)
|
||||
projects, err := project.List(ctx, dbConn)
|
||||
if err != nil {
|
||||
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.
|
||||
func (p *Product) Retrieve(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.Product.Retrieve")
|
||||
// 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 := product.Retrieve(ctx, dbConn, params["id"])
|
||||
prod, err := project.Retrieve(ctx, dbConn, params["id"])
|
||||
if err != nil {
|
||||
switch err {
|
||||
case product.ErrInvalidID:
|
||||
case project.ErrInvalidID:
|
||||
return web.NewRequestError(err, http.StatusBadRequest)
|
||||
case product.ErrNotFound:
|
||||
case project.ErrNotFound:
|
||||
return web.NewRequestError(err, http.StatusNotFound)
|
||||
default:
|
||||
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)
|
||||
}
|
||||
|
||||
// Create inserts a new product into the system.
|
||||
func (p *Product) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.Product.Create")
|
||||
// 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()
|
||||
@ -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")
|
||||
}
|
||||
|
||||
var np product.NewProduct
|
||||
var np project.NewProject
|
||||
if err := web.Decode(r, &np); err != nil {
|
||||
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 {
|
||||
return errors.Wrapf(err, "Product: %+v", &np)
|
||||
return errors.Wrapf(err, "Project: %+v", &np)
|
||||
}
|
||||
|
||||
return web.Respond(ctx, w, nUsr, http.StatusCreated)
|
||||
}
|
||||
|
||||
// Update updates the specified product in the system.
|
||||
func (p *Product) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.Product.Update")
|
||||
// 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()
|
||||
@ -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")
|
||||
}
|
||||
|
||||
var up product.UpdateProduct
|
||||
var up project.UpdateProject
|
||||
if err := web.Decode(r, &up); err != nil {
|
||||
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 {
|
||||
switch err {
|
||||
case product.ErrInvalidID:
|
||||
case project.ErrInvalidID:
|
||||
return web.NewRequestError(err, http.StatusBadRequest)
|
||||
case product.ErrNotFound:
|
||||
case project.ErrNotFound:
|
||||
return web.NewRequestError(err, http.StatusNotFound)
|
||||
default:
|
||||
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)
|
||||
}
|
||||
|
||||
// Delete removes the specified product from the system.
|
||||
func (p *Product) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
ctx, span := trace.StartSpan(ctx, "handlers.Product.Delete")
|
||||
// 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 := product.Delete(ctx, dbConn, params["id"])
|
||||
err := project.Delete(ctx, dbConn, params["id"])
|
||||
if err != nil {
|
||||
switch err {
|
||||
case product.ErrInvalidID:
|
||||
case project.ErrInvalidID:
|
||||
return web.NewRequestError(err, http.StatusBadRequest)
|
||||
case product.ErrNotFound:
|
||||
case project.ErrNotFound:
|
||||
return web.NewRequestError(err, http.StatusNotFound)
|
||||
default:
|
||||
return errors.Wrapf(err, "Id: %s", params["id"])
|
@ -37,15 +37,15 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *db.DB, authenticato
|
||||
// This route is not authenticated
|
||||
app.Handle("GET", "/v1/users/token", u.Token)
|
||||
|
||||
// Register product and sale endpoints.
|
||||
p := Product{
|
||||
// Register project and sale endpoints.
|
||||
p := Project{
|
||||
MasterDB: masterDB,
|
||||
}
|
||||
app.Handle("GET", "/v1/products", p.List, mid.Authenticate(authenticator))
|
||||
app.Handle("POST", "/v1/products", p.Create, mid.Authenticate(authenticator))
|
||||
app.Handle("GET", "/v1/products/:id", p.Retrieve, mid.Authenticate(authenticator))
|
||||
app.Handle("PUT", "/v1/products/:id", p.Update, mid.Authenticate(authenticator))
|
||||
app.Handle("DELETE", "/v1/products/:id", p.Delete, mid.Authenticate(authenticator))
|
||||
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
|
||||
}
|
@ -14,7 +14,7 @@ import (
|
||||
"syscall"
|
||||
"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/db"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
|
||||
@ -45,7 +45,7 @@ func main() {
|
||||
// =========================================================================
|
||||
// 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
|
||||
@ -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)
|
||||
}
|
||||
|
@ -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/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/cmpopts"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
// TestProducts is the entry point for the products
|
||||
func TestProducts(t *testing.T) {
|
||||
// TestProjects is the entry point for the projects
|
||||
func TestProjects(t *testing.T) {
|
||||
defer tests.Recover(t)
|
||||
|
||||
t.Run("getProducts200Empty", getProducts200Empty)
|
||||
t.Run("postProduct400", postProduct400)
|
||||
t.Run("postProduct401", postProduct401)
|
||||
t.Run("getProduct404", getProduct404)
|
||||
t.Run("getProduct400", getProduct400)
|
||||
t.Run("deleteProduct404", deleteProduct404)
|
||||
t.Run("putProduct404", putProduct404)
|
||||
t.Run("crudProducts", crudProduct)
|
||||
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)
|
||||
}
|
||||
|
||||
// getProducts200Empty validates an empty products list can be retrieved with the endpoint.
|
||||
func getProducts200Empty(t *testing.T) {
|
||||
r := httptest.NewRequest("GET", "/v1/products", nil)
|
||||
// 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 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 {
|
||||
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
|
||||
// unless a valid product document is submitted.
|
||||
func postProduct400(t *testing.T) {
|
||||
r := httptest.NewRequest("POST", "/v1/products", strings.NewReader(`{}`))
|
||||
// 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 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 {
|
||||
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
|
||||
func postProduct401(t *testing.T) {
|
||||
np := product.NewProduct{
|
||||
func postProject401(t *testing.T) {
|
||||
np := project.NewProject{
|
||||
Name: "Comic Books",
|
||||
Cost: 25,
|
||||
Quantity: 60,
|
||||
@ -124,16 +124,16 @@ func postProduct401(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("POST", "/v1/products", bytes.NewBuffer(body))
|
||||
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 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 {
|
||||
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.
|
||||
func getProduct400(t *testing.T) {
|
||||
// getProject400 validates a project request for a malformed id.
|
||||
func getProject400(t *testing.T) {
|
||||
id := "12345"
|
||||
|
||||
r := httptest.NewRequest("GET", "/v1/products/"+id, nil)
|
||||
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 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 {
|
||||
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.
|
||||
func getProduct404(t *testing.T) {
|
||||
// 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/products/"+id, nil)
|
||||
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 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 {
|
||||
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.
|
||||
func deleteProduct404(t *testing.T) {
|
||||
// deleteProject404 validates deleting a project that does not exist.
|
||||
func deleteProject404(t *testing.T) {
|
||||
id := bson.NewObjectId().Hex()
|
||||
|
||||
r := httptest.NewRequest("DELETE", "/v1/products/"+id, nil)
|
||||
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 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 {
|
||||
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.
|
||||
func putProduct404(t *testing.T) {
|
||||
up := product.UpdateProduct{
|
||||
// putProject404 validates updating a project that does not exist.
|
||||
func putProject404(t *testing.T) {
|
||||
up := project.UpdateProject{
|
||||
Name: tests.StringPointer("Nonexistent"),
|
||||
}
|
||||
|
||||
@ -252,16 +252,16 @@ func putProduct404(t *testing.T) {
|
||||
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()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
|
||||
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 {
|
||||
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.
|
||||
func crudProduct(t *testing.T) {
|
||||
p := postProduct201(t)
|
||||
defer deleteProduct204(t, p.ID.Hex())
|
||||
// crudProject performs a complete test of CRUD against the api.
|
||||
func crudProject(t *testing.T) {
|
||||
p := postProject201(t)
|
||||
defer deleteProject204(t, p.ID.Hex())
|
||||
|
||||
getProduct200(t, p.ID.Hex())
|
||||
putProduct204(t, p.ID.Hex())
|
||||
getProject200(t, p.ID.Hex())
|
||||
putProject204(t, p.ID.Hex())
|
||||
}
|
||||
|
||||
// postProduct201 validates a product can be created with the endpoint.
|
||||
func postProduct201(t *testing.T) product.Product {
|
||||
np := product.NewProduct{
|
||||
// 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,
|
||||
@ -302,7 +302,7 @@ func postProduct201(t *testing.T) product.Product {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("POST", "/v1/products", bytes.NewBuffer(body))
|
||||
r := httptest.NewRequest("POST", "/v1/projects", bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.Header.Set("Authorization", userAuthorization)
|
||||
@ -310,11 +310,11 @@ func postProduct201(t *testing.T) product.Product {
|
||||
a.ServeHTTP(w, r)
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
// deleteProduct200 validates deleting a product that does exist.
|
||||
func deleteProduct204(t *testing.T, id string) {
|
||||
r := httptest.NewRequest("DELETE", "/v1/products/"+id, nil)
|
||||
// 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 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 {
|
||||
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.
|
||||
func getProduct200(t *testing.T, id string) {
|
||||
r := httptest.NewRequest("GET", "/v1/products/"+id, nil)
|
||||
// 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 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 {
|
||||
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 product.Product
|
||||
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)
|
||||
}
|
||||
@ -402,26 +402,26 @@ func getProduct200(t *testing.T, id string) {
|
||||
}
|
||||
}
|
||||
|
||||
// putProduct204 validates updating a product that does exist.
|
||||
func putProduct204(t *testing.T, id string) {
|
||||
// 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/products/"+id, strings.NewReader(body))
|
||||
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 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 {
|
||||
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/products/"+id, nil)
|
||||
r = httptest.NewRequest("GET", "/v1/projects/"+id, nil)
|
||||
w = httptest.NewRecorder()
|
||||
|
||||
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)
|
||||
|
||||
var ru product.Product
|
||||
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)
|
||||
}
|
@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
"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/tests"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
|
@ -21,17 +21,17 @@ services:
|
||||
command: --bind_ip 0.0.0.0
|
||||
|
||||
# This is the core CRUD based service.
|
||||
sales-api:
|
||||
container_name: sales-api
|
||||
web-api:
|
||||
container_name: web-api
|
||||
networks:
|
||||
- shared-network
|
||||
image: gcr.io/sales-api/sales-api-amd64:1.0
|
||||
image: gcr.io/web-api/web-api-amd64:1.0
|
||||
ports:
|
||||
- 3000:3000 # CRUD API
|
||||
- 4000:4000 # DEBUG API
|
||||
environment:
|
||||
- SALES_AUTH_KEY_ID=1
|
||||
# - SALES_DB_HOST=got:got2015@ds039441.mongolab.com:39441/gotraining
|
||||
- WEB_APP_AUTH_KEY_ID=1
|
||||
# - WEB_APP_DB_HOST=got:got2015@ds039441.mongolab.com:39441/gotraining
|
||||
# - GODEBUG=gctrace=1
|
||||
|
||||
# This sidecar publishes metrics to the console by default.
|
||||
@ -39,7 +39,7 @@ services:
|
||||
container_name: metrics
|
||||
networks:
|
||||
- shared-network
|
||||
image: gcr.io/sales-api/metrics-amd64:1.0
|
||||
image: gcr.io/web-api/metrics-amd64:1.0
|
||||
ports:
|
||||
- 3001:3001 # EXPVAR API
|
||||
- 4001:4001 # DEBUG API
|
||||
@ -49,12 +49,12 @@ services:
|
||||
container_name: tracer
|
||||
networks:
|
||||
- shared-network
|
||||
image: gcr.io/sales-api/tracer-amd64:1.0
|
||||
image: gcr.io/web-api/tracer-amd64:1.0
|
||||
ports:
|
||||
- 3002:3002 # TRACER API
|
||||
- 4002:4002 # DEBUG API
|
||||
# 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.
|
||||
zipkin:
|
||||
|
@ -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: {}
|
@ -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: {}
|
@ -1,11 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mongo
|
||||
spec:
|
||||
selector:
|
||||
database: mongo
|
||||
ports:
|
||||
- name: "db"
|
||||
port: 27017
|
||||
targetPort: 27017
|
@ -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: {}
|
@ -20,7 +20,7 @@ import (
|
||||
type KeyFunc func(keyID string) (*rsa.PublicKey, error)
|
||||
|
||||
// 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.
|
||||
func NewSingleKeyFunc(id string, key *rsa.PublicKey) KeyFunc {
|
||||
return func(kid string) (*rsa.PublicKey, error) {
|
||||
|
@ -84,7 +84,7 @@ func TestApply(t *testing.T) {
|
||||
Host string `default:"mongo:27017/gotraining" flag:"h"`
|
||||
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}`
|
||||
|
||||
t.Log("Given the need to validate we can apply overrides a struct value.")
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package product
|
||||
package project
|
||||
|
||||
import (
|
||||
"time"
|
||||
@ -6,37 +6,37 @@ import (
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
// Product is an item we sell.
|
||||
type Product struct {
|
||||
// Project is an item we sell.
|
||||
type Project struct {
|
||||
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.
|
||||
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.
|
||||
DateModified time.Time `bson:"date_modified" json:"date_modified"` // When the product record was lost modified.
|
||||
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 project record was lost modified.
|
||||
}
|
||||
|
||||
// NewProduct is what we require from clients when adding a Product.
|
||||
type NewProduct struct {
|
||||
// NewProject is what we require from clients when adding a Project.
|
||||
type NewProject struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Cost int `json:"cost" validate:"required,gte=0"`
|
||||
Quantity int `json:"quantity" validate:"required,gte=1"`
|
||||
}
|
||||
|
||||
// UpdateProduct defines what information may be provided to modify an
|
||||
// existing Product. All fields are optional so clients can send just the
|
||||
// UpdateProject defines what information may be provided to modify an
|
||||
// existing Project. All fields are optional so clients can send just the
|
||||
// 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
|
||||
// explicitly blank. Normally we do not want to use pointers to basic types but
|
||||
// we make exceptions around marshalling/unmarshalling.
|
||||
type UpdateProduct struct {
|
||||
type UpdateProject struct {
|
||||
Name *string `json:"name"`
|
||||
Cost *int `json:"cost" validate:"omitempty,gte=0"`
|
||||
Quantity *int `json:"quantity" validate:"omitempty,gte=1"`
|
||||
}
|
||||
|
||||
// Sale represents a transaction where we sold some quantity of a
|
||||
// Product.
|
||||
// Project.
|
||||
type Sale struct{}
|
||||
|
||||
// NewSale defines what we require when creating a Sale record.
|
@ -1,4 +1,4 @@
|
||||
package product
|
||||
package project
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -12,7 +12,7 @@ import (
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
const productsCollection = "products"
|
||||
const projectsCollection = "projects"
|
||||
|
||||
var (
|
||||
// ErrNotFound abstracts the mgo not found error.
|
||||
@ -22,26 +22,26 @@ var (
|
||||
ErrInvalidID = errors.New("ID is not in its proper form")
|
||||
)
|
||||
|
||||
// List retrieves a list of existing products from the database.
|
||||
func List(ctx context.Context, dbConn *db.DB) ([]Product, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "internal.product.List")
|
||||
// List retrieves a list of existing projects from the database.
|
||||
func List(ctx context.Context, dbConn *db.DB) ([]Project, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "internal.project.List")
|
||||
defer span.End()
|
||||
|
||||
p := []Product{}
|
||||
p := []Project{}
|
||||
|
||||
f := func(collection *mgo.Collection) error {
|
||||
return collection.Find(nil).All(&p)
|
||||
}
|
||||
if err := dbConn.Execute(ctx, productsCollection, f); err != nil {
|
||||
return nil, errors.Wrap(err, "db.products.find()")
|
||||
if err := dbConn.Execute(ctx, projectsCollection, f); err != nil {
|
||||
return nil, errors.Wrap(err, "db.projects.find()")
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Retrieve gets the specified product from the database.
|
||||
func Retrieve(ctx context.Context, dbConn *db.DB, id string) (*Product, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "internal.product.Retrieve")
|
||||
// Retrieve gets the specified project from the database.
|
||||
func Retrieve(ctx context.Context, dbConn *db.DB, id string) (*Project, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "internal.project.Retrieve")
|
||||
defer span.End()
|
||||
|
||||
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)}
|
||||
|
||||
var p *Product
|
||||
var p *Project
|
||||
f := func(collection *mgo.Collection) error {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
// Create inserts a new product into the database.
|
||||
func Create(ctx context.Context, dbConn *db.DB, cp *NewProduct, now time.Time) (*Product, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "internal.product.Create")
|
||||
// Create inserts a new project into the database.
|
||||
func Create(ctx context.Context, dbConn *db.DB, cp *NewProject, now time.Time) (*Project, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "internal.project.Create")
|
||||
defer span.End()
|
||||
|
||||
// Mongo truncates times to milliseconds when storing. We and do the same
|
||||
// here so the value we return is consistent with what we store.
|
||||
now = now.Truncate(time.Millisecond)
|
||||
|
||||
p := Product{
|
||||
p := Project{
|
||||
ID: bson.NewObjectId(),
|
||||
Name: cp.Name,
|
||||
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 {
|
||||
return collection.Insert(&p)
|
||||
}
|
||||
if err := dbConn.Execute(ctx, productsCollection, f); err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("db.products.insert(%s)", db.Query(&p)))
|
||||
if err := dbConn.Execute(ctx, projectsCollection, f); err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("db.projects.insert(%s)", db.Query(&p)))
|
||||
}
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// Update replaces a product document in the database.
|
||||
func Update(ctx context.Context, dbConn *db.DB, id string, upd UpdateProduct, now time.Time) error {
|
||||
ctx, span := trace.StartSpan(ctx, "internal.product.Update")
|
||||
// Update replaces a project document in the database.
|
||||
func Update(ctx context.Context, dbConn *db.DB, id string, upd UpdateProject, now time.Time) error {
|
||||
ctx, span := trace.StartSpan(ctx, "internal.project.Update")
|
||||
defer span.End()
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return ErrNotFound
|
||||
}
|
||||
@ -136,9 +136,9 @@ func Update(ctx context.Context, dbConn *db.DB, id string, upd UpdateProduct, no
|
||||
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 {
|
||||
ctx, span := trace.StartSpan(ctx, "internal.product.Delete")
|
||||
ctx, span := trace.StartSpan(ctx, "internal.project.Delete")
|
||||
defer span.End()
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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
|
129
example-project/internal/project/project_test.go
Normal file
129
example-project/internal/project/project_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
SHELL := /bin/bash
|
||||
|
||||
all: keys sales-api metrics tracer
|
||||
all: keys web-api metrics tracer
|
||||
|
||||
keys:
|
||||
go run ./cmd/sales-admin/main.go --cmd keygen
|
||||
@ -8,10 +8,10 @@ keys:
|
||||
admin:
|
||||
go run ./cmd/sales-admin/main.go --cmd useradd --user_email admin@example.com --user_password gophers
|
||||
|
||||
sales-api:
|
||||
web-api:
|
||||
docker build \
|
||||
-t gcr.io/sales-api/sales-api-amd64:1.0 \
|
||||
--build-arg PACKAGE_NAME=sales-api \
|
||||
-t gcr.io/web-api/web-api-amd64:1.0 \
|
||||
--build-arg PACKAGE_NAME=web-api \
|
||||
--build-arg VCS_REF=`git rev-parse HEAD` \
|
||||
--build-arg BUILD_DATE=`date -u +”%Y-%m-%dT%H:%M:%SZ”` \
|
||||
.
|
||||
@ -19,7 +19,7 @@ sales-api:
|
||||
|
||||
metrics:
|
||||
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_PREFIX=sidecar/ \
|
||||
--build-arg VCS_REF=`git rev-parse HEAD` \
|
||||
@ -28,9 +28,8 @@ metrics:
|
||||
docker system prune -f
|
||||
|
||||
tracer:
|
||||
cd "$$GOPATH/src/geeks-accelerator/oss/saas-starter-kit/example-project"
|
||||
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_PREFIX=sidecar/ \
|
||||
--build-arg VCS_REF=`git rev-parse HEAD` \
|
||||
@ -61,27 +60,27 @@ remove-all:
|
||||
# GKE
|
||||
|
||||
config:
|
||||
@echo Setting environment for sales-api
|
||||
gcloud config set project sales-api
|
||||
@echo Setting environment for web-api
|
||||
gcloud config set project web-api
|
||||
gcloud config set compute/zone us-central1-b
|
||||
gcloud auth configure-docker
|
||||
@echo ======================================================================
|
||||
|
||||
project:
|
||||
gcloud projects create sales-api
|
||||
gcloud beta billing projects link sales-api --billing-account=$(ACCOUNT_ID)
|
||||
gcloud projects create web-api
|
||||
gcloud beta billing projects link web-api --billing-account=$(ACCOUNT_ID)
|
||||
gcloud services enable container.googleapis.com
|
||||
@echo ======================================================================
|
||||
|
||||
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
|
||||
@echo ======================================================================
|
||||
|
||||
upload:
|
||||
docker push gcr.io/sales-api/sales-api-amd64:1.0
|
||||
docker push gcr.io/sales-api/metrics-amd64:1.0
|
||||
docker push gcr.io/sales-api/tracer-amd64:1.0
|
||||
docker push gcr.io/web-api/web-api-amd64:1.0
|
||||
docker push gcr.io/web-api/metrics-amd64:1.0
|
||||
docker push gcr.io/web-api/tracer-amd64:1.0
|
||||
@echo ======================================================================
|
||||
|
||||
database:
|
||||
@ -90,8 +89,8 @@ database:
|
||||
@echo ======================================================================
|
||||
|
||||
services:
|
||||
kubectl create -f gke-deploy-sales-api.yaml
|
||||
kubectl expose -f gke-expose-sales-api.yaml --type=LoadBalancer
|
||||
kubectl create -f gke-deploy-web-api.yaml
|
||||
kubectl expose -f gke-expose-web-api.yaml --type=LoadBalancer
|
||||
@echo ======================================================================
|
||||
|
||||
shell:
|
||||
@ -102,15 +101,15 @@ status:
|
||||
gcloud container clusters list
|
||||
kubectl get nodes
|
||||
kubectl get pods
|
||||
kubectl get services sales-api
|
||||
kubectl get services web-api
|
||||
@echo ======================================================================
|
||||
|
||||
delete:
|
||||
kubectl delete services sales-api
|
||||
kubectl delete deployment sales-api
|
||||
gcloud container clusters delete sales-api-cluster
|
||||
gcloud projects delete sales-api
|
||||
docker image remove gcr.io/sales-api/sales-api-amd64:1.0
|
||||
docker image remove gcr.io/sales-api/metrics-amd64:1.0
|
||||
docker image remove gcr.io/sales-api/tracer-amd64:1.0
|
||||
kubectl delete services web-api
|
||||
kubectl delete deployment web-api
|
||||
gcloud container clusters delete web-api-cluster
|
||||
gcloud projects delete web-api
|
||||
docker image remove gcr.io/web-api/web-api-amd64:1.0
|
||||
docker image remove gcr.io/web-api/metrics-amd64:1.0
|
||||
docker image remove gcr.io/web-api/tracer-amd64:1.0
|
||||
@echo ======================================================================
|
||||
|
Reference in New Issue
Block a user