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

Added example web-app service

Example web-app required provided the ability to render html.
This commit is contained in:
Lee Brown
2019-05-18 18:06:10 -04:00
parent b40d389579
commit 308fee852c
19 changed files with 1400 additions and 22 deletions

View File

@ -45,14 +45,14 @@ func main() {
// ========================================================================= // =========================================================================
// Logging // Logging
log := log.New(os.Stdout, "WEB_APP : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) log := log.New(os.Stdout, "WEB_API : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
// ========================================================================= // =========================================================================
// Configuration // Configuration
var cfg struct { var cfg struct {
Web struct { HTTP struct {
APIHost string `default:"0.0.0.0:3000" envconfig:"API_HOST"` Host string `default:"0.0.0.0:3001" envconfig:"HTTP_HOST"`
DebugHost string `default:"0.0.0.0:4000" envconfig:"DEBUG_HOST"` DebugHost string `default:"0.0.0.0:4000" envconfig:"DEBUG_HOST"`
ReadTimeout time.Duration `default:"5s" envconfig:"READ_TIMEOUT"` ReadTimeout time.Duration `default:"5s" envconfig:"READ_TIMEOUT"`
WriteTimeout time.Duration `default:"5s" envconfig:"WRITE_TIMEOUT"` WriteTimeout time.Duration `default:"5s" envconfig:"WRITE_TIMEOUT"`
@ -164,10 +164,12 @@ func main() {
// //
// /debug/vars - Added to the default mux by the expvars package. // /debug/vars - Added to the default mux by the expvars package.
// /debug/pprof - Added to the default mux by the net/http/pprof package. // /debug/pprof - Added to the default mux by the net/http/pprof package.
go func() { if cfg.HTTP.DebugHost != "" {
log.Printf("main : Debug Listening %s", cfg.Web.DebugHost) go func() {
log.Printf("main : Debug Listener closed : %v", http.ListenAndServe(cfg.Web.DebugHost, http.DefaultServeMux)) log.Printf("main : Debug Listening %s", cfg.HTTP.DebugHost)
}() log.Printf("main : Debug Listener closed : %v", http.ListenAndServe(cfg.HTTP.DebugHost, http.DefaultServeMux))
}()
}
// ========================================================================= // =========================================================================
// Start API Service // Start API Service
@ -178,10 +180,10 @@ func main() {
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
api := http.Server{ api := http.Server{
Addr: cfg.Web.APIHost, Addr: cfg.HTTP.Host,
Handler: handlers.API(shutdown, log, masterDB, authenticator), Handler: handlers.API(shutdown, log, masterDB, authenticator),
ReadTimeout: cfg.Web.ReadTimeout, ReadTimeout: cfg.HTTP.ReadTimeout,
WriteTimeout: cfg.Web.WriteTimeout, WriteTimeout: cfg.HTTP.WriteTimeout,
MaxHeaderBytes: 1 << 20, MaxHeaderBytes: 1 << 20,
} }
@ -191,7 +193,7 @@ func main() {
// Start the service listening for requests. // Start the service listening for requests.
go func() { go func() {
log.Printf("main : API Listening %s", cfg.Web.APIHost) log.Printf("main : API Listening %s", cfg.HTTP.Host)
serverErrors <- api.ListenAndServe() serverErrors <- api.ListenAndServe()
}() }()
@ -207,13 +209,13 @@ func main() {
log.Printf("main : %v : Start shutdown..", sig) log.Printf("main : %v : Start shutdown..", sig)
// Create context for Shutdown call. // Create context for Shutdown call.
ctx, cancel := context.WithTimeout(context.Background(), cfg.Web.ShutdownTimeout) ctx, cancel := context.WithTimeout(context.Background(), cfg.HTTP.ShutdownTimeout)
defer cancel() defer cancel()
// Asking listener to shutdown and load shed. // Asking listener to shutdown and load shed.
err := api.Shutdown(ctx) err := api.Shutdown(ctx)
if err != nil { if err != nil {
log.Printf("main : Graceful shutdown did not complete in %v : %v", cfg.Web.ShutdownTimeout, err) log.Printf("main : Graceful shutdown did not complete in %v : %v", cfg.HTTP.ShutdownTimeout, err)
err = api.Close() err = api.Close()
} }

View File

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

View File

@ -0,0 +1,25 @@
package handlers
import (
"context"
"net/http"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"go.opencensus.io/trace"
)
// User represents the User API method handler set.
type Root struct {
MasterDB *db.DB
Renderer web.Renderer
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
}
// List returns all the existing users in the system.
func (u *Root) Index(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.Root.Index")
defer span.End()
return u.Renderer.Render(ctx, w, r, baseLayoutTmpl, "root-index.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil)
}

View File

@ -0,0 +1,52 @@
package handlers
import (
"log"
"net/http"
"os"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
)
const baseLayoutTmpl = "base.tmpl"
// API returns a handler for a set of routes.
func APP(shutdown chan os.Signal, log *log.Logger, staticDir, templateDir string, masterDB *db.DB, authenticator *auth.Authenticator, renderer web.Renderer) http.Handler {
// Construct the web.App which holds all routes as well as common Middleware.
app := web.NewApp(shutdown, log, mid.Logger(log), mid.Errors(log), mid.Metrics(), mid.Panics())
// Register health check endpoint. This route is not authenticated.
check := Check{
MasterDB: masterDB,
Renderer: renderer,
}
app.Handle("GET", "/v1/health", check.Health)
// Register user management and authentication endpoints.
u := User{
MasterDB: masterDB,
Renderer: renderer,
}
// This route is not authenticated
app.Handle("POST", "/users/login", u.Login)
app.Handle("GET", "/users/login", u.Login)
// Register root
r := Root{
MasterDB: masterDB,
Renderer: renderer,
}
// This route is not authenticated
app.Handle("GET", "/index.html", r.Index)
app.Handle("GET", "/", r.Index)
// Static file server
app.Handle("GET", "/*", web.Static(staticDir,""))
return app
}

View File

@ -0,0 +1,28 @@
package handlers
import (
"context"
"net/http"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"go.opencensus.io/trace"
)
// User represents the User API method handler set.
type User struct {
MasterDB *db.DB
Renderer web.Renderer
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
}
// List returns all the existing users in the system.
func (u *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "handlers.User.Login")
defer span.End()
//dbConn := u.MasterDB.Copy()
//defer dbConn.Close()
return u.Renderer.Render(ctx, w, r, baseLayoutTmpl, "user-login.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil)
}

View File

@ -0,0 +1 @@
console.log("test");

View File

@ -0,0 +1,10 @@
{{define "title"}}Welcome{{end}}
{{define "style"}}
{{end}}
{{define "content"}}
Welcome to the web app
{{end}}
{{define "js"}}
{{end}}

View File

@ -0,0 +1,10 @@
{{define "title"}}User Login{{end}}
{{define "style"}}
{{end}}
{{define "content"}}
Login to this amazing web app
{{end}}
{{define "js"}}
{{end}}

View File

@ -0,0 +1,48 @@
{{ define "base" }}
<!DOCTYPE html>
<html lang="en">
<head>
<title>
{{block "title" .}}{{end}} Web App
</title>
<meta name="description" content="{{block "description" .}}{{end}} ">
<meta name="author" content="{{block "author" .}}{{end}}">
<meta charset="utf-8">
<link rel="icon" type="image/png" sizes="16x16" href="{{ SiteAssetUrl "/assets/images/favicon.png" }}">
<!-- ============================================================== -->
<!-- CSS -->
<!-- ============================================================== -->
<link href="{{ SiteAssetUrl "/assets/css/base.css" }}" id="theme" rel="stylesheet">
<!-- ============================================================== -->
<!-- Page specific CSS -->
<!-- ============================================================== -->
{{block "style" .}} {{end}}
</head>
<body>
<!-- ============================================================== -->
<!-- Page content -->
<!-- ============================================================== -->
{{ template "content" . }}
<!-- ============================================================== -->
<!-- footer -->
<!-- ============================================================== -->
<footer class="footer">
© 2019 Keeni Space<br/>
{{ template "partials/buildinfo" . }}
</footer>
<!-- ============================================================== -->
<!-- Javascript -->
<!-- ============================================================== -->
<script src="{{ SiteAssetUrl "/js/base.js" }}"></script>
<!-- ============================================================== -->
<!-- Page specific Javascript -->
<!-- ============================================================== -->
{{block "js" .}} {{end}}
</body>
</html>
{{end}}

View File

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

View File

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

View File

@ -6,17 +6,21 @@ require (
github.com/go-playground/locales v0.12.1 github.com/go-playground/locales v0.12.1
github.com/go-playground/universal-translator v0.16.0 github.com/go-playground/universal-translator v0.16.0
github.com/google/go-cmp v0.2.0 github.com/google/go-cmp v0.2.0
github.com/hashicorp/golang-lru v0.5.1 // indirect
github.com/kelseyhightower/envconfig v1.3.0 github.com/kelseyhightower/envconfig v1.3.0
github.com/kr/pretty v0.1.0 // indirect github.com/kr/pretty v0.1.0 // indirect
github.com/leodido/go-urn v1.1.0 // indirect github.com/leodido/go-urn v1.1.0 // indirect
github.com/openzipkin/zipkin-go v0.1.1 github.com/openzipkin/zipkin-go v0.1.1
github.com/pborman/uuid v0.0.0-20180122190007-c65b2f87fee3 github.com/pborman/uuid v0.0.0-20180122190007-c65b2f87fee3
github.com/philhofer/fwd v1.0.0 // indirect
github.com/pkg/errors v0.8.0 github.com/pkg/errors v0.8.0
github.com/stretchr/testify v1.3.0 // indirect github.com/stretchr/testify v1.3.0 // indirect
github.com/tinylib/msgp v1.1.0 // indirect
go.opencensus.io v0.14.0 go.opencensus.io v0.14.0
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 // indirect golang.org/x/net v0.0.0-20180724234803-3673e40ba225 // indirect
golang.org/x/text v0.3.0 // indirect golang.org/x/text v0.3.0 // indirect
gopkg.in/DataDog/dd-trace-go.v1 v1.13.1
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v9 v9.28.0 gopkg.in/go-playground/validator.v9 v9.28.0

View File

@ -10,6 +10,8 @@ github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rm
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM= github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM=
github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -23,6 +25,8 @@ github.com/openzipkin/zipkin-go v0.1.1 h1:A/ADD6HaPnAKj3yS7HjGHRK77qi41Hi0DirOOI
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pborman/uuid v0.0.0-20180122190007-c65b2f87fee3 h1:9J0mOv1rXIBlRjQCiAGyx9C3dZZh5uIa3HU0oTV8v1E= github.com/pborman/uuid v0.0.0-20180122190007-c65b2f87fee3 h1:9J0mOv1rXIBlRjQCiAGyx9C3dZZh5uIa3HU0oTV8v1E=
github.com/pborman/uuid v0.0.0-20180122190007-c65b2f87fee3/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= github.com/pborman/uuid v0.0.0-20180122190007-c65b2f87fee3/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34=
github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -30,6 +34,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
go.opencensus.io v0.14.0 h1:1eTLxqxSIAylcKoxnNkdhvvBNZDA8JwkKNXxgyma0IA= go.opencensus.io v0.14.0 h1:1eTLxqxSIAylcKoxnNkdhvvBNZDA8JwkKNXxgyma0IA=
go.opencensus.io v0.14.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0= go.opencensus.io v0.14.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0=
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ= golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ=
@ -38,6 +44,8 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmy
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/DataDog/dd-trace-go.v1 v1.13.1 h1:oTzOClfuudNhW9Skkp2jxjqYO92uDKXqKLbiuPA13Rk=
gopkg.in/DataDog/dd-trace-go.v1 v1.13.1/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -0,0 +1,8 @@
package deploy
/*
func () {
func (c *CloudFront) ListDistributions(input *ListDistributionsInput)
} */

View File

@ -0,0 +1,12 @@
package web
import (
"context"
"net/http"
)
type Renderer interface {
Render(ctx context.Context, w http.ResponseWriter, req *http.Request, templateLayoutName, templateContentName, contentType string, statusCode int, data map[string]interface{}) error
Error(ctx context.Context, w http.ResponseWriter, req *http.Request, statusCode int, er error) error
Static(rootDir, prefix string) Handler
}

View File

@ -3,13 +3,32 @@ package web
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"net/http" "fmt"
"github.com/pkg/errors" "github.com/pkg/errors"
"net/http"
"os"
"path"
"path/filepath"
"strings"
) )
// RespondError sends an error reponse back to the client. const (
func RespondError(ctx context.Context, w http.ResponseWriter, err error) error { charsetUTF8 = "charset=UTF-8"
)
// MIME types
const (
MIMEApplicationJSON = "application/json"
MIMEApplicationJSONCharsetUTF8 = MIMEApplicationJSON + "; " + charsetUTF8
MIMETextHTML = "text/html"
MIMETextHTMLCharsetUTF8 = MIMETextHTML + "; " + charsetUTF8
MIMETextPlain = "text/plain"
MIMETextPlainCharsetUTF8 = MIMETextPlain + "; " + charsetUTF8
MIMEOctetStream = "application/octet-stream"
)
// RespondJsonError sends an error formatted as JSON response back to the client.
func RespondJsonError(ctx context.Context, w http.ResponseWriter, err error) error {
// If the error was of the type *Error, the handler has // If the error was of the type *Error, the handler has
// a specific status code and error to return. // a specific status code and error to return.
@ -18,7 +37,7 @@ func RespondError(ctx context.Context, w http.ResponseWriter, err error) error {
Error: webErr.Err.Error(), Error: webErr.Err.Error(),
Fields: webErr.Fields, Fields: webErr.Fields,
} }
if err := Respond(ctx, w, er, webErr.Status); err != nil { if err := RespondJson(ctx, w, er, webErr.Status); err != nil {
return err return err
} }
return nil return nil
@ -28,15 +47,15 @@ func RespondError(ctx context.Context, w http.ResponseWriter, err error) error {
er := ErrorResponse{ er := ErrorResponse{
Error: http.StatusText(http.StatusInternalServerError), Error: http.StatusText(http.StatusInternalServerError),
} }
if err := Respond(ctx, w, er, http.StatusInternalServerError); err != nil { if err := RespondJson(ctx, w, er, http.StatusInternalServerError); err != nil {
return err return err
} }
return nil return nil
} }
// Respond converts a Go value to JSON and sends it to the client. // RespondJson converts a Go value to JSON and sends it to the client.
// If code is StatusNoContent, v is expected to be nil. // If code is StatusNoContent, v is expected to be nil.
func Respond(ctx context.Context, w http.ResponseWriter, data interface{}, statusCode int) error { func RespondJson(ctx context.Context, w http.ResponseWriter, data interface{}, statusCode int) error {
// Set the status code for the request logger middleware. // Set the status code for the request logger middleware.
// If the context is missing this value, request the service // If the context is missing this value, request the service
@ -60,7 +79,7 @@ func Respond(ctx context.Context, w http.ResponseWriter, data interface{}, statu
} }
// Set the content type and headers once we know marshaling has succeeded. // Set the content type and headers once we know marshaling has succeeded.
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", MIMEApplicationJSONCharsetUTF8)
// Write the status code to the response. // Write the status code to the response.
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
@ -72,3 +91,102 @@ func Respond(ctx context.Context, w http.ResponseWriter, data interface{}, statu
return nil return nil
} }
// RespondError sends an error back to the client as plain text with
// the status code 500 Internal Service Error
func RespondError(ctx context.Context, w http.ResponseWriter, er error) error {
return RespondErrorStatus(ctx, w, er, http.StatusInternalServerError)
}
// RespondErrorStatus sends an error back to the client as plain text with
// the specified HTTP status code.
func RespondErrorStatus(ctx context.Context, w http.ResponseWriter, er error, statusCode int) error {
msg := fmt.Sprintf("%s", er)
if err := Respond(ctx, w, []byte(msg), statusCode, MIMETextPlainCharsetUTF8); err != nil {
return err
}
return nil
}
// Respond writes the data to the client with the specified HTTP status code and
// content type.
func Respond(ctx context.Context, w http.ResponseWriter, data []byte, statusCode int, contentType string) error {
// Set the status code for the request logger middleware.
// If the context is missing this value, request the service
// to be shutdown gracefully.
v, ok := ctx.Value(KeyValues).(*Values)
if !ok {
return NewShutdownError("web value missing from context")
}
v.StatusCode = statusCode
// If there is nothing to marshal then set status code and return.
if statusCode == http.StatusNoContent {
w.WriteHeader(statusCode)
return nil
}
// Set the content type and headers once we know marshaling has succeeded.
w.Header().Set("Content-Type", contentType)
// Write the status code to the response.
w.WriteHeader(statusCode)
// Send the result back to the client.
if _, err := w.Write(data); err != nil {
return err
}
return nil
}
// Static registers a new route with path prefix to serve static files from the
// provided root directory. All errors will result in 404 File Not Found.
func Static(rootDir, prefix string) Handler {
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
err := StaticHandler(ctx, w, r, params, rootDir, prefix)
if err != nil {
return RespondErrorStatus(ctx, w, err, http.StatusNotFound)
}
return nil
}
return h
}
// StaticHandler sends a static file wo the client. The error is returned directly
// from this function allowing it to be wrapped by a Handler. The handler then was the
// the ability to format/display the error before responding to the client.
func StaticHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string, rootDir, prefix string) error {
// Parse the URL from the http request.
urlPath := path.Clean("/"+r.URL.Path) // "/"+ for security
urlPath = strings.TrimLeft(urlPath, "/")
// Remove the static directory name from the url
urlPath = strings.TrimLeft(urlPath, filepath.Base(rootDir))
// Also remove the URL prefix used to serve the static file since
// this does not need to match any existing directory structure.
if prefix != "" {
urlPath = strings.TrimLeft(urlPath, prefix)
}
// Resolve the root directory to an absolute path
sd, err := filepath.Abs(rootDir)
if err != nil {
return err
}
// Append the requested file to the root directory
filePath := filepath.Join(sd, urlPath)
// Make sure the file exists before attempting to serve it so
// have the opportunity to handle the when a file does not exist.
if _, err := os.Stat(filePath); err != nil {
return err
}
// Serve the file from the local file system.
http.ServeFile(w, r , filePath)
return nil
}

View File

@ -0,0 +1,5 @@
requires the following directories in the template directory
content
layouts
partials

View File

@ -0,0 +1,337 @@
package template_renderer
import (
"context"
"fmt"
"html/template"
"math"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"github.com/pkg/errors"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
var (
errInvalidTemplate = errors.New("Invalid template")
// Base template to support applying custom
// TODO try to remove this
//mainTmpl = `{{define "main" }} {{ template "base" . }} {{ end }}`
)
type Template struct {
Funcs template.FuncMap
mainTemplate *template.Template
}
func NewTemplate(templateFuncs template.FuncMap) *Template {
t := &Template{}
// these functions are used and rendered on run-time of web page so don't have to use javascript/jquery
// to for basic template formatting. transformation happens server-side instead of client-side to
// provide base-level consistency.
t.Funcs = template.FuncMap{
// probably could provide examples of each of these
"Minus": func(a, b int) int {
return a - b
},
"Add": func(a, b int) int {
return a + b
},
"Mod": func(a, b int) int {
return int(math.Mod(float64(a), float64(b)))
},
"AssetUrl": func(p string) string {
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
return p
},
"AppAssetUrl": func(p string) string {
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
return p
},
"SiteS3Url": func(p string) string {
return p
},
"S3Url": func(p string) string {
return p
},
"AppBaseUrl": func(p string) string {
return p
},
"Http2Https": func(u string) string {
return strings.Replace(u, "http:", "https:", 1)
},
"StringHasPrefix": func(str, match string) bool {
if strings.HasPrefix(str, match) {
return true
}
return false
},
"StringHasSuffix": func(str, match string) bool {
if strings.HasSuffix(str, match) {
return true
}
return false
},
"StringContains": func(str, match string) bool {
if strings.Contains(str, match) {
return true
}
return false
},
"NavPageClass": func(uri, uriMatch, uriClass string) string {
u, err := url.Parse(uri)
if err != nil {
return "?"
}
if strings.HasPrefix(u.Path, uriMatch) {
return uriClass
}
return ""
},
"UrlEncode": func(k string) string {
return url.QueryEscape(k)
},
"html": func(value interface{}) template.HTML {
return template.HTML(fmt.Sprint(value))
},
}
for fn, f := range templateFuncs {
t.Funcs[fn] = f
}
return t
}
// TemplateRenderer is a custom html/template renderer for Echo framework
type TemplateRenderer struct {
templateDir string
// has to be map so can know the name and map the name to the location / file path
layoutFiles map[string]string
contentFiles map[string]string
partialFiles map[string]string
enableHotReload bool
templates map[string]*template.Template
globalViewData map[string]interface{}
//mainTemplate *template.Template
errorHandler func(ctx context.Context, w http.ResponseWriter, req *http.Request, renderer web.Renderer, statusCode int, er error) error
}
func NewTemplateRenderer(templateDir string, enableHotReload bool, globalViewData map[string]interface{}, tmpl *Template, errorHandler func(ctx context.Context, w http.ResponseWriter, req *http.Request, renderer web.Renderer, statusCode int, er error) error) (*TemplateRenderer, error) {
r := &TemplateRenderer{
templateDir: templateDir,
layoutFiles: make( map[string]string),
contentFiles: make( map[string]string),
partialFiles: make( map[string]string),
enableHotReload: enableHotReload,
templates: make(map[string]*template.Template),
globalViewData:globalViewData,
errorHandler: errorHandler,
}
//r.mainTemplate = template.New("main")
//r.mainTemplate, _ = r.mainTemplate.Parse(mainTmpl)
//r.mainTemplate.Funcs(tmpl.Funcs)
err := filepath.Walk(templateDir, func(path string, info os.FileInfo, err error) error {
dir := filepath.Base(filepath.Dir(path))
if info.IsDir() {
return nil
}
baseName := filepath.Base(path)
if dir == "content" {
r.contentFiles[baseName] = path
} else if dir == "layouts" {
r.layoutFiles[baseName] = path
} else if dir == "partials" {
r.partialFiles[baseName] = path
}
return err
})
if err != nil {
return r, err
}
// Ensure all layout files render successfully with no errors.
for _, f := range r.layoutFiles {
//t, err := r.mainTemplate.Clone()
//if err != nil {
// return r, err
//}
t := template.New("main")
t.Funcs(tmpl.Funcs)
template.Must(t.ParseFiles(f))
}
// Ensure all partial files render successfully with no errors.
for _, f := range r.partialFiles {
//t, err := r.mainTemplate.Clone()
//if err != nil {
// return r, err
//}
t := template.New("partial")
t.Funcs(tmpl.Funcs)
template.Must(t.ParseFiles(f))
}
// Ensure all content files render successfully with no errors.
for _, f := range r.contentFiles {
//t, err := r.mainTemplate.Clone()
//if err != nil {
// return r, err
//}
t := template.New("content")
t.Funcs(tmpl.Funcs)
template.Must(t.ParseFiles(f))
}
return r, nil
}
// Render renders a template document
func (r *TemplateRenderer) Render(ctx context.Context, w http.ResponseWriter, req *http.Request, templateLayoutName, templateContentName, contentType string, statusCode int, data map[string]interface{}) error {
t, ok := r.templates[templateContentName]
if !ok || r.enableHotReload {
layoutFile, ok := r.layoutFiles[templateLayoutName]
if !ok {
return errors.Wrapf(errInvalidTemplate, "template layout file for %s does not exist", templateLayoutName)
}
files := []string{layoutFile}
for _, f := range r.partialFiles {
files = append(files, f)
}
contentFile, ok := r.contentFiles[templateContentName]
if !ok {
return errors.Wrapf(errInvalidTemplate, "template content file for %s does not exist", templateContentName)
}
files = append(files, contentFile)
t = template.Must(t.ParseFiles(files...))
r.templates[templateContentName] = t
}
opts := []ddtrace.StartSpanOption{
tracer.SpanType(ext.SpanTypeWeb),
tracer.ResourceName(templateContentName),
}
var span tracer.Span
span, ctx = tracer.StartSpanFromContext(ctx, "web.Render", opts...)
defer span.Finish()
// Specific new data map for render to allow values to be overwritten on a request
// basis.
// append the global key/pairs
renderData := r.globalViewData
if renderData == nil {
renderData = make(map[string]interface{})
}
// Add Request URL to render data
reqData := map[string]interface{}{
"Url": "",
"Uri": "",
}
if req != nil {
reqData["Url"] = req.URL.String()
reqData["Uri"] = req.URL.RequestURI()
}
renderData["_Request"] = reqData
// Add context to render data, this supports template functions having the ability
// to define context.Context as an argument
renderData["_Ctx"] = ctx
// Append request data map to render data last so any previous value can be overwritten.
if data != nil {
for k, v := range data {
renderData[k] = v
}
}
// Render template with data.
err := t.Execute(w, renderData)
if err != nil {
return err
}
return nil
}
func (r *TemplateRenderer) Error(ctx context.Context, w http.ResponseWriter, req *http.Request, statusCode int, er error) error {
// If error hander was defined to support formated response for web, used it.
if r.errorHandler != nil {
return r.errorHandler(ctx, w, req, r, statusCode, er)
}
// Default response text response of error.
return web.RespondError(ctx, w, er)
}
func (tr *TemplateRenderer) Static(rootDir, prefix string) web.Handler {
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
err := web.StaticHandler(ctx, w, r, params, rootDir, prefix)
if err != nil {
return tr.Error(ctx, w, r, http.StatusNotFound, err)
}
return nil
}
return h
}
func S3Url(baseS3Url, baseS3Origin, p string) string {
if strings.HasPrefix(p, "http") {
return p
}
org := strings.TrimRight(baseS3Origin, "/")
if org != "" {
p = strings.Replace(p, org+"/", "", 1)
}
pts := strings.Split(p, "?")
p = pts[0]
var rq string
if len(pts) > 1 {
rq = pts[1]
}
p = strings.TrimLeft(p, "/")
baseUrl := baseS3Url
u, err := url.Parse(baseUrl)
if err != nil {
return "?"
}
ldir := filepath.Base(u.Path)
if strings.HasPrefix(p, ldir) {
p = strings.Replace(p, ldir+"/", "", 1)
}
u.Path = filepath.Join(u.Path, p)
u.RawQuery = rq
return u.String()
}