You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-06-15 00:15:15 +02:00
Added example web-app service
Example web-app required provided the ability to render html.
This commit is contained in:
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
37
example-project/cmd/web-app/handlers/check.go
Normal file
37
example-project/cmd/web-app/handlers/check.go
Normal 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)
|
||||||
|
}
|
25
example-project/cmd/web-app/handlers/root.go
Normal file
25
example-project/cmd/web-app/handlers/root.go
Normal 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)
|
||||||
|
}
|
52
example-project/cmd/web-app/handlers/routes.go
Normal file
52
example-project/cmd/web-app/handlers/routes.go
Normal 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
|
||||||
|
}
|
28
example-project/cmd/web-app/handlers/user.go
Normal file
28
example-project/cmd/web-app/handlers/user.go
Normal 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)
|
||||||
|
}
|
1
example-project/cmd/web-app/static/assets/js/base.js
Normal file
1
example-project/cmd/web-app/static/assets/js/base.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
console.log("test");
|
@ -0,0 +1,10 @@
|
|||||||
|
{{define "title"}}Welcome{{end}}
|
||||||
|
{{define "style"}}
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
Welcome to the web app
|
||||||
|
{{end}}
|
||||||
|
{{define "js"}}
|
||||||
|
|
||||||
|
{{end}}
|
@ -0,0 +1,10 @@
|
|||||||
|
{{define "title"}}User Login{{end}}
|
||||||
|
{{define "style"}}
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
Login to this amazing web app
|
||||||
|
{{end}}
|
||||||
|
{{define "js"}}
|
||||||
|
|
||||||
|
{{end}}
|
48
example-project/cmd/web-app/templates/layouts/base.tmpl
Normal file
48
example-project/cmd/web-app/templates/layouts/base.tmpl
Normal 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}}
|
97
example-project/cmd/web-app/tests/tests_test.go
Normal file
97
example-project/cmd/web-app/tests/tests_test.go
Normal 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()
|
||||||
|
}
|
576
example-project/cmd/web-app/tests/user_test.go
Normal file
576
example-project/cmd/web-app/tests/user_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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=
|
||||||
|
8
example-project/internal/platform/deploy/cloud_front.go
Normal file
8
example-project/internal/platform/deploy/cloud_front.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package deploy
|
||||||
|
|
||||||
|
/*
|
||||||
|
func () {
|
||||||
|
|
||||||
|
func (c *CloudFront) ListDistributions(input *ListDistributionsInput)
|
||||||
|
|
||||||
|
} */
|
12
example-project/internal/platform/web/renderer.go
Normal file
12
example-project/internal/platform/web/renderer.go
Normal 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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
requires the following directories in the template directory
|
||||||
|
content
|
||||||
|
layouts
|
||||||
|
partials
|
@ -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()
|
||||||
|
}
|
Reference in New Issue
Block a user