Refactor auth-proxy app to make it easier to test.

The command line application is now //kube/cmd/auth-proxy and just
a simple stub that calls into //kube/go/authproxy.

Added a cleanup.AtExit which allows for an orderly shutdown of
auth-proxy when receiving SIGTERM, which is important to let requests
drain from auth-proxy before termination:

https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-terminating-with-grace

Bug: b/249507110
Change-Id: Ie831590a02d8da9e53df3f49feae065b7f23f613
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/585897
Reviewed-by: Ravi Mistry <rmistry@google.com>
Commit-Queue: Joe Gregorio <jcgregorio@google.com>
diff --git a/go_repositories.bzl b/go_repositories.bzl
index fc652c7..824532f 100644
--- a/go_repositories.bzl
+++ b/go_repositories.bzl
@@ -2673,8 +2673,8 @@
     go_repository(
         name = "com_github_oklog_run",
         importpath = "github.com/oklog/run",
-        sum = "h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=",
-        version = "v1.0.0",
+        sum = "h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=",
+        version = "v1.1.0",
     )
 
     go_repository(
diff --git a/kube/Makefile b/kube/Makefile
index 253fda5..726899c 100644
--- a/kube/Makefile
+++ b/kube/Makefile
@@ -12,7 +12,7 @@
 	$(BAZEL) run //kube:configmap_reload_container
 
 release_auth_proxy:
-	CGO_ENABLED=0 GOOS=linux go install -a ./go/auth-proxy
+	CGO_ENABLED=0 GOOS=linux go install -a ./cmd/auth-proxy
 	./build_auth_proxy_release
 
 release_basealpine:
diff --git a/kube/cmd/auth-proxy/BUILD.bazel b/kube/cmd/auth-proxy/BUILD.bazel
new file mode 100644
index 0000000..adb94b1
--- /dev/null
+++ b/kube/cmd/auth-proxy/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "auth-proxy_lib",
+    srcs = ["main.go"],
+    importpath = "go.skia.org/infra/kube/cmd/auth-proxy",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//go/sklog",
+        "//kube/go/authproxy",
+    ],
+)
+
+go_binary(
+    name = "auth-proxy",
+    embed = [":auth-proxy_lib"],
+    visibility = ["//visibility:public"],
+)
diff --git a/kube/cmd/auth-proxy/main.go b/kube/cmd/auth-proxy/main.go
new file mode 100644
index 0000000..a42858d
--- /dev/null
+++ b/kube/cmd/auth-proxy/main.go
@@ -0,0 +1,10 @@
+package main
+
+import (
+	"go.skia.org/infra/go/sklog"
+	"go.skia.org/infra/kube/go/authproxy"
+)
+
+func main() {
+	sklog.Fatal(authproxy.Main())
+}
diff --git a/kube/go/auth-proxy/BUILD.bazel b/kube/go/auth-proxy/BUILD.bazel
deleted file mode 100644
index bed10f0..0000000
--- a/kube/go/auth-proxy/BUILD.bazel
+++ /dev/null
@@ -1,33 +0,0 @@
-load("//bazel/go:go_test.bzl", "go_test")
-load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
-
-go_library(
-    name = "auth-proxy_lib",
-    srcs = ["main.go"],
-    importpath = "go.skia.org/infra/kube/go/auth-proxy",
-    visibility = ["//visibility:private"],
-    deps = [
-        "//go/allowed",
-        "//go/common",
-        "//go/httputils",
-        "//go/sklog",
-        "//kube/go/auth-proxy/auth",
-        "@org_golang_x_oauth2//google",
-    ],
-)
-
-go_binary(
-    name = "auth-proxy",
-    embed = [":auth-proxy_lib"],
-    visibility = ["//visibility:public"],
-)
-
-go_test(
-    name = "auth-proxy_test",
-    srcs = ["main_test.go"],
-    embed = [":auth-proxy_lib"],
-    deps = [
-        "//kube/go/auth-proxy/auth/mocks",
-        "@com_github_stretchr_testify//require",
-    ],
-)
diff --git a/kube/go/auth-proxy/auth/mocks/generate.go b/kube/go/auth-proxy/auth/mocks/generate.go
deleted file mode 100644
index 6f9d347..0000000
--- a/kube/go/auth-proxy/auth/mocks/generate.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package mocks
-
-//go:generate bazelisk run //:mockery   -- --name Auth  --srcpkg=go.skia.org/infra/kube/go/auth-proxy/auth --output ${PWD}
diff --git a/kube/go/auth-proxy/main.go b/kube/go/auth-proxy/main.go
deleted file mode 100644
index 8858a4d..0000000
--- a/kube/go/auth-proxy/main.go
+++ /dev/null
@@ -1,143 +0,0 @@
-// auth-proxy is a reverse proxy that runs in front of applications and takes
-// care of authentication.
-//
-// This is useful for applications like Promentheus that doesn't handle
-// authentication itself, so we can run it behind auth-proxy to restrict access.
-//
-// The auth-proxy application also adds the X-WEBAUTH-USER header to each
-// authenticated request and gives it the value of the logged in users email
-// address, which can be used for audit logging. The application running behind
-// auth-proxy should then use:
-//
-//     https://pkg.go.dev/go.skia.org/infra/go/alogin/proxylogin
-//
-// When using --cria_group this application should be run using work-load
-// identity with a service account that as read access to CRIA, such as:
-//
-//     skia-auth-proxy-cria-reader@skia-public.iam.gserviceaccount.com
-//
-// See also:
-//
-//     https://chrome-infra-auth.appspot.com/auth/groups/project-skia-auth-service-access
-//
-//     https://grafana.com/blog/2015/12/07/grafana-authproxy-have-it-your-way/
-package main
-
-import (
-	"context"
-	"flag"
-	"fmt"
-	"net/http"
-	"net/http/httputil"
-	"net/url"
-	"strings"
-
-	"go.skia.org/infra/go/allowed"
-	"go.skia.org/infra/go/common"
-	"go.skia.org/infra/go/httputils"
-	"go.skia.org/infra/go/sklog"
-	"go.skia.org/infra/kube/go/auth-proxy/auth"
-	"golang.org/x/oauth2/google"
-)
-
-var (
-	criaGroup   = flag.String("cria_group", "", "The chrome infra auth group to use for restricting access. Example: 'google/skia-staff@google.com'")
-	local       = flag.Bool("local", false, "Running locally if true. As opposed to in production.")
-	port        = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
-	promPort    = flag.String("prom_port", ":20000", "Metrics service address (e.g., ':10110')")
-	targetPort  = flag.String("target_port", ":9000", "The port we are proxying to.")
-	allowPost   = flag.Bool("allow_post", false, "Allow POST requests to bypass auth.")
-	allowedFrom = flag.String("allowed_from", "", "A comma separated list of of domains and email addresses that are allowed to access the site. Example: 'google.com'")
-	passive     = flag.Bool("passive", false, "If true then allow unauthenticated requests to go through, while still adding logged in users emails in via the webAuthHeaderName.")
-)
-
-// Send the logged in user email in the following header. This allows decoupling
-// of authentication from the core of the app. See
-// https://grafana.com/blog/2015/12/07/grafana-authproxy-have-it-your-way/ for
-// how Grafana uses this to support almost any authentication handler.
-const webAuthHeaderName = "X-WEBAUTH-USER"
-
-type proxy struct {
-	reverseProxy http.Handler
-	authProvider auth.Auth
-}
-
-func newProxy(target *url.URL, authProvider auth.Auth) *proxy {
-	return &proxy{
-		reverseProxy: httputil.NewSingleHostReverseProxy(target),
-		authProvider: authProvider,
-	}
-}
-
-func (p proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	email := p.authProvider.LoggedInAs(r)
-	r.Header.Del(webAuthHeaderName)
-	r.Header.Add(webAuthHeaderName, email)
-	if r.Method == "POST" && *allowPost {
-		p.reverseProxy.ServeHTTP(w, r)
-		return
-	}
-	if !*passive {
-		if email == "" {
-			http.Redirect(w, r, p.authProvider.LoginURL(w, r), http.StatusSeeOther)
-			return
-		}
-		if !p.authProvider.IsViewer(r) {
-			http.Error(w, "403 Forbidden", http.StatusForbidden)
-			return
-		}
-	}
-	p.reverseProxy.ServeHTTP(w, r)
-}
-
-func validateFlags() error {
-	if *criaGroup != "" && *allowedFrom != "" {
-		return fmt.Errorf("Only one of the flags in [--auth_group, --allowed_from] can be specified.")
-	}
-	if *criaGroup == "" && *allowedFrom == "" {
-		return fmt.Errorf("At least one of the flags in [--auth_group, --allowed_from] must be specified.")
-	}
-
-	return nil
-}
-
-func main() {
-	common.InitWithMust(
-		"auth-proxy",
-		common.PrometheusOpt(promPort),
-		common.MetricsLoggingOpt(),
-	)
-
-	if err := validateFlags(); err != nil {
-		sklog.Fatal(err)
-	}
-
-	var allow allowed.Allow
-	if *criaGroup != "" {
-		ctx := context.Background()
-		ts, err := google.DefaultTokenSource(ctx, "email")
-		if err != nil {
-			sklog.Fatal(err)
-		}
-		criaClient := httputils.DefaultClientConfig().WithTokenSource(ts).With2xxOnly().Client()
-		allow, err = allowed.NewAllowedFromChromeInfraAuth(criaClient, *criaGroup)
-		if err != nil {
-			sklog.Fatal(err)
-		}
-	} else {
-		allow = allowed.NewAllowedFromList(strings.Split(*allowedFrom, ","))
-	}
-
-	authInstance := auth.New()
-	authInstance.SimpleInitWithAllow(*port, *local, nil, nil, allow)
-	targetURL := fmt.Sprintf("http://localhost%s", *targetPort)
-	target, err := url.Parse(targetURL)
-	if err != nil {
-		sklog.Fatalf("Unable to parse target URL %s: %s", targetURL, err)
-	}
-
-	var h http.Handler = newProxy(target, authInstance)
-	h = httputils.HealthzAndHTTPS(h)
-	http.Handle("/", h)
-	sklog.Fatal(http.ListenAndServe(*port, nil))
-}
diff --git a/kube/go/authproxy/BUILD.bazel b/kube/go/authproxy/BUILD.bazel
new file mode 100644
index 0000000..56edb57
--- /dev/null
+++ b/kube/go/authproxy/BUILD.bazel
@@ -0,0 +1,31 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("//bazel/go:go_test.bzl", "go_test")
+
+go_library(
+    name = "authproxy",
+    srcs = ["authproxy.go"],
+    importpath = "go.skia.org/infra/kube/go/authproxy",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//go/allowed",
+        "//go/cleanup",
+        "//go/common",
+        "//go/httputils",
+        "//go/skerr",
+        "//go/sklog",
+        "//kube/go/authproxy/auth",
+        "@org_golang_x_oauth2//google",
+    ],
+)
+
+go_test(
+    name = "authproxy_test",
+    srcs = ["authproxy_test.go"],
+    embed = [":authproxy"],
+    deps = [
+        "//go/cleanup",
+        "//kube/go/authproxy/auth/mocks",
+        "@com_github_stretchr_testify//assert",
+        "@com_github_stretchr_testify//require",
+    ],
+)
diff --git a/kube/go/auth-proxy/auth/BUILD.bazel b/kube/go/authproxy/auth/BUILD.bazel
similarity index 80%
rename from kube/go/auth-proxy/auth/BUILD.bazel
rename to kube/go/authproxy/auth/BUILD.bazel
index 6464262..21ddcc7 100644
--- a/kube/go/auth-proxy/auth/BUILD.bazel
+++ b/kube/go/authproxy/auth/BUILD.bazel
@@ -6,7 +6,7 @@
         "auth.go",
         "impl.go",
     ],
