mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
wip: 🔕 temporary commit
This commit is contained in:
parent
669aaf6a1e
commit
1b94bc367c
@ -5,5 +5,4 @@
|
|||||||
!/cmd
|
!/cmd
|
||||||
!/internal
|
!/internal
|
||||||
!/templates
|
!/templates
|
||||||
!/error-pages.yml
|
|
||||||
!/go.*
|
!/go.*
|
||||||
|
19
Dockerfile
19
Dockerfile
@ -48,22 +48,19 @@ WORKDIR /tmp/rootfs
|
|||||||
|
|
||||||
# prepare rootfs for runtime
|
# prepare rootfs for runtime
|
||||||
RUN --mount=type=bind,source=.,target=/src set -x \
|
RUN --mount=type=bind,source=.,target=/src set -x \
|
||||||
&& mkdir -p ./etc ./bin ./opt/html \
|
&& mkdir -p ./etc ./bin \
|
||||||
&& echo 'appuser:x:10001:10001::/nonexistent:/sbin/nologin' > ./etc/passwd \
|
&& echo 'appuser:x:10001:10001::/nonexistent:/sbin/nologin' > ./etc/passwd \
|
||||||
&& echo 'appuser:x:10001:' > ./etc/group \
|
&& echo 'appuser:x:10001:' > ./etc/group
|
||||||
&& cp -rv /src/templates ./opt/templates \
|
|
||||||
&& rm -v ./opt/templates/*.md \
|
|
||||||
&& cp -rv /src/error-pages.yml ./opt/error-pages.yml
|
|
||||||
|
|
||||||
# take the binary from the compile stage
|
# take the binary from the compile stage
|
||||||
COPY --from=compile /tmp/error-pages ./bin/error-pages
|
COPY --from=compile /tmp/error-pages ./bin/error-pages
|
||||||
|
|
||||||
WORKDIR /tmp/rootfs/opt
|
WORKDIR /tmp/rootfs/opt
|
||||||
|
|
||||||
# generate static error pages (for usage inside another docker images, for example)
|
## generate static error pages (for usage inside another docker images, for example)
|
||||||
RUN set -x \
|
#RUN set -x \
|
||||||
&& ./../bin/error-pages --verbose build --config-file ./error-pages.yml --index ./html \
|
# && ./../bin/error-pages --verbose build --config-file ./error-pages.yml --index ./html \
|
||||||
&& ls -l ./html
|
# && ls -l ./html
|
||||||
|
|
||||||
# -✂- and this is the final stage (an empty filesystem is used) -------------------------------------------------------
|
# -✂- and this is the final stage (an empty filesystem is used) -------------------------------------------------------
|
||||||
FROM scratch AS runtime
|
FROM scratch AS runtime
|
||||||
@ -98,9 +95,9 @@ WORKDIR /opt
|
|||||||
|
|
||||||
# docs: https://docs.docker.com/reference/dockerfile/#healthcheck
|
# docs: https://docs.docker.com/reference/dockerfile/#healthcheck
|
||||||
HEALTHCHECK --interval=10s --start-interval=1s --start-period=5s --timeout=2s CMD [\
|
HEALTHCHECK --interval=10s --start-interval=1s --start-period=5s --timeout=2s CMD [\
|
||||||
"/bin/error-pages", "--log-json", "healthcheck" \
|
"/bin/error-pages", "--log-format", "json", "healthcheck" \
|
||||||
]
|
]
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/error-pages"]
|
ENTRYPOINT ["/bin/error-pages"]
|
||||||
|
|
||||||
CMD ["--log-json", "serve"]
|
CMD ["--log-format", "json", "serve"]
|
||||||
|
@ -10,7 +10,7 @@ services:
|
|||||||
web:
|
web:
|
||||||
build: {target: runtime}
|
build: {target: runtime}
|
||||||
ports: ['8080:8080/tcp'] # open http://127.0.0.1:8080
|
ports: ['8080:8080/tcp'] # open http://127.0.0.1:8080
|
||||||
command: serve --show-details --proxy-headers=X-Foo,Bar,Baz_blah --catch-all
|
command: --log-level debug serve --show-details --proxy-headers=X-Foo,Bar,Baz_blah
|
||||||
develop: # available since docker compose v2.22, https://docs.docker.com/compose/file-watch/
|
develop: # available since docker compose v2.22, https://docs.docker.com/compose/file-watch/
|
||||||
watch: [{action: rebuild, path: .}]
|
watch: [{action: rebuild, path: .}]
|
||||||
security_opt: [no-new-privileges:true]
|
security_opt: [no-new-privileges:true]
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
_ "github.com/urfave/cli-docs/v3" // required for `go generate` to work
|
_ "github.com/urfave/cli-docs/v3" // required for `go generate` to work
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
|
|
||||||
"gh.tarampamp.am/error-pages/internal-old/env"
|
|
||||||
"gh.tarampamp.am/error-pages/internal/appmeta"
|
"gh.tarampamp.am/error-pages/internal/appmeta"
|
||||||
"gh.tarampamp.am/error-pages/internal/cli/healthcheck"
|
"gh.tarampamp.am/error-pages/internal/cli/healthcheck"
|
||||||
"gh.tarampamp.am/error-pages/internal/cli/serve"
|
"gh.tarampamp.am/error-pages/internal/cli/serve"
|
||||||
@ -25,7 +24,7 @@ func NewApp(appName string) *cli.Command { //nolint:funlen
|
|||||||
Name: "log-level",
|
Name: "log-level",
|
||||||
Value: logger.InfoLevel.String(),
|
Value: logger.InfoLevel.String(),
|
||||||
Usage: "logging level (" + strings.Join(logger.LevelStrings(), "/") + ")",
|
Usage: "logging level (" + strings.Join(logger.LevelStrings(), "/") + ")",
|
||||||
Sources: cli.EnvVars(env.LogLevel.String()),
|
Sources: cli.EnvVars("LOG_LEVEL"),
|
||||||
OnlyOnce: true,
|
OnlyOnce: true,
|
||||||
Config: cli.StringConfig{TrimSpace: true},
|
Config: cli.StringConfig{TrimSpace: true},
|
||||||
Validator: func(s string) error {
|
Validator: func(s string) error {
|
||||||
@ -41,7 +40,7 @@ func NewApp(appName string) *cli.Command { //nolint:funlen
|
|||||||
Name: "log-format",
|
Name: "log-format",
|
||||||
Value: logger.ConsoleFormat.String(),
|
Value: logger.ConsoleFormat.String(),
|
||||||
Usage: "logging format (" + strings.Join(logger.FormatStrings(), "/") + ")",
|
Usage: "logging format (" + strings.Join(logger.FormatStrings(), "/") + ")",
|
||||||
Sources: cli.EnvVars(env.LogFormat.String()),
|
Sources: cli.EnvVars("LOG_FORMAT"),
|
||||||
OnlyOnce: true,
|
OnlyOnce: true,
|
||||||
Config: cli.StringConfig{TrimSpace: true},
|
Config: cli.StringConfig{TrimSpace: true},
|
||||||
Validator: func(s string) error {
|
Validator: func(s string) error {
|
||||||
|
@ -4,13 +4,22 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// New creates a new handler that always returns "OK" with status code 200.
|
// New creates a new handler that returns "OK" for GET and HEAD requests.
|
||||||
func New() http.Handler {
|
func New() http.Handler {
|
||||||
var body = []byte("OK")
|
var body = []byte("OK\n")
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, _ = w.Write(body)
|
_, _ = w.Write(body)
|
||||||
|
|
||||||
|
case http.MethodHead:
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
default:
|
||||||
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -13,14 +13,51 @@ import (
|
|||||||
func TestServeHTTP(t *testing.T) {
|
func TestServeHTTP(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
var handler = live.New()
|
||||||
|
|
||||||
|
t.Run("get", func(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
req = httptest.NewRequest(http.MethodGet, "http://testing", http.NoBody)
|
req = httptest.NewRequest(http.MethodGet, "http://testing", http.NoBody)
|
||||||
rr = httptest.NewRecorder()
|
rr = httptest.NewRecorder()
|
||||||
)
|
)
|
||||||
|
|
||||||
live.New().ServeHTTP(rr, req)
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
assert.Equal(t, rr.Header().Get("Content-Type"), "text/plain; charset=utf-8")
|
assert.Equal(t, rr.Header().Get("Content-Type"), "text/plain; charset=utf-8")
|
||||||
assert.Equal(t, rr.Code, http.StatusOK)
|
assert.Equal(t, rr.Code, http.StatusOK)
|
||||||
assert.Equal(t, rr.Body.String(), "OK")
|
assert.Equal(t, "OK\n", rr.Body.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("head", func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
req = httptest.NewRequest(http.MethodHead, "http://testing", http.NoBody)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, rr.Code, http.StatusOK)
|
||||||
|
assert.Empty(t, rr.Header().Get("Content-Type"))
|
||||||
|
assert.Empty(t, rr.Body.Bytes())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("method not allowed", func(t *testing.T) {
|
||||||
|
for _, method := range []string{
|
||||||
|
http.MethodDelete,
|
||||||
|
http.MethodPatch,
|
||||||
|
http.MethodPost,
|
||||||
|
http.MethodPut,
|
||||||
|
} {
|
||||||
|
var (
|
||||||
|
req = httptest.NewRequest(method, "http://testing", http.NoBody)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, rr.Header().Get("Content-Type"), "text/plain; charset=utf-8")
|
||||||
|
assert.Equal(t, rr.Code, http.StatusMethodNotAllowed)
|
||||||
|
assert.Equal(t, "Method Not Allowed\n", rr.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,17 @@ func New(ver string) http.Handler {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, _ = w.Write(body)
|
_, _ = w.Write(body)
|
||||||
|
|
||||||
|
case http.MethodHead:
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
default:
|
||||||
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -13,13 +13,51 @@ import (
|
|||||||
func TestServeHTTP(t *testing.T) {
|
func TestServeHTTP(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
var handler = version.New("\t\n foo@bar ")
|
||||||
|
|
||||||
|
t.Run("get", func(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
req = httptest.NewRequest(http.MethodGet, "http://testing", http.NoBody)
|
req = httptest.NewRequest(http.MethodGet, "http://testing", http.NoBody)
|
||||||
rr = httptest.NewRecorder()
|
rr = httptest.NewRecorder()
|
||||||
)
|
)
|
||||||
|
|
||||||
version.New("\t\n foo@bar ").ServeHTTP(rr, req)
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, rr.Header().Get("Content-Type"), "application/json; charset=utf-8")
|
||||||
assert.Equal(t, rr.Code, http.StatusOK)
|
assert.Equal(t, rr.Code, http.StatusOK)
|
||||||
assert.Equal(t, rr.Body.String(), `{"version":"foo@bar"}`)
|
assert.Equal(t, rr.Body.String(), `{"version":"foo@bar"}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("head", func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
req = httptest.NewRequest(http.MethodHead, "http://testing", http.NoBody)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, rr.Code, http.StatusOK)
|
||||||
|
assert.Empty(t, rr.Header().Get("Content-Type"))
|
||||||
|
assert.Empty(t, rr.Body.Bytes())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("method not allowed", func(t *testing.T) {
|
||||||
|
for _, method := range []string{
|
||||||
|
http.MethodDelete,
|
||||||
|
http.MethodPatch,
|
||||||
|
http.MethodPost,
|
||||||
|
http.MethodPut,
|
||||||
|
} {
|
||||||
|
var (
|
||||||
|
req = httptest.NewRequest(method, "http://testing", http.NoBody)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, rr.Header().Get("Content-Type"), "text/plain; charset=utf-8")
|
||||||
|
assert.Equal(t, rr.Code, http.StatusMethodNotAllowed)
|
||||||
|
assert.Equal(t, "Method Not Allowed\n", rr.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -22,7 +23,6 @@ import (
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
log *zap.Logger
|
log *zap.Logger
|
||||||
server *http.Server
|
server *http.Server
|
||||||
mux *http.ServeMux
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new HTTP server.
|
// NewServer creates a new HTTP server.
|
||||||
@ -33,45 +33,65 @@ func NewServer(baseCtx context.Context, log *zap.Logger) Server {
|
|||||||
maxHeaderBytes = (1 << 20) * 5 //nolint:mnd // 5 MB
|
maxHeaderBytes = (1 << 20) * 5 //nolint:mnd // 5 MB
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
return Server{
|
||||||
mux = http.NewServeMux()
|
log: log,
|
||||||
srv = &http.Server{
|
server: &http.Server{
|
||||||
Handler: mux,
|
|
||||||
ReadTimeout: readTimeout,
|
ReadTimeout: readTimeout,
|
||||||
WriteTimeout: writeTimeout,
|
WriteTimeout: writeTimeout,
|
||||||
ReadHeaderTimeout: readTimeout,
|
ReadHeaderTimeout: readTimeout,
|
||||||
MaxHeaderBytes: maxHeaderBytes,
|
MaxHeaderBytes: maxHeaderBytes,
|
||||||
ErrorLog: zap.NewStdLog(log),
|
ErrorLog: zap.NewStdLog(log),
|
||||||
BaseContext: func(net.Listener) context.Context { return baseCtx },
|
BaseContext: func(net.Listener) context.Context { return baseCtx },
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
return Server{log: log, server: srv, mux: mux}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register server handlers, middlewares, etc.
|
// Register server handlers, middlewares, etc.
|
||||||
func (s *Server) Register(cfg *config.Config) error {
|
func (s *Server) Register(cfg *config.Config) error {
|
||||||
// register middleware
|
var (
|
||||||
|
liveHandler = live.New()
|
||||||
|
versionHandler = version.New(appmeta.Version())
|
||||||
|
errorPagesHandler = error_page.New()
|
||||||
|
|
||||||
|
errorPageRegex = regexp.MustCompile(`^/(\d{3})(?:\.html|\.htm)?$`) // TODO: rewrite to function
|
||||||
|
)
|
||||||
|
|
||||||
|
s.server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var url, method = r.URL.Path, r.Method
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// live endpoints
|
||||||
|
case url == "/health/live" || url == "/health" || url == "/healthz" || url == "/live":
|
||||||
|
liveHandler.ServeHTTP(w, r)
|
||||||
|
// version endpoint
|
||||||
|
case url == "/version":
|
||||||
|
versionHandler.ServeHTTP(w, r)
|
||||||
|
// error pages endpoints:
|
||||||
|
// - /
|
||||||
|
// - /{code}.html
|
||||||
|
// - /{code}.htm
|
||||||
|
// - /{code}
|
||||||
|
case method == http.MethodGet && (url == "/" || errorPageRegex.MatchString(url)):
|
||||||
|
errorPagesHandler.ServeHTTP(w, r)
|
||||||
|
// wrong requests handling
|
||||||
|
default:
|
||||||
|
switch {
|
||||||
|
case method == http.MethodHead:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
case method == http.MethodGet:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// apply middleware
|
||||||
s.server.Handler = logreq.New(s.log, func(r *http.Request) bool {
|
s.server.Handler = logreq.New(s.log, func(r *http.Request) bool {
|
||||||
// skip logging healthcheck requests
|
// skip logging healthcheck requests
|
||||||
return strings.Contains(strings.ToLower(r.UserAgent()), "healthcheck")
|
return strings.Contains(strings.ToLower(r.UserAgent()), "healthcheck")
|
||||||
})(s.server.Handler)
|
})(s.server.Handler)
|
||||||
|
|
||||||
{ // register handlers (https://go.dev/blog/routing-enhancements)
|
|
||||||
var errorPageHandler = error_page.New()
|
|
||||||
|
|
||||||
s.mux.Handle("/", errorPageHandler)
|
|
||||||
s.mux.Handle("/{any}", errorPageHandler)
|
|
||||||
|
|
||||||
var liveHandler = live.New()
|
|
||||||
|
|
||||||
s.mux.Handle("GET /health/live", liveHandler)
|
|
||||||
s.mux.Handle("GET /healthz", liveHandler)
|
|
||||||
s.mux.Handle("GET /live", liveHandler)
|
|
||||||
|
|
||||||
s.mux.Handle("GET /version", version.New(appmeta.Version()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
221
internal/http/server_test.go
Normal file
221
internal/http/server_test.go
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
package http_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"gh.tarampamp.am/error-pages/internal/config"
|
||||||
|
appHttp "gh.tarampamp.am/error-pages/internal/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRouting(t *testing.T) {
|
||||||
|
var (
|
||||||
|
srv = appHttp.NewServer(context.Background(), zap.NewNop())
|
||||||
|
cfg = config.New()
|
||||||
|
)
|
||||||
|
|
||||||
|
require.NoError(t, srv.Register(&cfg))
|
||||||
|
|
||||||
|
var baseUrl, stopServer = startServer(t, &srv)
|
||||||
|
|
||||||
|
defer stopServer()
|
||||||
|
|
||||||
|
t.Run("health", func(t *testing.T) {
|
||||||
|
var routes = []string{"/health/live", "/health", "/healthz", "/live"}
|
||||||
|
|
||||||
|
t.Run("success (get)", func(t *testing.T) {
|
||||||
|
for _, route := range routes {
|
||||||
|
status, body, headers := sendRequest(t, http.MethodGet, baseUrl+route)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
assert.NotEmpty(t, body)
|
||||||
|
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("success (head)", func(t *testing.T) {
|
||||||
|
for _, route := range routes {
|
||||||
|
status, body, headers := sendRequest(t, http.MethodHead, baseUrl+route)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
assert.Empty(t, body)
|
||||||
|
assert.Empty(t, headers.Get("Content-Type"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("method not allowed", func(t *testing.T) {
|
||||||
|
for _, route := range routes {
|
||||||
|
var url = baseUrl + route
|
||||||
|
|
||||||
|
for _, method := range []string{http.MethodDelete, http.MethodPatch, http.MethodPost, http.MethodPut} {
|
||||||
|
status, body, headers := sendRequest(t, method, url)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusMethodNotAllowed, status)
|
||||||
|
assert.NotEmpty(t, body)
|
||||||
|
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("version", func(t *testing.T) {
|
||||||
|
var url = baseUrl + "/version"
|
||||||
|
|
||||||
|
t.Run("success (get)", func(t *testing.T) {
|
||||||
|
status, body, headers := sendRequest(t, http.MethodGet, url)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
assert.NotEmpty(t, body)
|
||||||
|
assert.Contains(t, headers.Get("Content-Type"), "application/json")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("success (head)", func(t *testing.T) {
|
||||||
|
status, body, headers := sendRequest(t, http.MethodHead, url)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, status)
|
||||||
|
assert.Empty(t, body)
|
||||||
|
assert.Empty(t, headers.Get("Content-Type"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("method not allowed", func(t *testing.T) {
|
||||||
|
for _, method := range []string{http.MethodDelete, http.MethodPatch, http.MethodPost, http.MethodPut} {
|
||||||
|
status, body, headers := sendRequest(t, method, url)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusMethodNotAllowed, status)
|
||||||
|
assert.NotEmpty(t, body)
|
||||||
|
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error page", func(t *testing.T) {
|
||||||
|
t.Skip("not implemented")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("errors handling", func(t *testing.T) {
|
||||||
|
var missingRoutes = []string{"/not-found", "/not-found/", "/not-found.html"}
|
||||||
|
|
||||||
|
t.Run("not found (get)", func(t *testing.T) {
|
||||||
|
for _, path := range missingRoutes {
|
||||||
|
status, body, headers := sendRequest(t, http.MethodGet, baseUrl+path)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, status)
|
||||||
|
assert.NotEmpty(t, body)
|
||||||
|
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not found (head)", func(t *testing.T) {
|
||||||
|
for _, path := range missingRoutes {
|
||||||
|
status, body, headers := sendRequest(t, http.MethodHead, baseUrl+path)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, status)
|
||||||
|
assert.Empty(t, body)
|
||||||
|
assert.Empty(t, headers.Get("Content-Type"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("methods not allowed", func(t *testing.T) {
|
||||||
|
for _, path := range missingRoutes {
|
||||||
|
for _, method := range []string{http.MethodDelete, http.MethodPatch, http.MethodPost, http.MethodPut} {
|
||||||
|
status, body, headers := sendRequest(t, method, baseUrl+path)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusMethodNotAllowed, status)
|
||||||
|
assert.NotEmpty(t, body)
|
||||||
|
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendRequest is a helper function to send an HTTP request and return its status code, body, and headers.
|
||||||
|
func sendRequest(t *testing.T, method, url string, headers ...map[string]string) (
|
||||||
|
status int,
|
||||||
|
body []byte,
|
||||||
|
_ http.Header,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
req, reqErr := http.NewRequest(method, url, nil)
|
||||||
|
|
||||||
|
require.NoError(t, reqErr)
|
||||||
|
|
||||||
|
if len(headers) > 0 {
|
||||||
|
for key, value := range headers[0] {
|
||||||
|
req.Header.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
body, _ = io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
require.NoError(t, resp.Body.Close())
|
||||||
|
|
||||||
|
return resp.StatusCode, body, resp.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// startServer is a helper function to start an HTTP server and return its base URL and a stop function.
|
||||||
|
func startServer(t *testing.T, srv *appHttp.Server) (_ string, stop func()) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var (
|
||||||
|
port = getFreeTcpPort(t)
|
||||||
|
hostPort = fmt.Sprintf("%s:%d", "127.0.0.1", port)
|
||||||
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := srv.Start("127.0.0.1", port); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait until the server starts
|
||||||
|
for {
|
||||||
|
if conn, err := net.DialTimeout("tcp", hostPort, time.Second); err == nil {
|
||||||
|
require.NoError(t, conn.Close())
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
<-time.After(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("http://%s", hostPort), func() { assert.NoError(t, srv.Stop(10*time.Millisecond)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFreeTcpPort is a helper function to get a free TCP port number.
|
||||||
|
func getFreeTcpPort(t *testing.T) uint16 {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
l, lErr := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, lErr)
|
||||||
|
|
||||||
|
port := l.Addr().(*net.TCPAddr).Port
|
||||||
|
require.NoError(t, l.Close())
|
||||||
|
|
||||||
|
// make sure port is closed
|
||||||
|
for {
|
||||||
|
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, conn.Close())
|
||||||
|
<-time.After(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint16(port)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user