Compare commits

...

10 Commits

Author SHA1 Message Date
tamsin johnson
cd40158cc4 lets-go:14.5 get some mocks in place 2024-02-22 16:52:40 -08:00
tamsin johnson
01bfefac13 lets-go:14.3 refactor test server setup 2024-02-14 14:46:27 -08:00
tamsin johnson
eeca8ca34c lets-go:14.3 end-to-end server testing 2024-02-14 13:27:02 -08:00
tamsin johnson
be85d936a9 lets-go:14.2 middleware tests 2024-02-14 10:27:32 -08:00
tamsin johnson
6744d12001 lets-go:14.2 ping handler/test 2024-02-13 12:54:07 -08:00
tamsin johnson
6da4184eb0 lets-go:14.1 assertion helper 2024-02-13 12:23:57 -08:00
tamsin johnson
52e1bbaa70 lets-go:14.1 passing tests 2024-02-13 11:59:59 -08:00
tamsin johnson
15d817c5d1 lets-go:14.1 failing unit tests 2024-02-13 11:59:59 -08:00
tamsin johnson
f5c642ba4e lets-go:13.2 embed templates 2024-02-08 12:54:00 -08:00
tamsin johnson
ba4f38b425 lets-go:13.1 embed static 2024-02-08 10:11:53 -08:00
11 changed files with 264 additions and 13 deletions

View File

@ -11,6 +11,11 @@ import (
"snippetbox.chaosfem.tw/internal/validator"
)
// ping ...
func ping(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}
// home ...
func (app *application) home(w http.ResponseWriter, r *http.Request) {
snippets, err := app.snippets.Latest()

View File

@ -0,0 +1,19 @@
package main
import (
"net/http"
"snippetbox.chaosfem.tw/internal/assert"
"testing"
)
func TestPing(t *testing.T) {
app := newTestApplication(t)
ts := newTestServer(t, app.routes())
defer ts.Close()
statusCode, _, body := ts.get(t, "/ping")
assert.Equal(t, statusCode, http.StatusOK)
assert.Equal(t, body, "OK")
}

View File

@ -0,0 +1,43 @@
package main
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"testing"
"snippetbox.chaosfem.tw/internal/assert"
)
func TestSecureHeaders(t *testing.T) {
rr := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodGet, "/", nil)
if err != nil {
t.Fatal(err)
}
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
secureHeaders(next).ServeHTTP(rr, r)
rs := rr.Result()
assert.Equal(t, rs.Header.Get("Content-Security-Policy"), "default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com")
assert.Equal(t, rs.Header.Get("Referrer-Policy"), "origin-when-cross-origin")
assert.Equal(t, rs.Header.Get("X-Content-Type-Options"), "nosniff")
assert.Equal(t, rs.Header.Get("X-Frame-Options"), "deny")
assert.Equal(t, rs.Header.Get("X-XSS-Protection"), "0")
defer rs.Body.Close()
body, err := io.ReadAll(rs.Body)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, string(bytes.TrimSpace(body)), "OK")
}

View File

@ -5,6 +5,8 @@ import (
"github.com/julienschmidt/httprouter"
"github.com/justinas/alice"
"snippetbox.chaosfem.tw/ui"
)
// routes ...
@ -20,8 +22,10 @@ func (app *application) routes() http.Handler {
})
// setup server for static files
fileServer := http.FileServer(http.Dir("./ui/static"))
router.Handler(http.MethodGet, "/static/*filepath", http.StripPrefix("/static", fileServer))
fileServer := http.FileServer(http.FS(ui.Files))
router.Handler(http.MethodGet, "/static/*filepath", fileServer)
router.HandlerFunc(http.MethodGet, "/ping", ping)
dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate)

View File