-    importpath = "go.skia.org/infra/kube/go/auth-proxy/auth",
+    importpath = "go.skia.org/infra/kube/go/authproxy/auth",
     visibility = ["//visibility:public"],
     deps = [
         "//go/allowed",
diff --git a/kube/go/auth-proxy/auth/auth.go b/kube/go/authproxy/auth/auth.go
similarity index 100%
rename from kube/go/auth-proxy/auth/auth.go
rename to kube/go/authproxy/auth/auth.go
diff --git a/kube/go/auth-proxy/auth/impl.go b/kube/go/authproxy/auth/impl.go
similarity index 100%
rename from kube/go/auth-proxy/auth/impl.go
rename to kube/go/authproxy/auth/impl.go
diff --git a/kube/go/auth-proxy/auth/mocks/Auth.go b/kube/go/authproxy/auth/mocks/Auth.go
similarity index 100%
rename from kube/go/auth-proxy/auth/mocks/Auth.go
rename to kube/go/authproxy/auth/mocks/Auth.go
diff --git a/kube/go/auth-proxy/auth/mocks/BUILD.bazel b/kube/go/authproxy/auth/mocks/BUILD.bazel
similarity index 80%
rename from kube/go/auth-proxy/auth/mocks/BUILD.bazel
rename to kube/go/authproxy/auth/mocks/BUILD.bazel
index d554267..926abd0 100644
--- a/kube/go/auth-proxy/auth/mocks/BUILD.bazel
+++ b/kube/go/authproxy/auth/mocks/BUILD.bazel
@@ -6,7 +6,7 @@
         "Auth.go",
         "generate.go",
     ],
