Repository: google/go-safeweb
Pull request #349 (2022-10-17 20:02:23)

Examples fix and tests introduction
This PR fixes the examples attached to this project. In particular, inside of them, composite literals were used to create new `ServeMuxConfig` instances instead of passing `nil` to the `NewServeMuxConfig` function. This was causing the applications to panic during the `Mux()` method execution.

In addition, some tests have been introduced in order to be notified in case of future breaks.

- [x] Tests pass
- [ ] Appropriate changes to README are included in PR


Patch:
diff --git a/examples/echo/echo.go b/examples/echo/echo.go
index 10b3a89..325ba71 100644
--- a/examples/echo/echo.go
+++ b/examples/echo/echo.go
@@ -1,4 +1,4 @@
-// Copyright 2020 Google LLC
+// Copyright 2022 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -69,7 +69,7 @@ func uptime(w safehttp.ResponseWriter, req *safehttp.IncomingRequest) safehttp.R
 		Uptime    time.Duration
 		EasterEgg string
 	}
-	x.Uptime = time.Now().Sub(start)
+	x.Uptime = time.Since(start)
 
 	// Easter egg handling.
 	q, err := req.URL().Query()
diff --git a/examples/echo/security/web/web.go b/examples/echo/security/web/web.go
index 030b60c..2db32da 100644
--- a/examples/echo/security/web/web.go
+++ b/examples/echo/security/web/web.go
@@ -1,4 +1,4 @@
-// Copyright 2020 Google LLC
+// Copyright 2022 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -48,7 +48,7 @@ import (
 // Warning: XSRF protection is currently missing due to
 // https://github.com/google/go-safeweb/issues/171.
 func NewMuxConfig(addr string) *safehttp.ServeMuxConfig {
-	c := &safehttp.ServeMuxConfig{}
+	c := safehttp.NewServeMuxConfig(nil)
 
 	c.Intercept(coop.Default(""))
 	c.Intercept(staticheaders.Interceptor{})
@@ -81,7 +81,7 @@ func NewMuxConfig(addr string) *safehttp.ServeMuxConfig {
 // Important: the host checking plugin will accept only requests coming to
 // localhost:port, not e.g. 127.0.0.1:port.
 func NewMuxConfigDev(port int) *safehttp.ServeMuxConfig {
-	c := &safehttp.ServeMuxConfig{}
+	c := safehttp.NewServeMuxConfig(nil)
 
 	c.Intercept(coop.Default(""))
 	for _, i := range csp.Default("") {
diff --git a/examples/sample-application/secure/mux.go b/examples/sample-application/secure/mux.go
index ac7ab32..e2b5919 100644
--- a/examples/sample-application/secure/mux.go
+++ b/examples/sample-application/secure/mux.go
@@ -34,8 +34,7 @@ import (
 
 // NewMuxConfig creates a safe ServeMuxConfig.
 func NewMuxConfig(db *storage.DB, addr string) *safehttp.ServeMuxConfig {
-	c := safehttp.NewServeMuxConfig(dispatcher{})
-
+	c := safehttp.NewServeMuxConfig(nil)
 	c.Intercept(coop.Default(""))
 	c.Intercept(staticheaders.Interceptor{})
 	for _, i := range csp.Default("") {
diff --git a/examples/trustedtypes/server.go b/examples/trustedtypes/server.go
index dc49745..4bb73de 100644
--- a/examples/trustedtypes/server.go
+++ b/examples/trustedtypes/server.go
@@ -1,4 +1,4 @@
-// Copyright 2020 Google LLC
+// Copyright 2022 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -32,21 +32,13 @@ import (
 )
 
 func main() {
-	host := "localhost"
-	port := "8080"
-	addr := net.JoinHostPort(host, port)
-	var mc safehttp.ServeMuxConfig
-	for _, i := range csp.Default("") {
-		mc.Intercept(i)
-	}
-	mux := mc.Mux()
+	mb, addr := newServeMuxConfig()
+	mux := mb.Mux()
 
-	safeTmplSrc, _ := template.TrustedSourceFromConstantDir("", template.TrustedSource{}, "safe.html")
-	safeTmpl := template.Must(htmlinject.LoadFiles(nil, htmlinject.LoadConfig{}, safeTmplSrc))
+	safeTmpl, _ := loadTemplate("safe.html")
 	mux.Handle("/safe", safehttp.MethodGet, safehttp.HandlerFunc(handleTemplate(safeTmpl)))
 
-	unsafeTmplSrc, _ := template.TrustedSourceFromConstantDir("", template.TrustedSource{}, "unsafe.html")
-	unsafeTmpl := template.Must(htmlinject.LoadFiles(nil, htmlinject.LoadConfig{}, unsafeTmplSrc))
+	unsafeTmpl, _ := loadTemplate("unsafe.html")
 	mux.Handle("/unsafe", safehttp.MethodGet, safehttp.HandlerFunc(handleTemplate(unsafeTmpl)))
 
 	log.Printf("Visit http://%s\n", addr)
@@ -54,6 +46,27 @@ func main() {
 	log.Fatal(http.ListenAndServe(addr, mux))
 }
 
+func newServeMuxConfig() (*safehttp.ServeMuxConfig, string) {
+	host := "localhost"
+	port := "8080"
+	addr := net.JoinHostPort(host, port)
+	mc := safehttp.NewServeMuxConfig(nil)
+	for _, i := range csp.Default("") {
+		mc.Intercept(i)
+	}
+	return mc, addr
+}
+
+func loadTemplate(src string) (*template.Template, error) {
+	tmplSrc, err := template.TrustedSourceFromConstantDir("", template.TrustedSource{}, src)
+	if err != nil {
+		return nil, err
+	}
+
+	tmpl := template.Must(htmlinject.LoadFiles(nil, htmlinject.LoadConfig{}, tmplSrc))
+	return tmpl, nil
+}
+
 func handleTemplate(tmpl safehttp.Template) func(w safehttp.ResponseWriter, req *safehttp.IncomingRequest) safehttp.Result {
 	return func(w safehttp.ResponseWriter, req *safehttp.IncomingRequest) safehttp.Result {
 		return safehttp.ExecuteTemplate(w, tmpl, nil)
diff --git a/safehttp/plugins/xsrf/xsrfblockall/xsrf.go b/safehttp/plugins/xsrf/xsrfblockall/xsrf.go
new file mode 100644
index 0000000..65f5bb4
--- /dev/null
+++ b/safehttp/plugins/xsrf/xsrfblockall/xsrf.go
@@ -0,0 +1,49 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package xsrfblockall
+
+import (
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/xsrf"
+	"log"
+)
+
+// Interceptor provides protection against Cross-Site Request Forgery attacks
+// by rejecting all state-changing request.
+type Interceptor struct{}
+
+var _ safehttp.Interceptor = &Interceptor{}
+
+// Before rejects every state-changing request (all except GET, HEAD, and OPTIONS).
+func (it *Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, _ safehttp.InterceptorConfig) safehttp.Result {
+	if xsrf.StatePreserving(r) {
+		return safehttp.NotWritten()
+	}
+
+	if safehttp.IsLocalDev() {
+		log.Println("xsrfblockall plugin blocked a state changing request")
+	}
+
+	return w.WriteError(safehttp.StatusForbidden)
+}
+
+// Commit does nothing.
+func (it *Interceptor) Commit(w safehttp.ResponseHeadersWriter, r *safehttp.IncomingRequest, resp safehttp.Response, _ safehttp.InterceptorConfig) {
+}
+
+// Match returns false since there are no supported configurations.
+func (*Interceptor) Match(safehttp.InterceptorConfig) bool {
+	return false
+}


Test patch:
diff --git a/examples/echo/echo_test.go b/examples/echo/echo_test.go
new file mode 100644
index 0000000..e0b9059
--- /dev/null
+++ b/examples/echo/echo_test.go
@@ -0,0 +1,113 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"fmt"
+	"net/http/httptest"
+	"regexp"
+	"testing"
+	"time"
+
+	"github.com/google/go-safeweb/safehttp"
+)
+
+func TestEcho(t *testing.T) {
+	tests := []struct {
+		name    string
+		req     string
+		want    string
+		wantErr bool
+	}{
+		{
+			name:    "no error",
+			req:     "?message=<h1>h4x0r</h1>",
+			want:    "&lt;h1&gt;h4x0r&lt;/h1&gt;",
+			wantErr: false,
+		},
+		{
+			name:    "empty message",
+			req:     "?message=",
+			want:    "",
+			wantErr: true,
+		}, {
+			name:    "invalid query parameters",
+			req:     "?message=;something;",
+			want:    "",
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		mb := safehttp.NewServeMuxConfig(nil)
+		mux := mb.Mux()
+		mux.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(echo))
+
+		req := httptest.NewRequest(safehttp.MethodGet, fmt.Sprintf("http://foo.com/%s", tt.req), nil)
+		rw := httptest.NewRecorder()
+		mux.ServeHTTP(rw, req)
+
+		if rw.Code != int(safehttp.StatusOK) && !tt.wantErr {
+			t.Errorf("echo() status = %v, wantErr = %v", rw.Code, tt.wantErr)
+		}
+
+		if body := rw.Body.String(); !tt.wantErr && body != tt.want {
+			t.Errorf("body got: %q want: %q", body, tt.want)
+		}
+	}
+}
+
+func TestUptime(t *testing.T) {
+	tests := []struct {
+		name    string
+		req     string
+		wantErr bool
+	}{
+		{
+			name:    "no error",
+			req:     "",
+			wantErr: false,
+		},
+		{
+			name:    "invalid query parameters",
+			req:     "?message=;something;",
+			wantErr: true,
+		},
+	}
+
+	rgx := regexp.MustCompile("^<h1>Uptime: (.*)</h1>$")
+
+	for _, tt := range tests {
+		mb := safehttp.NewServeMuxConfig(nil)
+		mux := mb.Mux()
+		start = time.Date(1991, time.September, 17, 00, 00, 00, 00, time.UTC)
+		mux.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(uptime))
+
+		req := httptest.NewRequest(safehttp.MethodGet, fmt.Sprintf("http://foo.com/%s", tt.req), nil)
+		rw := httptest.NewRecorder()
+		mux.ServeHTTP(rw, req)
+
+		if rw.Code != int(safehttp.StatusOK) && !tt.wantErr {
+			t.Errorf("uptime() status = %v, wantErr = %v", rw.Code, tt.wantErr)
+		}
+
+		if !tt.wantErr {
+			matched := rgx.Match(rw.Body.Bytes())
+			if !matched {
+				t.Errorf("body got: %q want: %q", rw.Body.String(), "<h1>Uptime: X</h1>")
+			}
+		}
+	}
+}
diff --git a/examples/echo/security/web/web_test.go b/examples/echo/security/web/web_test.go
new file mode 100644
index 0000000..f4b9e8f
--- /dev/null
+++ b/examples/echo/security/web/web_test.go
@@ -0,0 +1,68 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package web
+
+import (
+	"fmt"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/safehtml"
+)
+
+func TestNewMuxConfig(t *testing.T) {
+	tests := []struct {
+		name    string
+		config  *safehttp.ServeMuxConfig
+		addr    string
+		hasHSTS bool
+	}{
+		{
+			name:    "development mux config",
+			config:  NewMuxConfigDev(8080),
+			addr:    "https://localhost:8080",
+			hasHSTS: false,
+		},
+		{
+			name:    "production mux config",
+			config:  NewMuxConfig("localhost:8080"),
+			addr:    "https://localhost:8080",
+			hasHSTS: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mux := tt.config.Mux()
+			h := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+				return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
+			})
+			mux.Handle("/spaghetti", safehttp.MethodGet, h)
+
+			req := httptest.NewRequest(safehttp.MethodGet, fmt.Sprintf("%s/spaghetti", tt.addr), nil)
+			rw := httptest.NewRecorder()
+			mux.ServeHTTP(rw, req)
+
+			hasHSTSHeader := rw.Header().Get("Strict-Transport-Security") != ""
+			if tt.hasHSTS && !hasHSTSHeader {
+				t.Errorf("expected \"Strict-Transport-Security\" header since HTST is enabled")
+			}
+			if !tt.hasHSTS && hasHSTSHeader {
+				t.Errorf("unexpected \"Strict-Transport-Security\" header since HTST is disabled")
+			}
+		})
+	}
+}
diff --git a/examples/sample-application/secure/auth/auth_test.go b/examples/sample-application/secure/auth/auth_test.go
new file mode 100644
index 0000000..46fab0f
--- /dev/null
+++ b/examples/sample-application/secure/auth/auth_test.go
@@ -0,0 +1,219 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package auth
+
+import (
+	"testing"
+
+	"github.com/google/go-safeweb/examples/sample-application/storage"
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
+)
+
+const TEST_USER = "test"
+
+func TestInterceptorBefore(t *testing.T) {
+	tests := []struct {
+		name    string
+		cfg     safehttp.InterceptorConfig
+		hasAuth bool
+		want    safehttp.StatusCode
+	}{
+		{
+			name:    "base case, no error",
+			hasAuth: true,
+			cfg:     nil,
+			want:    safehttp.StatusOK,
+		},
+		{
+			name:    "force skip using config",
+			hasAuth: true,
+			cfg:     Skip{},
+			want:    safehttp.StatusOK,
+		},
+		{
+			name:    "missing auth, error",
+			hasAuth: false,
+			cfg:     nil,
+			want:    safehttp.StatusUnauthorized,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			withUserDB, token := addTestUser(storage.NewDB())
+			ip := newTestInterceptor(withUserDB)
+
+			req := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
+			rw, r := safehttptest.NewFakeResponseWriter()
+
+			if tt.hasAuth {
+				addTestUserCookie(req, token)
+			}
+
+			// Note: "Before" return value is not significant
+			ip.Before(rw, req, tt.cfg)
+
+			if got := r.Code; got != int(tt.want) {
+				t.Errorf("status code got: %d, want %d", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestInterceptorCommit(t *testing.T) {
+	tests := []struct {
+		name      string
+		action    sessionAction
+		hasCookie bool
+	}{
+		{
+			name:      "clear session, no error",
+			action:    clearSess,
+			hasCookie: false,
+		}, {
+			name:      "set session, no error",
+			action:    setSess,
+			hasCookie: true,
+		},
+		{
+			name:      "unexpected action, skip",
+			action:    sessionAction("unexpected"),
+			hasCookie: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			withUserDB, _ := addTestUser(storage.NewDB())
+			ip := newTestInterceptor(withUserDB)
+
+			req := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
+			rw, r := safehttptest.NewFakeResponseWriter()
+
+			safehttp.FlightValues(req.Context()).Put(userKey, "user")
+			safehttp.FlightValues(req.Context()).Put(changeSessKey, tt.action)
+
+			ip.Commit(rw, req, r.Result, nil)
+
+			var token string
+			for _, c := range rw.Cookies {
+				if c.Name() == sessionCookie {
+					token = c.Value()
+				}
+			}
+
+			if tt.hasCookie == (token == "") {
+				t.Errorf("token = %q, want %v", token, tt.hasCookie)
+			}
+		})
+	}
+}
+
+func TestInterceptorMatch(t *testing.T) {
+	tests := []struct {
+		name string
+		cfg  safehttp.InterceptorConfig
+		want bool
+	}{
+		{
+			name: "basic case, no error",
+			cfg:  Skip{},
+			want: true,
+		},
+		{
+			name: "no Skip{}, error",
+			cfg:  nil,
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ip := newTestInterceptor(nil)
+			if got := ip.Match(tt.cfg); got != tt.want {
+				t.Errorf("Interceptor.Match() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestInterceptorUserFromCookie(t *testing.T) {
+	withUserDB, validToken := addTestUser(storage.NewDB())
+	ip := newTestInterceptor(withUserDB)
+
+	tests := []struct {
+		name  string
+		token string
+		want  string
+	}{
+		{
+			name:  "basic case, no error",
+			token: validToken,
+			want:  TEST_USER,
+		},
+		{
+			name:  "empty cookie, error",
+			token: "",
+			want:  "",
+		},
+		{
+			name:  "invalid token, error",
+			token: "not_a_valid_token",
+			want:  "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
+			addTestUserCookie(req, tt.token)
+
+			if got := ip.userFromCookie(req); got != tt.want {
+				t.Errorf("Interceptor.userFromCookie() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestSessionManagement(t *testing.T) {
+	want := "wanted"
+	r := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
+
+	CreateSession(r, want)
+	if got := User(r); got != want {
+		t.Errorf("user id got: %q, want %q", got, want)
+	}
+
+	ClearSession(r)
+	// Note: `ctxSessionAction` already tested inside ctx_test.go
+	if got := ctxSessionAction(r.Context()); got != clearSess {
+		t.Errorf("no clearSess action found in context after ClearSession")
+	}
+}
+
+func addTestUserCookie(r *safehttp.IncomingRequest, v string) {
+	r.Header.Add("Cookie", safehttp.NewCookie(sessionCookie, v).String())
+}
+
+func newTestInterceptor(db *storage.DB) Interceptor {
+	if db == nil {
+		db = storage.NewDB()
+	}
+	return Interceptor{
+		DB: db,
+	}
+}
+
+func addTestUser(db *storage.DB) (*storage.DB, string) {
+	token := (*db).GetToken(TEST_USER)
+	return db, token
+}
diff --git a/examples/sample-application/secure/auth/ctx_test.go b/examples/sample-application/secure/auth/ctx_test.go
new file mode 100644
index 0000000..2552ce4
--- /dev/null
+++ b/examples/sample-application/secure/auth/ctx_test.go
@@ -0,0 +1,107 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package auth
+
+import (
+	"net/http/httptest"
+	"testing"
+
+	"github.com/google/go-safeweb/safehttp"
+)
+
+func TestPutSessionAction(t *testing.T) {
+	req := safehttp.NewIncomingRequest(httptest.NewRequest(safehttp.MethodGet, "/spaghetti", nil))
+	ctx := req.Context()
+
+	putSessionAction(ctx, setSess)
+
+	if v := safehttp.FlightValues(ctx).Get(changeSessKey); v != setSess {
+		t.Errorf("putSessionAction(), got: %q, want: %q ", v, setSess)
+	}
+}
+
+func TestCtxSessionAction(t *testing.T) {
+	tests := []struct {
+		name  string
+		value interface{}
+		want  sessionAction
+	}{
+		{
+			name:  "basic session action set/get",
+			value: setSess,
+			want:  setSess,
+		},
+		{
+			name:  "no sessionAction value, return empty string",
+			value: "unexpected",
+			want:  "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := safehttp.NewIncomingRequest(httptest.NewRequest(safehttp.MethodGet, "/", nil))
+			ctx := req.Context()
+
+			safehttp.FlightValues(ctx).Put(changeSessKey, tt.value)
+
+			if got := ctxSessionAction(ctx); got != tt.want {
+				t.Errorf("ctxSessionAction() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestPutUser(t *testing.T) {
+	req := safehttp.NewIncomingRequest(httptest.NewRequest(safehttp.MethodGet, "/spaghetti", nil))
+	ctx := req.Context()
+	want := "someoneName"
+
+	putUser(ctx, want)
+
+	if v := safehttp.FlightValues(ctx).Get(userKey); v != want {
+		t.Errorf("putUser(), got: %q, want: %q ", v, setSess)
+	}
+}
+
+func TestCtxUser(t *testing.T) {
+	tests := []struct {
+		name  string
+		value interface{}
+		want  string
+	}{
+		{
+			name:  "basic session user set/get",
+			value: "safeUser",
+			want:  "safeUser",
+		},
+		{
+			name:  "no string value, return empty string",
+			value: 42,
+			want:  "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := safehttp.NewIncomingRequest(httptest.NewRequest(safehttp.MethodGet, "/", nil))
+			ctx := req.Context()
+
+			safehttp.FlightValues(ctx).Put(userKey, tt.value)
+
+			if got := ctxUser(ctx); got != tt.want {
+				t.Errorf("ctxUser() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/examples/sample-application/secure/dispatcher_test.go b/examples/sample-application/secure/dispatcher_test.go
new file mode 100644
index 0000000..9213c99
--- /dev/null
+++ b/examples/sample-application/secure/dispatcher_test.go
@@ -0,0 +1,58 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package secure
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/google/go-safeweb/examples/sample-application/secure/responses"
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/safehtml"
+)
+
+func TestDispatcherError(t *testing.T) {
+	tests := []struct {
+		name string
+		resp safehttp.ErrorResponse
+		want string
+	}{
+		{
+			name: "default dispatcher, plaintext",
+			resp: safehttp.StatusForbidden,
+			want: http.StatusText(int(safehttp.StatusForbidden)),
+		},
+		{
+			name: "custom dispatcher, safe HTML",
+			resp: responses.Error{
+				StatusCode: safehttp.StatusNotFound,
+				Message:    safehtml.HTMLEscaped("<h1>Escaped</h1"),
+			},
+			want: "<p>&lt;h1&gt;Escaped&lt;/h1</p>",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			d := dispatcher{}
+			rw := httptest.NewRecorder()
+			d.Error(rw, tt.resp)
+			if op := rw.Body.String(); !strings.Contains(op, tt.want) {
+				t.Errorf("body got: %q, want: %q", op, tt.want)
+			}
+		})
+	}
+}
diff --git a/examples/sample-application/secure/mux_test.go b/examples/sample-application/secure/mux_test.go
new file mode 100644
index 0000000..4bfd6fe
--- /dev/null
+++ b/examples/sample-application/secure/mux_test.go
@@ -0,0 +1,30 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package secure
+
+import (
+	"net/http/httptest"
+	"testing"
+
+	"github.com/google/go-safeweb/safehttp"
+)
+
+func TestNewMuxConfig(t *testing.T) {
+	mux := NewMuxConfig(nil, "foo:8080").Mux()
+	mux.Handle("/", safehttp.MethodGet, nil)
+	req := httptest.NewRequest(safehttp.MethodGet, "http://foo:8080/", nil)
+	rw := httptest.NewRecorder()
+	mux.ServeHTTP(rw, req)
+}
diff --git a/examples/trustedtypes/server_test.go b/examples/trustedtypes/server_test.go
new file mode 100644
index 0000000..6f1495b
--- /dev/null
+++ b/examples/trustedtypes/server_test.go
@@ -0,0 +1,96 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"fmt"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/google/safehtml/template"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/safehtml"
+)
+
+func TestNewMuxConfig(t *testing.T) {
+	cf, addr := newServeMuxConfig()
+	mux := cf.Mux()
+	h := safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+		return w.Write(safehtml.HTMLEscaped("<h1>Hello World!</h1>"))
+	})
+	mux.Handle("/", safehttp.MethodGet, h)
+
+	req := httptest.NewRequest(safehttp.MethodGet, fmt.Sprintf("http://%s/", addr), nil)
+	rw := httptest.NewRecorder()
+	mux.ServeHTTP(rw, req)
+
+	body := rw.Body.String()
+	if want := "&lt;h1&gt;Hello World!&lt;/h1&gt;"; body != want {
+		t.Errorf("body got: %q want: %q", body, want)
+	}
+}
+
+func Test_loadTemplate(t *testing.T) {
+	tests := []struct {
+		name    string
+		src     string
+		wantErr bool
+	}{
+		{
+			name:    "existing template, no error",
+			src:     "safe.html",
+			wantErr: false,
+		},
+		{
+			name:    "missing template, error",
+			src:     "not_existing.html",
+			wantErr: true,
+		},
+		{
+			name:    "invalid source name, error",
+			src:     "../../hidden.html",
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			defer func() {
+				if r := recover(); r != nil && !tt.wantErr {
+					t.Errorf("unexpected panic: %v", r)
+				}
+			}()
+			_, err := loadTemplate(tt.src)
+			if err != nil && !tt.wantErr {
+				t.Errorf("loadTemplate() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+		})
+	}
+}
+
+func TestHandleTemplate(t *testing.T) {
+	mux := safehttp.NewServeMuxConfig(nil).Mux()
+	safeTmpl := template.Must(template.New("standard").Parse(`<h1>Hi there!</h1>`))
+	mux.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(handleTemplate(safeTmpl)))
+
+	req := httptest.NewRequest(safehttp.MethodGet, "/spaghetti", nil)
+	rw := httptest.NewRecorder()
+	mux.ServeHTTP(rw, req)
+
+	if body, want := rw.Body.String(), "<h1>Hi there!</h1>"; body != want {
+		t.Errorf("handleTemplate() got %q, want %q", body, want)
+	}
+}
diff --git a/safehttp/plugins/xsrf/xsrfblockall/xsrf_test.go b/safehttp/plugins/xsrf/xsrfblockall/xsrf_test.go
new file mode 100644
index 0000000..ce995fe
--- /dev/null
+++ b/safehttp/plugins/xsrf/xsrfblockall/xsrf_test.go
@@ -0,0 +1,124 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package xsrfblockall
+
+import (
+	"testing"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
+)
+
+func TestStateChanging(t *testing.T) {
+	test := []struct {
+		name       string
+		host       string
+		method     string
+		wantStatus safehttp.StatusCode
+	}{
+		{
+			name:       "POST request",
+			host:       "foo.com",
+			method:     safehttp.MethodPost,
+			wantStatus: safehttp.StatusForbidden,
+		},
+		{
+			name:       "PUT request",
+			host:       "foo.com",
+			method:     safehttp.MethodPut,
+			wantStatus: safehttp.StatusForbidden,
+		},
+		{
+			name:       "DELETE request",
+			host:       "foo.com",
+			method:     safehttp.MethodDelete,
+			wantStatus: safehttp.StatusForbidden,
+		},
+		{
+			name:       "PATCH request",
+			host:       "foo.com",
+			method:     safehttp.MethodPatch,
+			wantStatus: safehttp.StatusForbidden,
+		},
+		{
+			name:       "TRACE request",
+			host:       "foo.com",
+			method:     safehttp.MethodTrace,
+			wantStatus: safehttp.StatusForbidden,
+		},
+		{
+			name:       "CONNECT request",
+			host:       "foo.com",
+			method:     safehttp.MethodConnect,
+			wantStatus: safehttp.StatusForbidden,
+		},
+	}
+
+	for _, test := range test {
+		t.Run(test.name, func(t *testing.T) {
+			fakeRW, rr := safehttptest.NewFakeResponseWriter()
+			req := safehttptest.NewRequest(test.method, "https://foo.com/", nil)
+
+			i := Interceptor{}
+			i.Before(fakeRW, req, nil)
+
+			if got := rr.Code; got != int(test.wantStatus) {
+				t.Errorf("rr.Code: got %v, want %v", got, test.wantStatus)
+			}
+		})
+	}
+}
+
+func TestNonStateChanging(t *testing.T) {
+	test := []struct {
+		name       string
+		host       string
+		method     string
+		wantStatus safehttp.StatusCode
+	}{
+		{
+			name:       "GET request",
+			host:       "foo.com",
+			method:     safehttp.MethodGet,
+			wantStatus: safehttp.StatusOK,
+		},
+		{
+			name:       "HEAD request",
+			host:       "foo.com",
+			method:     safehttp.MethodHead,
+			wantStatus: safehttp.StatusOK,
+		},
+		{
+			name:       "OPTIONS request",
+			host:       "foo.com",
+			method:     safehttp.MethodOptions,
+			wantStatus: safehttp.StatusOK,
+		},
+	}
+
+	for _, test := range test {
+		t.Run(test.name, func(t *testing.T) {
+			fakeRW, rr := safehttptest.NewFakeResponseWriter()
+			req := safehttptest.NewRequest(test.method, "https://foo.com/", nil)
+
+			i := Interceptor{}
+			i.Before(fakeRW, req, nil)
+
+			if got := rr.Code; got != int(test.wantStatus) {
+				t.Errorf("rr.Code: got %v, want %v", got, test.wantStatus)
+			}
+		})
+	}
+}