diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 25edf77..4b67a4f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,9 @@ jobs: with: go-version: ${{ matrix.go }} - - run: "go vet ./..." + - run: "make vet" + + - run: "make test" - uses: dominikh/staticcheck-action@v1.3.1 with: diff --git a/Makefile b/Makefile index 541bfb9..7cfbd5e 100644 --- a/Makefile +++ b/Makefile @@ -28,4 +28,8 @@ vet: .PHONY: staticcheck staticcheck: - staticcheck ./... \ No newline at end of file + staticcheck ./... + +.PHONY: test +test: + go test -race -v -timeout 30s ./... \ No newline at end of file diff --git a/go.mod b/go.mod index 8d54171..8e18c90 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect @@ -18,7 +19,10 @@ require ( github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/jwx/v2 v2.0.19 // indirect github.com/lestrrat-go/option v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/segmentio/asm v1.2.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect golang.org/x/crypto v0.18.0 // indirect golang.org/x/sys v0.16.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b7b95fb..982a408 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= diff --git a/internal/handlers/getabout_test.go b/internal/handlers/getabout_test.go new file mode 100644 index 0000000..9c5fb9e --- /dev/null +++ b/internal/handlers/getabout_test.go @@ -0,0 +1,59 @@ +package handlers + +import ( + "bytes" + "context" + "goth/internal/middleware" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetAboutHandler(t *testing.T) { + + testCases := []struct { + name string + expectedStatusCode int + expectedBody []byte + }{ + { + name: "render successfully", + expectedStatusCode: http.StatusOK, + expectedBody: []byte("My website"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + assert := assert.New(t) + + handler := NewAboutHandler() + + req, err := http.NewRequest("GET", "/about", nil) + assert.NoError(err) + + value := middleware.Nonces{ + Htmx: "nonce-1234", + ResponseTargets: "nonce-5678", + Tw: "nonce-9101", + HtmxCSSHash: "sha256-pgn1TCGZX6O77zDvy0oTODMOxemn0oj0LeCnQTRj7Kg=", + } + ctx := context.WithValue(req.Context(), middleware.NonceKey, value) + req = req.WithContext(ctx) + + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + + assert.Equal(tc.expectedStatusCode, rr.Code, "handler returned wrong status code: got %v want %v", rr.Code, tc.expectedStatusCode) + + assert.True(bytes.Contains(rr.Body.Bytes(), tc.expectedBody), "handler returned unexpected body: got %v want %v", rr.Body.String(), tc.expectedBody) + + }) + + } + +} diff --git a/internal/handlers/web.go b/internal/handlers/web.go new file mode 100644 index 0000000..eabbbca --- /dev/null +++ b/internal/handlers/web.go @@ -0,0 +1,22 @@ +package handlers + +import ( + "goth/internal/templates" + "net/http" +) + +type WebHandler struct{} + +func NewWebHandler() *WebHandler { + return &WebHandler{} +} + +func (h *WebHandler) ServeHTTPAbout(w http.ResponseWriter, r *http.Request) { + c := templates.About() + err := templates.Layout(c, "My website").Render(r.Context(), w) + + if err != nil { + http.Error(w, "Error rendering template", http.StatusInternalServerError) + return + } +} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 37a6ccb..eee2f9d 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -14,12 +14,10 @@ type key string var NonceKey key = "nonces" type Nonces struct { - Htmx string - ResponseTargets string - Tw string - WordPressScripts string - WordPressStyles string - HtmxCSSHash string + Htmx string + ResponseTargets string + Tw string + HtmxCSSHash string } func generateRandomString(length int) string { @@ -38,18 +36,22 @@ func CSPMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Create a new Nonces struct for every request when here. - // move to outisde the handler to use the same nonces in all responses - var nonceSet Nonces - nonceSet.Htmx = generateRandomString(16) - nonceSet.ResponseTargets = generateRandomString(16) - nonceSet.Tw = generateRandomString(16) - - htmxCSSHash := "sha256-pgn1TCGZX6O77zDvy0oTODMOxemn0oj0LeCnQTRj7Kg=" + // move to outside the handler to use the same nonces in all responses + nonceSet := Nonces{ + Htmx: generateRandomString(16), + ResponseTargets: generateRandomString(16), + Tw: generateRandomString(16), + HtmxCSSHash: "sha256-pgn1TCGZX6O77zDvy0oTODMOxemn0oj0LeCnQTRj7Kg=", + } // set nonces in context ctx := context.WithValue(r.Context(), NonceKey, nonceSet) // insert the nonces into the content security policy header - cspHeader := fmt.Sprintf("default-src 'self'; script-src 'nonce-%s' 'nonce-%s' ; style-src 'nonce-%s' '%s';", nonceSet.Htmx, nonceSet.ResponseTargets, nonceSet.Tw, htmxCSSHash) + cspHeader := fmt.Sprintf("default-src 'self'; script-src 'nonce-%s' 'nonce-%s' ; style-src 'nonce-%s' '%s';", + nonceSet.Htmx, + nonceSet.ResponseTargets, + nonceSet.Tw, + nonceSet.HtmxCSSHash) w.Header().Set("Content-Security-Policy", cspHeader) next.ServeHTTP(w, r.WithContext(ctx)) @@ -63,27 +65,35 @@ func TextHTMLMiddleware(next http.Handler) http.Handler { }) } -// get the Nonce from the context, it is a struct called Nonces, so we can get the nonce we need by the key, i.e. HtmxNonce -func GetNonces(ctx context.Context) any { - +// get the Nonce from the context, it is a struct called Nonces, +// so we can get the nonce we need by the key, i.e. HtmxNonce +func GetNonces(ctx context.Context) Nonces { nonceSet := ctx.Value(NonceKey) if nonceSet == nil { - log.Fatal("nooo no nonces") + log.Fatal("error getting nonce set - is nil") } - return nonceSet + + nonces, ok := nonceSet.(Nonces) + + if !ok { + log.Fatal("error getting nonce set - not ok") + } + + return nonces } func GetHtmxNonce(ctx context.Context) string { - nonceSet := GetNonces(ctx).(Nonces) + nonceSet := GetNonces(ctx) + return nonceSet.Htmx } func GetResponseTargetsNonce(ctx context.Context) string { - nonceSet := GetNonces(ctx).(Nonces) + nonceSet := GetNonces(ctx) return nonceSet.ResponseTargets } func GetTwNonce(ctx context.Context) string { - nonceSet := GetNonces(ctx).(Nonces) + nonceSet := GetNonces(ctx) return nonceSet.Tw }