-    importpath = "go.skia.org/infra/kube/go/auth-proxy/auth/mocks",
+    importpath = "go.skia.org/infra/kube/go/authproxy/auth/mocks",
     visibility = ["//visibility:public"],
     deps = [
         "//go/allowed",
diff --git a/kube/go/authproxy/auth/mocks/generate.go b/kube/go/authproxy/auth/mocks/generate.go
new file mode 100644
index 0000000..62b9bdb
--- /dev/null
+++ b/kube/go/authproxy/auth/mocks/generate.go
@@ -0,0 +1,3 @@
+package mocks
+
+//go:generate bazelisk run //:mockery   -- --name Auth  --srcpkg=go.skia.org/infra/kube/go/authproxy/auth --output ${PWD}
diff --git a/kube/go/authproxy/authproxy.go b/kube/go/authproxy/authproxy.go
new file mode 100644
index 0000000..49d8029
--- /dev/null
+++ b/kube/go/authproxy/authproxy.go
@@ -0,0 +1,234 @@
+// auth-proxy is a reverse proxy that runs in front of applications and takes
+// care of authentication.
+//
+// This is useful for applications like Promentheus that doesn't handle
+// authentication itself, so we can run it behind auth-proxy to restrict access.
+//
+// The auth-proxy application also adds the X-WEBAUTH-USER header to each
+// authenticated request and gives it the value of the logged in users email
+// address, which can be used for audit logging. The application running behind
+// auth-proxy should then use:
+//
+//     https://pkg.go.dev/go.skia.org/infra/go/alogin/proxylogin
+//
+// When using --cria_group this application should be run using work-load
+// identity with a service account that as read access to CRIA, such as:
+//
+//     skia-auth-proxy-cria-reader@skia-public.iam.gserviceaccount.com
+//
+// See also:
+//
+//     https://chrome-infra-auth.appspot.com/auth/groups/project-skia-auth-service-access
+//
+//     https://grafana.com/blog/2015/12/07/grafana-authproxy-have-it-your-way/
+package authproxy
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"net/http"
+	"net/http/httputil"
+	"net/url"
+	"strings"
+	"time"
+
+	"go.skia.org/infra/go/allowed"
+	"go.skia.org/infra/go/cleanup"
+	"go.skia.org/infra/go/common"
+	"go.skia.org/infra/go/httputils"
+	"go.skia.org/infra/go/skerr"
+	"go.skia.org/infra/go/sklog"
+	"go.skia.org/infra/kube/go/authproxy/auth"
+	"golang.org/x/oauth2/google"
+)
+
+const (
+	appName            = "auth-proxy"
+	serverReadTimeout  = time.Hour
+	serverWriteTimeout = time.Hour
+	drainTime          = time.Minute
+)
+
+// Send the logged in user email in the following header. This allows decoupling
+// of authentication from the core of the app. See
+// https://grafana.com/blog/2015/12/07/grafana-authproxy-have-it-your-way/ for
+// how Grafana uses this to support almost any authentication handler.
+const webAuthHeaderName = "X-WEBAUTH-USER"
+
+type proxy struct {
+	allowPost    bool
+	passive      bool
+	reverseProxy http.Handler
+	authProvider auth.Auth
+}
+
+func newProxy(target *url.URL, authProvider auth.Auth, allowPost bool, passive bool) *proxy {
+	return &proxy{
+		reverseProxy: httputil.NewSingleHostReverseProxy(target),
+		authProvider: authProvider,
+		allowPost:    allowPost,
+		passive:      passive,
+	}
+}
+
+func (p proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	email := p.authProvider.LoggedInAs(r)
+	r.Header.Del(webAuthHeaderName)
+	r.Header.Add(webAuthHeaderName, email)
+	if r.Method == "POST" && p.allowPost {
+		p.reverseProxy.ServeHTTP(w, r)
+		return
+	}
+	if !p.passive {
+		if email == "" {
+			http.Redirect(w, r, p.authProvider.LoginURL(w, r), http.StatusSeeOther)
+			return
+		}
+		if !p.authProvider.IsViewer(r) {
+			http.Error(w, "403 Forbidden", http.StatusForbidden)
+			return
+		}
+	}
+	p.reverseProxy.ServeHTTP(w, r)
+}
+
+// App is the auth-proxy application.
+type App struct {
+	port        string
+	promPort    string
+	criaGroup   string
+	local       bool
+	targetPort  string
+	allowPost   bool
+	allowedFrom string
+	passive     bool
+
+	target       *url.URL
+	authProvider auth.Auth
+	server       *http.Server
+}
+
+// Flagset constructs a flag.FlagSet for the App.
+func (a *App) Flagset() *flag.FlagSet {
+	fs := flag.NewFlagSet(appName, flag.ExitOnError)
+	fs.StringVar(&a.port, "port", ":8000", "HTTP service address (e.g., ':8000')")
+	fs.StringVar(&a.promPort, "prom-port", ":20000", "Metrics service address (e.g., ':10110')")
+	fs.StringVar(&a.criaGroup, "cria_group", "", "The chrome infra auth group to use for restricting access. Example: 'google/skia-staff@google.com'")
+	fs.BoolVar(&a.local, "local", false, "Running locally if true. As opposed to in production.")
+	fs.StringVar(&a.targetPort, "target_port", ":9000", "The port we are proxying to.")
+	fs.BoolVar(&a.allowPost, "allow_post", false, "Allow POST requests to bypass auth.")
+	fs.StringVar(&a.allowedFrom, "allowed_from", "", "A comma separated list of of domains and email addresses that are allowed to access the site. Example: 'google.com'")
+	fs.BoolVar(&a.passive, "passive", false, "If true then allow unauthenticated requests to go through, while still adding logged in users emails in via the webAuthHeaderName.")
+
+	return fs
+}
+
+// New returns a new *App.
+func New(ctx context.Context) (*App, error) {
+	var ret App
+
+	err := common.InitWith(
+		appName,
+		common.PrometheusOpt(&ret.promPort),
+		common.MetricsLoggingOpt(),
+		common.FlagSetOpt(ret.Flagset()),
+	)
+	if err != nil {
+		return nil, skerr.Wrap(err)
+	}
+
+	err = ret.validateFlags()
+	if err != nil {
+		return nil, skerr.Wrap(err)
+	}
+
+	var allow allowed.Allow
+	if ret.criaGroup != "" {
+		ctx := context.Background()
+		ts, err := google.DefaultTokenSource(ctx, "email")
+		if err != nil {
+			return nil, skerr.Wrap(err)
+		}
+		criaClient := httputils.DefaultClientConfig().WithTokenSource(ts).With2xxOnly().Client()
+		allow, err = allowed.NewAllowedFromChromeInfraAuth(criaClient, ret.criaGroup)
+		if err != nil {
+			return nil, skerr.Wrap(err)
+		}
+	} else {
+		allow = allowed.NewAllowedFromList(strings.Split(ret.allowedFrom, ","))
+	}
+
+	authInstance := auth.New()
+	authInstance.SimpleInitWithAllow(ret.port, ret.local, nil, nil, allow)
+	targetURL := fmt.Sprintf("http://localhost%s", ret.targetPort)
+	target, err := url.Parse(targetURL)
+	if err != nil {
+		return nil, skerr.Wrap(err)
+	}
+	ret.authProvider = authInstance
+	ret.target = target
+	ret.registerCleanup()
+
+	return &ret, nil
+}
+
+func (a *App) registerCleanup() {
+	cleanup.AtExit(func() {
+		if a.server != nil {
+			sklog.Info("Shutdown server gracefully.")
+			ctx, cancel := context.WithTimeout(context.Background(), drainTime)
+			err := a.server.Shutdown(ctx)
+			if err != nil {
+				sklog.Error(err)
+			}
+			cancel()
+		}
+	})
+
+}
+
+// Run starts the application serving, it does not return unless there is an
+// error or the passed in context is cancelled.
+func (a *App) Run(ctx context.Context) error {
+	var h http.Handler = newProxy(a.target, a.authProvider, a.allowPost, a.passive)
+	h = httputils.HealthzAndHTTPS(h)
+	server := &http.Server{
+		Addr:           a.port,
+		Handler:        h,
+		ReadTimeout:    serverReadTimeout,
+		WriteTimeout:   serverWriteTimeout,
+		MaxHeaderBytes: 1 << 20,
+	}
+	a.server = server
+
+	sklog.Infof("Ready to serve on port %s", a.port)
+	err := server.ListenAndServe()
+	if err == http.ErrServerClosed {
+		// This is an orderly shutdown.
+		return nil
+	}
+	return skerr.Wrap(err)
+}
+
+func (a *App) validateFlags() error {
+	if a.criaGroup != "" && a.allowedFrom != "" {
+		return fmt.Errorf("Only one of the flags in [--auth_group, --allowed_from] can be specified.")
+	}
+	if a.criaGroup == "" && a.allowedFrom == "" {
+		return fmt.Errorf("At least one of the flags in [--auth_group, --allowed_from] must be specified.")
+	}
+
+	return nil
+}
+
+// Main constructs and runs the application. This function will only return on failure.
+func Main() error {
+	ctx := context.Background()
+	app, err := New(ctx)
+	if err != nil {
+		return skerr.Wrap(err)
+	}
+
+	return app.Run(ctx)
+}
diff --git a/kube/go/auth-proxy/main_test.go b/kube/go/authproxy/authproxy_test.go
similarity index 73%
rename from kube/go/auth-proxy/main_test.go
rename to kube/go/authproxy/authproxy_test.go
index a1a0085..9110724 100644
--- a/kube/go/auth-proxy/main_test.go
+++ b/kube/go/authproxy/authproxy_test.go
@@ -1,20 +1,23 @@
-package main
+package authproxy
 
 import (
+	"context"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
+	"sync"
 	"testing"
+	"time"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	"go.skia.org/infra/kube/go/auth-proxy/auth/mocks"
+	"go.skia.org/infra/go/cleanup"
+	"go.skia.org/infra/kube/go/authproxy/auth/mocks"
 )
 
 const email = "nobody@example.org"
 
 func setupForTest(t *testing.T, cb http.HandlerFunc) (*url.URL, *bool, *httptest.ResponseRecorder, *http.Request) {
-	*allowPost = false
-	*passive = false
 	called := false
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		cb(w, r)
@@ -38,11 +41,10 @@
 		require.Equal(t, []string{""}, r.Header.Values(webAuthHeaderName))
 		require.Equal(t, []string(nil), r.Header.Values("X-SOME-UNSET-HEADER"))
 	})
