diff --git a/example-project/.gitignore b/example-project/.gitignore new file mode 100644 index 0000000..a2496ec --- /dev/null +++ b/example-project/.gitignore @@ -0,0 +1 @@ +private.pem diff --git a/example-project/README.md b/example-project/README.md index 0a065b6..c4bb9b1 100644 --- a/example-project/README.md +++ b/example-project/README.md @@ -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. diff --git a/example-project/cmd/sales-admin/main.go b/example-project/cmd/sales-admin/main.go deleted file mode 100644 index 8495703..0000000 --- a/example-project/cmd/sales-admin/main.go +++ /dev/null @@ -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 -} diff --git a/example-project/cmd/sidecar/metrics/main.go b/example-project/cmd/sidecar/metrics/main.go index 19eac69..30a546f 100644 --- a/example-project/cmd/sidecar/metrics/main.go +++ b/example-project/cmd/sidecar/metrics/main.go @@ -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"` diff --git a/example-project/cmd/sidecar/tracer/handlers/zipkin.go b/example-project/cmd/sidecar/tracer/handlers/zipkin.go index 5531c7b..92599a4 100644 --- a/example-project/cmd/sidecar/tracer/handlers/zipkin.go +++ b/example-project/cmd/sidecar/tracer/handlers/zipkin.go @@ -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 } diff --git a/example-project/cmd/sales-api/handlers/check.go b/example-project/cmd/web-api/handlers/check.go similarity index 100% rename from example-project/cmd/sales-api/handlers/check.go rename to example-project/cmd/web-api/handlers/check.go diff --git a/example-project/cmd/sales-api/handlers/product.go b/example-project/cmd/web-api/handlers/project.go similarity index 59% rename from example-project/cmd/sales-api/handlers/product.go rename to example-project/cmd/web-api/handlers/project.go index fff8f61..b50098b 100644 --- a/example-project/cmd/sales-api/handlers/product.go +++ b/example-project/cmd/web-api/handlers/project.go @@ -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"]) diff --git a/example-project/cmd/sales-api/handlers/routes.go b/example-project/cmd/web-api/handlers/routes.go similarity index 82% rename from example-project/cmd/sales-api/handlers/routes.go rename to example-project/cmd/web-api/handlers/routes.go index 37fc4f0..afb72c8 100644 --- a/example-project/cmd/sales-api/handlers/routes.go +++ b/example-project/cmd/web-api/handlers/routes.go @@ -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 } diff --git a/example-project/cmd/sales-api/handlers/user.go b/example-project/cmd/web-api/handlers/user.go similarity index 100% rename from example-project/cmd/sales-api/handlers/user.go rename to example-project/cmd/web-api/handlers/user.go diff --git a/example-project/cmd/sales-api/main.go b/example-project/cmd/web-api/main.go similarity index 97% rename from example-project/cmd/sales-api/main.go rename to example-project/cmd/web-api/main.go index 99c987a..6992515 100644 --- a/example-project/cmd/sales-api/main.go +++ b/example-project/cmd/web-api/main.go @@ -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) } diff --git a/example-project/cmd/sales-api/tests/product_test.go b/example-project/cmd/web-api/tests/project_test.go similarity index 69% rename from example-project/cmd/sales-api/tests/product_test.go rename to example-project/cmd/web-api/tests/project_test.go index 756f57b..92ea9a9 100644 --- a/example-project/cmd/sales-api/tests/product_test.go +++ b/example-project/cmd/web-api/tests/project_test.go @@ -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) } diff --git a/example-project/cmd/sales-api/tests/tests_test.go b/example-project/cmd/web-api/tests/tests_test.go similarity index 96% rename from example-project/cmd/sales-api/tests/tests_test.go rename to example-project/cmd/web-api/tests/tests_test.go index 9362365..520fb6a 100644 --- a/example-project/cmd/sales-api/tests/tests_test.go +++ b/example-project/cmd/web-api/tests/tests_test.go @@ -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" diff --git a/example-project/cmd/sales-api/tests/user_test.go b/example-project/cmd/web-api/tests/user_test.go similarity index 100% rename from example-project/cmd/sales-api/tests/user_test.go rename to example-project/cmd/web-api/tests/user_test.go diff --git a/example-project/docker-compose.yaml b/example-project/docker-compose.yaml index 4813ce7..45a45ed 100644 --- a/example-project/docker-compose.yaml +++ b/example-project/docker-compose.yaml @@ -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: diff --git a/example-project/gke-deploy-database.yaml b/example-project/gke-deploy-database.yaml deleted file mode 100644 index 8ad40b8..0000000 --- a/example-project/gke-deploy-database.yaml +++ /dev/null @@ -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: {} \ No newline at end of file diff --git a/example-project/gke-deploy-sales-api.yaml b/example-project/gke-deploy-sales-api.yaml deleted file mode 100644 index f41daf9..0000000 --- a/example-project/gke-deploy-sales-api.yaml +++ /dev/null @@ -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: {} diff --git a/example-project/gke-expose-database.yaml b/example-project/gke-expose-database.yaml deleted file mode 100644 index fbf1245..0000000 --- a/example-project/gke-expose-database.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: mongo -spec: - selector: - database: mongo - ports: - - name: "db" - port: 27017 - targetPort: 27017 \ No newline at end of file diff --git a/example-project/gke-expose-sales-api.yaml b/example-project/gke-expose-sales-api.yaml deleted file mode 100644 index c57d239..0000000 --- a/example-project/gke-expose-sales-api.yaml +++ /dev/null @@ -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: {} \ No newline at end of file diff --git a/example-project/internal/platform/auth/auth.go b/example-project/internal/platform/auth/auth.go index 2e38195..249f4f5 100644 --- a/example-project/internal/platform/auth/auth.go +++ b/example-project/internal/platform/auth/auth.go @@ -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) { diff --git a/example-project/internal/platform/flag/flag_test.go b/example-project/internal/platform/flag/flag_test.go index 69296f6..3fc8683 100644 --- a/example-project/internal/platform/flag/flag_test.go +++ b/example-project/internal/platform/flag/flag_test.go @@ -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.") diff --git a/example-project/internal/product/product_test.go b/example-project/internal/product/product_test.go deleted file mode 100644 index 0fdfb20..0000000 --- a/example-project/internal/product/product_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/example-project/internal/product/models.go b/example-project/internal/project/models.go similarity index 74% rename from example-project/internal/product/models.go rename to example-project/internal/project/models.go index e398aeb..f343f12 100644 --- a/example-project/internal/product/models.go +++ b/example-project/internal/project/models.go @@ -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. diff --git a/example-project/internal/product/product.go b/example-project/internal/project/project.go similarity index 63% rename from example-project/internal/product/product.go rename to example-project/internal/project/project.go index f1a7810..4e1e9d8 100644 --- a/example-project/internal/product/product.go +++ b/example-project/internal/project/project.go @@ -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 diff --git a/example-project/internal/project/project_test.go b/example-project/internal/project/project_test.go new file mode 100644 index 0000000..3b0f482 --- /dev/null +++ b/example-project/internal/project/project_test.go @@ -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) + } + } +} diff --git a/example-project/makefile b/example-project/makefile index 21378a7..e51c7ef 100644 --- a/example-project/makefile +++ b/example-project/makefile @@ -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 ======================================================================