Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial tests #7

Merged
merged 9 commits into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/github-action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: "CI"

on:
pull_request:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
go: [ '1.22.x', '1.23.x' ]

steps:
- name: "Checking out code..."
uses: actions/checkout@v4
- name: "Installing Go..."
uses: WillAbides/setup-go-faster@v1
with:
go-version: ${{ matrix.go }}
- name: "Install dependencies..."
run: go mod download
- name: "Verifying dependencies..."
run: go mod verify
- name: "Vetting code..."
run: go vet ./...
- uses: dominikh/staticcheck-action@v1
with:
version: "latest"
install-go: false
cache-key: ${{ matrix.go }}
- name: "Running tests..."
run: go test -race -vet=off ./...
62 changes: 62 additions & 0 deletions cmd/web/handlers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package main

import (
"net/http"
"testing"

"github.com/rynhndrcksn/go-starter-site/internal/assert"
)

func TestHomeHandler(t *testing.T) {
// Make a new test application.
app := newTestApplication(t)

// Make a new test server to make calls with.
ts := newTestServer(t, app.sessionManager.LoadAndSave(app.routes()))
defer ts.Close()

// Make a request to the handler being tested.
code, _, body := ts.get(t, "/")

// Assert we're getting a 200 code response.
assert.Equal(t, code, http.StatusOK)

// Assert that the body contains the text from the <title> tag.
assert.StringContains(t, body, "<title>Home - Site</title>")
}

func TestNotFoundHandler(t *testing.T) {
// Make a new test application.
app := newTestApplication(t)

// Make a new test server to make calls with.
ts := newTestServer(t, app.sessionManager.LoadAndSave(app.routes()))
defer ts.Close()

// Make a request to the handler being tested.
code, _, body := ts.get(t, "/not-found")

// Assert we're getting a 200 code response.
assert.Equal(t, code, http.StatusNotFound)

// Assert that the body contains the text from the <title> tag.
assert.StringContains(t, body, "<title>Not Found - Site</title>")
}

func TestServerErrorHandler(t *testing.T) {
// Make a new test application.
app := newTestApplication(t)

// Make a new test server to make calls with.
ts := newTestServer(t, app.sessionManager.LoadAndSave(app.routeThatPanics()))
defer ts.Close()

// Make a request to the handler being tested.
code, _, body := ts.get(t, "/server-error")

// Assert we're getting a 200 code response.
assert.Equal(t, code, http.StatusInternalServerError)

// Assert that the body contains the text from the <title> tag.
assert.StringContains(t, body, "<title>Server Error - Site</title>")
}
39 changes: 39 additions & 0 deletions cmd/web/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"net/http"
"testing"
)

func TestBackground(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/", nil)
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
r *http.Request
fn func()
}{
{
name: "Background works normally",
r: req,
fn: func() {
t.Log("This is a background test")
},
},
{
name: "Background recovers from panics",
r: req,
fn: func() {
panic("this is a panic")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := newTestApplication(t)
app.background(tt.r, tt.fn)
})
}
}
5 changes: 4 additions & 1 deletion cmd/web/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"log/slog"
"net/http"
"strings"
)

// commonHeaders sets all the default headers we want on every request.
Expand All @@ -30,7 +31,9 @@ func (app *application) logRequest(next http.Handler) http.Handler {
method = r.Method
uri = r.RequestURI
)
app.logger.Info("Received request", slog.String("ip", ip), slog.String("proto", proto), slog.String("method", method), slog.String("uri", uri))
if !strings.Contains(uri, "static") {
app.logger.Info("Received request", slog.String("ip", ip), slog.String("proto", proto), slog.String("method", method), slog.String("uri", uri))
}
next.ServeHTTP(w, r)
})
}
Expand Down
100 changes: 100 additions & 0 deletions cmd/web/middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package main

import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/rynhndrcksn/go-starter-site/internal/assert"
)

func TestCommonHeaders(t *testing.T) {
// Initialize a new httptest.ResponseRecorder and fake http.Request.
rr := httptest.NewRecorder()

r, err := http.NewRequest(http.MethodGet, "/", nil)
if err != nil {
t.Fatal(err)
}

// Create a mock HTTP handler that we can pass to our commonHeaders middleware, which writes a 200 status code and an "OK" response body.
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("OK"))
})

// Initialize a new test application.
app := newTestApplication(t)

// Pass the mock HTTP handler to our commonHeaders middleware.
app.commonHeaders(next).ServeHTTP(rr, r)

// Call the Result() method on the http.ResponseRecorder to get the results of the test.
rs := rr.Result()

// Check that the middleware has correctly set the Content-Security-Policy header on the response.
expectedValue := "default-src 'self';frame-ancestors 'none';"
assert.Equal(t, rs.Header.Get("Content-Security-Policy"), expectedValue)

// Check that the middleware has correctly set the Referrer-Policy header on the response.
expectedValue = "origin-when-cross-origin"
assert.Equal(t, rs.Header.Get("Referrer-Policy"), expectedValue)

expectedValue = "max-age=63072000; includeSubDomains; preload"
assert.Equal(t, rs.Header.Get("Strict-Transport-Security"), expectedValue)