-	*allowPost = true
 	authMock := &mocks.Auth{}
 	authMock.On("LoggedInAs", r).Return("")
 
-	proxy := newProxy(u, authMock)
+	proxy := newProxy(u, authMock, true, false)
 
 	proxy.ServeHTTP(w, r)
 	require.True(t, *called)
@@ -59,7 +61,7 @@
 	authMock.On("LoggedInAs", r).Return(email)
 	authMock.On("IsViewer", r).Return(true)
 
-	proxy := newProxy(u, authMock)
+	proxy := newProxy(u, authMock, false, false)
 
 	proxy.ServeHTTP(w, r)
 	require.True(t, *called)
@@ -74,7 +76,7 @@
 	authMock.On("LoggedInAs", r).Return("")
 	authMock.On("LoginURL", w, r).Return("http://example.org/login")
 
-	proxy := newProxy(u, authMock)
+	proxy := newProxy(u, authMock, false, false)
 
 	proxy.ServeHTTP(w, r)
 	require.False(t, *called)
@@ -88,7 +90,7 @@
 	authMock.On("LoggedInAs", r).Return(email)
 	authMock.On("IsViewer", r).Return(false)
 
-	proxy := newProxy(u, authMock)
+	proxy := newProxy(u, authMock, false, false)
 
 	proxy.ServeHTTP(w, r)
 	require.False(t, *called)
