Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-pages.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJ. Shuster <joshuagregoryshuster@gmail.com>2017-09-08 11:58:42 +0300
committerNick Thomas <nick@gitlab.com>2017-09-08 11:58:42 +0300
commit0a144bc6055b41fac726fdc6eeaa7150f622bd20 (patch)
tree9ed2a4dbecbd8146245ae8fa609cd9f54d038bf6 /internal
parent0173d4e6b6b17443155d121a9098d0e742b9c4e3 (diff)
Add an artifacts proxy to GitLab Pages
Diffstat (limited to 'internal')
-rw-r--r--internal/artifact/artifact.go120
-rw-r--r--internal/artifact/artifact_test.go303
-rw-r--r--internal/httperrors/httperrors.go162
-rw-r--r--internal/httperrors/httperrors_test.go105
4 files changed, 690 insertions, 0 deletions
diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go
new file mode 100644
index 00000000..bcb525ac
--- /dev/null
+++ b/internal/artifact/artifact.go
@@ -0,0 +1,120 @@
+package artifact
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors"
+)
+
+const (
+ baseURL = "/projects/%s/jobs/%s/artifacts"
+ hostPatternTemplate = `(?i)\Aartifact~(\d+)~(\d+)\.%s\z`
+ minStatusCode = 200
+ maxStatusCode = 299
+)
+
+// Artifact is a struct that is made up of a url.URL, http.Client, and
+// regexp.Regexp that is used to proxy requests where applicable.
+type Artifact struct {
+ server string
+ client *http.Client
+ pattern *regexp.Regexp
+}
+
+// New when provided the arguments defined herein, returns a pointer to an
+// Artifact that is used to proxy requests.
+func New(s string, timeout int, pagesDomain string) *Artifact {
+ return &Artifact{
+ server: s,
+ client: &http.Client{Timeout: time.Second * time.Duration(timeout)},
+ pattern: hostPatternGen(pagesDomain),
+ }
+
+}
+
+// TryMakeRequest will attempt to proxy a request and write it to the argument
+// http.ResponseWriter, ultimately returning a bool that indicates if the
+// http.ResponseWriter has been written to in any capacity.
+func (a *Artifact) TryMakeRequest(host string, w http.ResponseWriter, r *http.Request) bool {
+ if a == nil || a.server == "" {
+ return false
+ }
+
+ reqURL, ok := a.buildURL(host, r.URL.Path)
+ if !ok {
+ return false
+ }
+
+ resp, err := a.client.Get(reqURL.String())
+ if err != nil {
+ httperrors.Serve502(w)
+ return true
+ }
+
+ if resp.StatusCode == http.StatusNotFound {
+ httperrors.Serve404(w)
+ return true
+ }
+
+ if resp.StatusCode == http.StatusInternalServerError {
+ httperrors.Serve500(w)
+ return true
+ }
+
+ // we only cache responses within the 2xx series response codes
+ if (resp.StatusCode >= minStatusCode) && (resp.StatusCode <= maxStatusCode) {
+ w.Header().Set("Cache-Control", "max-age=3600")
+ }
+
+ w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
+ w.Header().Set("Content-Length", strconv.FormatInt(resp.ContentLength, 10))
+ w.WriteHeader(resp.StatusCode)
+ io.Copy(w, resp.Body)
+ return true
+}
+
+// buildURL returns a pointer to a url.URL for where the request should be
+// proxied to. The returned bool will indicate if there is some sort of issue
+// with the url while it is being generated.
+func (a *Artifact) buildURL(host, path string) (*url.URL, bool) {
+ ids := a.pattern.FindAllStringSubmatch(host, -1)
+ if len(ids) != 1 || len(ids[0]) != 3 {
+ return nil, false
+ }
+
+ strippedIds := ids[0][1:3]
+ body := fmt.Sprintf(baseURL, strippedIds[0], strippedIds[1])
+ ourPath := a.server
+ if strings.HasSuffix(ourPath, "/") {
+ ourPath = ourPath[0:len(ourPath)-1] + body
+ } else {
+ ourPath = ourPath + body
+ }
+
+ if len(path) == 0 || strings.HasPrefix(path, "/") {
+ ourPath = ourPath + path
+ } else {
+ ourPath = ourPath + "/" + path
+ }
+
+ u, err := url.Parse(ourPath)
+ if err != nil {
+ return nil, false
+ }
+ return u, true
+}
+
+// hostPatternGen returns a pointer to a regexp.Regexp that is made up of
+// the constant hostPatternTemplate and the argument which represents the pages domain.
+// This is used to ensure that the requested page meets not only the hostPatternTemplate
+// requirements, but is suffixed with the proper pagesDomain.
+func hostPatternGen(pagesDomain string) *regexp.Regexp {
+ return regexp.MustCompile(fmt.Sprintf(hostPatternTemplate, regexp.QuoteMeta(pagesDomain)))
+}
diff --git a/internal/artifact/artifact_test.go b/internal/artifact/artifact_test.go
new file mode 100644
index 00000000..ad2285f6
--- /dev/null
+++ b/internal/artifact/artifact_test.go
@@ -0,0 +1,303 @@
+package artifact
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "regexp"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestTryMakeRequest(t *testing.T) {
+ content := "<!DOCTYPE html><html><head><title>Title of the document</title></head><body></body></html>"
+ contentType := "text/html; charset=utf-8"
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", contentType)
+ switch r.URL.Path {
+ case "/projects/1/jobs/2/artifacts/200.html":
+ w.WriteHeader(http.StatusOK)
+ case "/projects/1/jobs/2/artifacts/max-caching.html":
+ w.WriteHeader(http.StatusIMUsed)
+ case "/projects/1/jobs/2/artifacts/non-caching.html":
+ w.WriteHeader(http.StatusTeapot)
+ case "/projects/1/jobs/2/artifacts/500.html":
+ w.WriteHeader(http.StatusInternalServerError)
+ case "/projects/1/jobs/2/artifacts/404.html":
+ w.WriteHeader(http.StatusNotFound)
+ }
+ fmt.Fprint(w, content)
+ }))
+ defer testServer.Close()
+
+ cases := []struct {
+ Path string
+ Status int
+ Content string
+ Length string
+ CacheControl string
+ ContentType string
+ Description string
+ }{
+ {
+ "/200.html",
+ http.StatusOK,
+ content,
+ "90",
+ "max-age=3600",
+ "text/html; charset=utf-8",
+ "basic successful request",
+ },
+ {
+ "/max-caching.html",
+ http.StatusIMUsed,
+ content,
+ "90",
+ "max-age=3600",
+ "text/html; charset=utf-8",
+ "max caching request",
+ },
+ {
+ "/non-caching.html",
+ http.StatusTeapot,
+ content,
+ "90",
+ "",
+ "text/html; charset=utf-8",
+ "no caching request",
+ },
+ }
+
+ for _, c := range cases {
+ result := httptest.NewRecorder()
+ reqURL, err := url.Parse(c.Path)
+ assert.NoError(t, err)
+ r := &http.Request{URL: reqURL}
+ art := &Artifact{
+ server: testServer.URL,
+ client: &http.Client{Timeout: time.Second * time.Duration(1)},
+ pattern: regexp.MustCompile(fmt.Sprintf(hostPatternTemplate, "gitlab-example.io")),
+ }
+
+ assert.True(t, art.TryMakeRequest("artifact~1~2.gitlab-example.io", result, r))
+ assert.Equal(t, c.ContentType, result.Header().Get("Content-Type"))
+ assert.Equal(t, c.Length, result.Header().Get("Content-Length"))
+ assert.Equal(t, c.CacheControl, result.Header().Get("Cache-Control"))
+ assert.Equal(t, c.Content, string(result.Body.Bytes()))
+ assert.Equal(t, c.Status, result.Code)
+ }
+}
+
+func TestBuildURL(t *testing.T) {
+ cases := []struct {
+ RawServer string
+ Host string
+ Path string
+ Expected string
+ PagesDomain string
+ Ok bool
+ Description string
+ }{
+ {
+ "https://gitlab.com/api/v4",
+ "artifact~1~2.gitlab.io",
+ "/path/to/file.txt",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/path/to/file.txt",
+ "gitlab.io",
+ true,
+ "basic case",
+ },
+ {
+ "https://gitlab.com/api/v4/",
+ "artifact~1~2.gitlab.io",
+ "/path/to/file.txt",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/path/to/file.txt",
+ "gitlab.io",
+ true,
+ "basic case 2",
+ },
+ {
+ "https://gitlab.com/api/v4",
+ "artifact~1~2.gitlab.io",
+ "path/to/file.txt",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/path/to/file.txt",
+ "gitlab.io",
+ true,
+ "basic case 3",
+ },
+ {
+ "https://gitlab.com/api/v4/",
+ "artifact~1~2.gitlab.io",
+ "path/to/file.txt",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/path/to/file.txt",
+ "gitlab.io",
+ true,
+ "basic case 4",
+ },
+ {
+ "https://gitlab.com/api/v4",
+ "artifact~1~2.gitlab.io",
+ "",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts",
+ "gitlab.io",
+ true,
+ "basic case 5",
+ },
+ {
+ "https://gitlab.com/api/v4/",
+ "artifact~1~2.gitlab.io",
+ "",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts",
+ "gitlab.io",
+ true,
+ "basic case 6",
+ },
+ {
+ "https://gitlab.com/api/v4",
+ "artifact~1~2.gitlab.io",
+ "/",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/",
+ "gitlab.io",
+ true,
+ "basic case 7",
+ },
+ {
+ "https://gitlab.com/api/v4/",
+ "artifact~1~2.gitlab.io",
+ "/",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/",
+ "gitlab.io",
+ true,
+ "basic case 8",
+ },
+ {
+ "https://gitlab.com/api/v4",
+ "artifact~100000~200000.gitlab.io",
+ "/file.txt",
+ "https://gitlab.com/api/v4/projects/100000/jobs/200000/artifacts/file.txt",
+ "gitlab.io",
+ true,
+ "expanded case",
+ },
+ {
+ "https://gitlab.com/api/v4/",
+ "artifact~1~2.gitlab.io",
+ "/file.txt",
+ "https://gitlab.com/api/v4/projects/1/jobs/2/artifacts/file.txt",
+ "gitlab.io",
+ true,
+ "server with tailing slash",
+ },
+ {
+ "https://gitlab.com/api/v4",
+ "artifact~A~B.gitlab.io",
+ "/index.html",
+ "",
+ "example.com",
+ false,
+ "non matching domain and request",
+ },
+ {
+ "",
+ "artifact~A~B.gitlab.io",
+ "",
+ "",
+ "",
+ false,
+ "un-parseable Host",
+ },
+ }
+
+ for _, c := range cases {
+ a := &Artifact{server: c.RawServer, pattern: regexp.MustCompile(fmt.Sprintf(hostPatternTemplate, c.PagesDomain))}
+ u, ok := a.buildURL(c.Host, c.Path)
+ assert.Equal(t, c.Ok, ok, c.Description)
+ if c.Ok {
+ assert.Equal(t, c.Expected, u.String(), c.Description)
+ }
+ }
+}
+
+func TestMatchHostGen(t *testing.T) {
+ cases := []struct {
+ URLHost string
+ PagesDomain string
+ Expected bool
+ Description string
+ }{
+ {
+ "artifact~1~2.gitlab.io",
+ "gitlab.io",
+ true,
+ "basic case",
+ },
+ {
+ "ARTIFACT~1~2.gitlab.io",
+ "gitlab.io",
+ true,
+ "capital letters case",
+ },
+ {
+ "ARTIFACT~11234~2908908.gitlab.io",
+ "gitlab.io",
+ true,
+ "additional capital letters case",
+ },
+ {
+ "artifact~10000~20000.gitlab.io",
+ "gitlab.io",
+ true,
+ "expanded case",
+ },
+ {
+ "artifact~86753095555~55550935768.gitlab.io",
+ "gitlab.io",
+ true,
+ "large number case",
+ },
+ {
+ "artifact~one~two.gitlab.io",
+ "gitlab.io",
+ false,
+ "letters rather than numbers",
+ },
+ {
+ "artifact~One111~tWo222.gitlab.io",
+ "gitlab.io",
+ false,
+ "Mixture of alphanumeric",
+ },
+ {
+ "artifact~!@#$%~%$#@!.gitlab.io",
+ "gitlab.io",
+ false,
+ "special characters",
+ },
+ {
+ "artifact~1.gitlab.io",
+ "gitlab.io",
+ false,
+ "not enough ids",
+ },
+ {
+ "artifact~1~2~34444~1~4.gitlab.io",
+ "gitlab.io",
+ false,
+ "too many ids",
+ },
+ {
+ "artifact~1~2.gitlab.io",
+ "otherhost.io",
+ false,
+ "different domain / suffix",
+ },
+ }
+
+ for _, c := range cases {
+ reg := hostPatternGen(c.PagesDomain)
+ assert.Equal(t, c.Expected, reg.MatchString(c.URLHost), c.Description)
+ }
+}
diff --git a/internal/httperrors/httperrors.go b/internal/httperrors/httperrors.go
new file mode 100644
index 00000000..82d43fc5
--- /dev/null
+++ b/internal/httperrors/httperrors.go
@@ -0,0 +1,162 @@
+package httperrors
+
+import (
+ "fmt"
+ "net/http"
+)
+
+type content struct {
+ status int
+ title string
+ statusString string
+ header string
+ subHeader string
+}
+
+var (
+ content404 = content{
+ http.StatusNotFound,
+ "The page you're looking for could not be found (404)",
+ "404",
+ "The page you're looking for could not be found.",
+ `<p>The resource that you are attempting to access does not exist or you don't have the necessary permissions to view it.</p>
+ <p>Make sure the address is correct and that the page hasn't moved.</p>
+ <p>Please contact your GitLab administrator if you think this is a mistake.</p>`,
+ }
+ content500 = content{
+ http.StatusInternalServerError,
+ "Something went wrong (500)",
+ "500",
+ "Whoops, something went wrong on our end.",
+ `<p>Try refreshing the page, or going back and attempting the action again.</p>
+ <p>Please contact your GitLab administrator if this problem persists.</p>`,
+ }
+
+ content502 = content{
+ http.StatusBadGateway,
+ "Something went wrong (502)",
+ "502",
+ "Whoops, something went wrong on our end.",
+ `<p>Try refreshing the page, or going back and attempting the action again.</p>
+ <p>Please contact your GitLab administrator if this problem persists.</p>`,
+ }
+)
+
+const predefinedErrorPage = `
+<!DOCTYPE html>
+<html>
+<head>
+ <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
+ <title>%v</title>
+ <style>
+ body {
+ color: #666;
+ text-align: center;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ margin: auto;
+ font-size: 14px;
+ }
+
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: 400;
+ color: #456;
+ }
+
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
+
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: 400;
+ line-height: 28px;
+ }
+
+ hr {
+ max-width: 800px;
+ margin: 18px auto;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+
+ img {
+ max-width: 40vw;
+ display: block;
+ margin: 40px auto;
+ }
+
+ a {
+ line-height: 100px;
+ font-weight: 400;
+ color: #4A8BEE;
+ font-size: 18px;
+ text-decoration: none;
+ }
+
+ .container {
+ margin: auto 20px;
+ }
+
+ .go-back {
+ display: none;
+ }
+
+ </style>
+</head>
+
+<body>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo="
+ alt="GitLab Logo" />
+ <h1>
+ %v
+ </h1>
+ <div class="container">
+ <h3>%v</h3>
+ <hr />
+ %v
+ <a href="javascript:history.back()" class="js-go-back go-back">Go back</a>
+ </div>
+ <script>
+ (function () {
+ var goBack = document.querySelector('.js-go-back');
+
+ if (history.length > 1) {
+ goBack.style.display = 'inline';
+ }
+ })();
+ </script>
+</body>
+</html>
+`
+
+func generateErrorHTML(c content) string {
+ return fmt.Sprintf(predefinedErrorPage, c.title, c.statusString, c.header, c.subHeader)
+}
+
+func serveErrorPage(w http.ResponseWriter, c content) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Header().Set("X-Content-Type-Options", "nosniff")
+ w.WriteHeader(c.status)
+ fmt.Fprintln(w, generateErrorHTML(c))
+}
+
+// Serve404 returns a 404 error response / HTML page to the http.ResponseWriter
+func Serve404(w http.ResponseWriter) {
+ serveErrorPage(w, content404)
+}
+
+// Serve500 returns a 500 error response / HTML page to the http.ResponseWriter
+func Serve500(w http.ResponseWriter) {
+ serveErrorPage(w, content500)
+}
+
+// Serve502 returns a 502 error response / HTML page to the http.ResponseWriter
+func Serve502(w http.ResponseWriter) {
+ serveErrorPage(w, content502)
+}
diff --git a/internal/httperrors/httperrors_test.go b/internal/httperrors/httperrors_test.go
new file mode 100644
index 00000000..1a79d850
--- /dev/null
+++ b/internal/httperrors/httperrors_test.go
@@ -0,0 +1,105 @@
+package httperrors
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// creates a new implementation of http.ResponseWriter that allows the
+// casting of values in order to aid testing efforts.
+type testResponseWriter struct {
+ status int
+ content string
+ http.ResponseWriter
+}
+
+func newTestResponseWriter(w http.ResponseWriter) *testResponseWriter {
+ return &testResponseWriter{0, "", w}
+}
+
+func (w *testResponseWriter) Status() int {
+ return w.status
+}
+
+func (w *testResponseWriter) Content() string {
+ return w.content
+}
+
+func (w *testResponseWriter) Header() http.Header {
+ return w.ResponseWriter.Header()
+}
+
+func (w *testResponseWriter) Write(data []byte) (int, error) {
+ w.content = string(data)
+ return w.ResponseWriter.Write(data)
+}
+
+func (w *testResponseWriter) WriteHeader(statusCode int) {
+ w.status = statusCode
+ w.ResponseWriter.WriteHeader(statusCode)
+}
+
+var (
+ testingContent = content{
+ http.StatusNotFound,
+ "Title",
+ "533",
+ "Header test",
+ "subheader text",
+ }
+)
+
+func TestGenerateemailHTML(t *testing.T) {
+ actual := generateErrorHTML(testingContent)
+ assert.Contains(t, actual, testingContent.title)
+ assert.Contains(t, actual, testingContent.statusString)
+ assert.Contains(t, actual, testingContent.header)
+ assert.Contains(t, actual, testingContent.subHeader)
+}
+
+func TestServeErrorPage(t *testing.T) {
+ w := newTestResponseWriter(httptest.NewRecorder())
+ serveErrorPage(w, testingContent)
+ assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8")
+ assert.Equal(t, w.Header().Get("X-Content-Type-Options"), "nosniff")
+ assert.Equal(t, w.Status(), testingContent.status)
+}
+
+func TestServe404(t *testing.T) {
+ w := newTestResponseWriter(httptest.NewRecorder())
+ Serve404(w)
+ assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8")
+ assert.Equal(t, w.Header().Get("X-Content-Type-Options"), "nosniff")
+ assert.Equal(t, w.Status(), content404.status)
+ assert.Contains(t, w.Content(), content404.title)
+ assert.Contains(t, w.Content(), content404.statusString)
+ assert.Contains(t, w.Content(), content404.header)
+ assert.Contains(t, w.Content(), content404.subHeader)
+}
+
+func TestServe500(t *testing.T) {
+ w := newTestResponseWriter(httptest.NewRecorder())
+ Serve500(w)
+ assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8")
+ assert.Equal(t, w.Header().Get("X-Content-Type-Options"), "nosniff")
+ assert.Equal(t, w.Status(), content500.status)
+ assert.Contains(t, w.Content(), content500.title)
+ assert.Contains(t, w.Content(), content500.statusString)
+ assert.Contains(t, w.Content(), content500.header)
+ assert.Contains(t, w.Content(), content500.subHeader)
+}
+
+func TestServe502(t *testing.T) {
+ w := newTestResponseWriter(httptest.NewRecorder())
+ Serve502(w)
+ assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8")
+ assert.Equal(t, w.Header().Get("X-Content-Type-Options"), "nosniff")
+ assert.Equal(t, w.Status(), content502.status)
+ assert.Contains(t, w.Content(), content502.title)
+ assert.Contains(t, w.Content(), content502.statusString)
+ assert.Contains(t, w.Content(), content502.header)
+ assert.Contains(t, w.Content(), content502.subHeader)
+}