// Check that the middleware has correctly set the X-Content-Type-Options header on the response.
expectedValue = "nosniff"
assert.Equal(t, rs.Header.Get("X-Content-Type-Options"), expectedValue)

// Check that the middleware has correctly set the X-Frame-Options header on the response.
expectedValue = "deny"
assert.Equal(t, rs.Header.Get("X-Frame-Options"), expectedValue)

// Check that the middleware has correctly called the next handler in line and the response status code and body are as expected.
assert.Equal(t, rs.StatusCode, http.StatusOK)

defer func(Body io.ReadCloser) {
err = Body.Close()
if err != nil {
t.Fatal(err)
}
}(rs.Body)
body, err := io.ReadAll(rs.Body)
if err != nil {
t.Fatal(err)
}
body = bytes.TrimSpace(body)

assert.Equal(t, string(body), "OK")
}

func TestRecoverPanic(t *testing.T) {
// Initialize a new httptest.ResponseRecorder and fake http.Request.
rr := httptest.NewRecorder()

r, err := http.NewRequest(http.MethodGet, "/", nil)
if err != nil {
t.Fatal(err)
}

// Create a mock HTTP handler that we can pass to our recoverPanic middleware, which panics.
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
panic("this is a panic")
})

// Initialize a new test application.
app := newTestApplication(t)

// Pass the mock HTTP handler to the recoverPanic middleware.
// Note: we need to wrap the recoverPanic middleware inside a call
// to "app.sessionManager.LoadAndSave()", otherwise scs panics.
app.sessionManager.LoadAndSave(app.recoverPanic(next)).ServeHTTP(rr, r)

// Call the Result() method on the http.ResponseRecorder to get the results of the test.
rs := rr.Result()

// Check that the middleware worked and the response is what's wanted.
assert.Equal(t, rs.Header.Get("Connection"), "close")
}
12 changes: 12 additions & 0 deletions cmd/web/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,15 @@ func (app *application) routes() http.Handler {

return app.recoverPanic(app.logRequest(app.commonHeaders(router)))
}

func (app *application) routeThatPanics() http.Handler {
// Initialize new httprouter instance.
router := httprouter.New()

// Register route to test against:
router.HandlerFunc(http.MethodGet, "/server-error", func(writer http.ResponseWriter, r *http.Request) {
panic("this triggers a 500 status page")
})

return app.recoverPanic(router)
}
11 changes: 10 additions & 1 deletion cmd/web/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import (
"github.com/rynhndrcksn/go-starter-site/ui"
)

var (
errPropsKeyValueCountMismatch = errors.New("mismatched amount of key/value pairs")
errPropsKeyValueCountIsZero = errors.New("length of 'pairs' must be greater than 0")
)

// functions contains a template.FuncMap that maps the above functions to functions that can then be called inside the templates.
var functions = template.FuncMap{
"humanDate": humanDate,
Expand All @@ -28,8 +33,12 @@ func humanDate(t time.Time) string {

// props takes any number of key/value pairs and passes them into a child template.
func props(pairs ...any) (map[string]any, error) {
if len(pairs) == 0 {
return nil, errPropsKeyValueCountIsZero
}

if len(pairs)%2 != 0 {
return nil, errors.New("mismatched amount of key/value pairs")
return nil, errPropsKeyValueCountMismatch
}

m := make(map[string]any, len(pairs)/2)
Expand Down
78 changes: 78 additions & 0 deletions cmd/web/templates_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package main

import (
"maps"
"testing"
"time"

"github.com/rynhndrcksn/go-starter-site/internal/assert"
)

func TestHumanDate(t *testing.T) {
tests := []struct {
name string
tm time.Time
want string
}{
{
name: "UTC",
tm: time.Date(2024, 01, 1, 12, 0, 0, 0, time.UTC),
want: "01 Jan 2024 at 12:00",
},
{
name: "Empty",
tm: time.Time{},
want: "",
},
{
name: "PST",
tm: time.Date(2024, 01, 1, 12, 0, 0, 0, time.FixedZone("UTC-8", 8*60*60)),
want: "01 Jan 2024 at 04:00",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hd := humanDate(tt.tm)
assert.Equal(t, hd, tt.want)
})
}
}

func TestProps(t *testing.T) {
validMap := make(map[string]any, 2)
validMap["key1"] = "value1"
validMap["key2"] = 5
validMap["key3"] = true
tests := []struct {
name string
input []any
wantMap map[string]any
wantErr error
}{
{
name: "Passed in 0 items",
input: []any{},
wantMap: nil,
wantErr: errPropsKeyValueCountIsZero,
},
{
name: "Mismatched key value pairs",
input: []any{"key1", "value1", "key2"},
wantMap: nil,
wantErr: errPropsKeyValueCountMismatch,
},
{
name: "Valid amount of key value pairs",
input: []any{"key1", "value1", "key2", 5, "key3", true},
wantMap: validMap,
wantErr: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotMap, gotErr := props(test.input...)
assert.Equal(t, gotErr, test.wantErr)
assert.Equal(t, maps.Equal(gotMap, test.wantMap), true)
})
}
}
Loading