@@ -106,7 +108,7 @@
 	authMock.On("LoggedInAs", r).Return(email)
 	authMock.On("IsViewer", r).Return(true)
 
-	proxy := newProxy(u, authMock)
+	proxy := newProxy(u, authMock, false, false)
 
 	proxy.ServeHTTP(w, r)
 	require.True(t, *called)
@@ -119,12 +121,11 @@
 		require.Equal(t, []string{""}, r.Header.Values(webAuthHeaderName))
 	})
 
-	*passive = true
 	r.Header.Add(webAuthHeaderName, "haxor@example.org") // Try to spoof the header.
 	authMock := &mocks.Auth{}
 	authMock.On("LoggedInAs", r).Return("")
 
-	proxy := newProxy(u, authMock)
+	proxy := newProxy(u, authMock, false, true)
 
 	proxy.ServeHTTP(w, r)
 	require.True(t, *called)
@@ -137,12 +138,11 @@
 		require.Equal(t, []string{email}, r.Header.Values(webAuthHeaderName))
 	})
 
-	*passive = true
 	r.Header.Add(webAuthHeaderName, "haxor@example.org") // Try to spoof the header.
 	authMock := &mocks.Auth{}
 	authMock.On("LoggedInAs", r).Return(email)
 
-	proxy := newProxy(u, authMock)
+	proxy := newProxy(u, authMock, false, true)
 
 	proxy.ServeHTTP(w, r)
 	require.True(t, *called)