@ -2,10 +2,12 @@ package main
import (
"html/template"
"io/fs"
"path/filepath"
"time"
"snippetbox.chaosfem.tw/internal/models"
"snippetbox.chaosfem.tw/ui"
)
type templateData struct {
@ -20,7 +22,11 @@ type templateData struct {
// humanDate ...
func humanDate(t time.Time) string {
return t.Format("02 Jan 2006 at 15:04")
if t.IsZero() {
return ""
}
return t.UTC().Format("02 Jan 2006 at 15:04")
}
var functions = template.FuncMap{
@ -31,7 +37,7 @@ var functions = template.FuncMap{
func newTemplateCache() (map[string]*template.Template, error) {
cache := map[string]*template.Template{}
pages, err := filepath.Glob("./ui/html/pages/*.tmpl")
pages, err := fs.Glob(ui.Files, "html/pages/*.tmpl")
if err != nil {
return nil, err
}
@ -39,17 +45,13 @@ func newTemplateCache() (map[string]*template.Template, error) {
for _, page := range pages {
name := filepath.Base(page)
ts, err := template.New(name).Funcs(functions).ParseFiles("./ui/html/base.tmpl")
if err != nil {
return nil, err
patterns := []string{
"html/base.tmpl",
"html/partials/*.tmpl",
page,
}
ts, err = ts.ParseGlob("./ui/html/partials/*.tmpl")
if err != nil {
return nil, err
}
ts, err = ts.ParseFiles(page)
ts, err := template.New(name).Funcs(functions).ParseFS(ui.Files, patterns...)
if err != nil {
return nil, err
}

View File

@ -0,0 +1,39 @@
package main
import (
"testing"
"time"
"snippetbox.chaosfem.tw/internal/assert"
)
func TestHumanDate(t *testing.T) {
tests := []struct {
name string
tm time.Time
want string
}{
{
name: "UTC",
tm: time.Date(2023, 3, 17, 10, 15, 0, 0, time.UTC),
want: "17 Mar 2023 at 10:15",
},
{
name: "Empty",
tm: time.Time{},
want: "",
},
{
name: "CET",
tm: time.Date(2023, 3, 17, 10, 15, 0, 0, time.FixedZone("CET", 1*60*60)),
want: "17 Mar 2023 at 09:15",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, humanDate(tt.tm), tt.want)
})
}
}

View File

@ -0,0 +1,53 @@
package main
import (
"bytes"
"io"
"log/slog"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"testing"
)
func newTestApplication(t *testing.T) *application {
return &application{
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
}
}
type testServer struct {
*httptest.Server
}
func newTestServer(t *testing.T, h http.Handler) *testServer {
ts := httptest.NewTLSServer(h)
jar, err := cookiejar.New(nil)
if err != nil {
t.Fatal(err)
}
ts.Client().Jar = jar
ts.Client().CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
return &testServer{ts}
}
func (ts *testServer) get(t *testing.T, urlPath string) (int, http.Header, string) {
rs, err := ts.Client().Get(ts.URL + urlPath)
if err != nil {
t.Fatal(err)
}
defer rs.Body.Close()
body, err := io.ReadAll(rs.Body)
if err != nil {
t.Fatal(err)
}
return rs.StatusCode, rs.Header, string(bytes.TrimSpace(body))
}

View File

@ -0,0 +1,13 @@
package assert
import (
"testing"
)
func Equal[T comparable](t *testing.T, actual, expected T) {
t.Helper()
if actual != expected {
t.Errorf("got %v; want %v", actual, expected)
}
}

View File

@ -0,0 +1,34 @@
package mocks
import (
"time"
"snippetbox.alexedwards.net/internal/models"
)
var mockSnippet = models.Snippet{
ID: 1,
Title: "An old silent pond",
Content: "An old silent pond...",
Created: time.Now(),
Expires: time.Now(),
}
type SnippetModel struct{}
func (m *SnippetModel) Insert(title string, content string, expires int) (int, error) {
return 2, nil
}
func (m *SnippetModel) Get(id int) (models.Snippet, error) {
switch id {
case 1:
return mockSnippet, nil
default:
return models.Snippet{}, models.ErrNoRecord
}
}
func (m *SnippetModel) Latest() ([]models.Snippet, error) {
return []models.Snippet{mockSnippet}, nil
}

View File

@ -0,0 +1,31 @@
package mocks
import (
"snippetbox.alexedwards.net/internal/models"
)
type UserModel struct{}
func (m *UserModel) Insert(name, email, password string) error {
switch email {
case "dupe@example.com":
return models.ErrDuplicateEmail
default:
return nil
}
}
func (m *UserModel) Authenticate(email, password string) (int, error) {
if email == "alice@example.com" && password == "pa$$word" {
return 1, nil
}
}
func (m *UserModel) Exists(id int) (bool, error) {
switch id {
case 1:
return true, nil
default:
return false, nil
}
}

8
snippetbox/ui/efs.go Normal file
View File

@ -0,0 +1,8 @@
package ui
import (
"embed"
)
//go:embed "html" "static"
var Files embed.FS