Skip to content

Commit

Permalink
test: add initial tests for project
Browse files Browse the repository at this point in the history
* Test: Update makefile to include testing commands

* Test: Add initial testing files

* test: add test for props() function

* test: add test for recoverPanic middleware

* test: add handlers_test.go

* test: add test for background function

* chore: address staticcheck errors

* ci: add github-action.yml
  • Loading branch information
rynhndrcksn authored Sep 1, 2024
1 parent 3756ab6 commit f2a2bd5
Show file tree
Hide file tree
Showing 13 changed files with 485 additions and 5 deletions.
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

0 comments on commit f2a2bd5

Please sign in to comment.