@@ -150,27 +150,67 @@
 }
 
 func TestValidateFlags_BothFlagsSpecified_ReturnsError(t *testing.T) {
-	*criaGroup = "project-angle-committers"
-	*allowedFrom = "google.com"
+	app := &App{
+		criaGroup:   "project-angle-committers",
+		allowedFrom: "google.com",
+	}
 
-	require.Error(t, validateFlags())
+	require.Error(t, app.validateFlags())
 }
 
 func TestValidateFlags_NeitherFlagIsSpecified_ReturnsError(t *testing.T) {
-	*criaGroup = ""
-	*allowedFrom = ""
+	app := &App{
+		criaGroup:   "",
+		allowedFrom: "",
+	}
 
-	require.Error(t, validateFlags())
+	require.Error(t, app.validateFlags())
 }
 
 func TestValidateFlags_OnlyOneFlagIsSpecified_ReturnsNoError(t *testing.T) {
-	*criaGroup = "project-angle-committers"
-	*allowedFrom = ""
 
-	require.NoError(t, validateFlags())
+	app := &App{
+		criaGroup:   "project-angle-committers",
+		allowedFrom: "",
+	}
 
-	*criaGroup = ""
-	*allowedFrom = "google.com"
+	require.NoError(t, app.validateFlags())
 
-	require.NoError(t, validateFlags())
+	app = &App{
+		criaGroup:   "",
+		allowedFrom: "google.com",
+	}
+
+	require.NoError(t, app.validateFlags())
+}
+
+func TestAppRun_ContextIsCancelled_ReturnsNil(t *testing.T) {
+	// Construct minimal App.
+	target, err := url.Parse("http://my-service")
+	require.NoError(t, err)
+	app := &App{
+		target:   target,
+		port:     ":0",
+		promPort: ":0",
+	}
+	app.registerCleanup()
+
+	var w sync.WaitGroup
+	w.Add(1)
+	go func() {
+		err := app.Run(context.Background())
+		assert.NoError(t, err)
+		w.Done()
+	}()
+
+	// Ensure the server has been started.
+	for app.server == nil {
+		time.Sleep(time.Millisecond)
+	}
+
+	// Force a cleanup.
+	cleanup.Cleanup()
+	w.Wait()
+
+	// Test will fail by timeout if the app.Run() didn't return.
 }