(Feat): Initial Commit
This commit is contained in:
13
internal/api/router/echo_logger.go
Normal file
13
internal/api/router/echo_logger.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package router
|
||||
|
||||
import "github.com/rs/zerolog"
|
||||
|
||||
type echoLogger struct {
|
||||
level zerolog.Level
|
||||
log zerolog.Logger
|
||||
}
|
||||
|
||||
func (l *echoLogger) Write(p []byte) (int, error) {
|
||||
l.log.WithLevel(l.level).Msgf("%s", p)
|
||||
return len(p), nil
|
||||
}
|
||||
27
internal/api/router/echo_renderer.go
Normal file
27
internal/api/router/echo_renderer.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/api/router/templates"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type echoRenderer struct {
|
||||
templates map[templates.ViewTemplate]*template.Template
|
||||
}
|
||||
|
||||
func (t *echoRenderer) Render(writer io.Writer, name string, data interface{}, _ echo.Context) error {
|
||||
tmplHTML, ok := t.templates[templates.ViewTemplate(name)]
|
||||
if !ok {
|
||||
return fmt.Errorf("template not found: %s", name)
|
||||
}
|
||||
|
||||
if err := tmplHTML.Execute(writer, data); err != nil {
|
||||
return fmt.Errorf("failed to execute template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
150
internal/api/router/handlers.go
Normal file
150
internal/api/router/handlers.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/api/httperrors"
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
"allaboutapps.dev/aw/go-starter/internal/types"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/timewasted/go-accept-headers"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultHTTPErrorHandlerConfig = HTTPErrorHandlerConfig{
|
||||
HideInternalServerErrorDetails: true,
|
||||
}
|
||||
)
|
||||
|
||||
type HTTPErrorHandlerConfig struct {
|
||||
HideInternalServerErrorDetails bool
|
||||
}
|
||||
|
||||
func HTTPErrorHandler() echo.HTTPErrorHandler {
|
||||
return HTTPErrorHandlerWithConfig(DefaultHTTPErrorHandlerConfig)
|
||||
}
|
||||
|
||||
func HTTPErrorHandlerWithConfig(config HTTPErrorHandlerConfig) echo.HTTPErrorHandler {
|
||||
return func(err error, c echo.Context) {
|
||||
var code int64
|
||||
var resultErr error
|
||||
|
||||
var httpError *httperrors.HTTPError
|
||||
var httpValidationError *httperrors.HTTPValidationError
|
||||
var echoHTTPError *echo.HTTPError
|
||||
|
||||
switch {
|
||||
case errors.As(err, &httpError):
|
||||
code = *httpError.Code
|
||||
resultErr = httpError
|
||||
|
||||
if code == http.StatusInternalServerError && config.HideInternalServerErrorDetails {
|
||||
if httpError.Internal == nil {
|
||||
//nolint:errorlint
|
||||
httpError.Internal = fmt.Errorf("internal error: %s", httpError)
|
||||
}
|
||||
|
||||
httpError.Title = swag.String(http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
case errors.As(err, &httpValidationError):
|
||||
code = *httpValidationError.Code
|
||||
resultErr = httpValidationError
|
||||
|
||||
if code == http.StatusInternalServerError && config.HideInternalServerErrorDetails {
|
||||
if httpValidationError.Internal == nil {
|
||||
//nolint:errorlint
|
||||
httpValidationError.Internal = fmt.Errorf("internal error: %s", httpValidationError)
|
||||
}
|
||||
|
||||
httpValidationError.Title = swag.String(http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
case errors.As(err, &echoHTTPError):
|
||||
code = int64(echoHTTPError.Code)
|
||||
|
||||
//nolint:nestif
|
||||
if code == http.StatusInternalServerError && config.HideInternalServerErrorDetails {
|
||||
if echoHTTPError.Internal == nil {
|
||||
//nolint:errorlint
|
||||
echoHTTPError.Internal = fmt.Errorf("internal error: %s", echoHTTPError)
|
||||
}
|
||||
|
||||
resultErr = &httperrors.HTTPError{
|
||||
PublicHTTPError: types.PublicHTTPError{
|
||||
Code: swag.Int64(int64(echoHTTPError.Code)),
|
||||
Title: swag.String(http.StatusText(http.StatusInternalServerError)),
|
||||
Type: types.PublicHTTPErrorTypeGeneric.Pointer(),
|
||||
},
|
||||
Internal: echoHTTPError.Internal,
|
||||
}
|
||||
} else {
|
||||
msg, ok := echoHTTPError.Message.(string)
|
||||
if !ok {
|
||||
if m, errr := json.Marshal(msg); err == nil {
|
||||
msg = string(m)
|
||||
} else {
|
||||
msg = fmt.Sprintf("failed to marshal HTTP error message: %v", errr)
|
||||
}
|
||||
}
|
||||
|
||||
resultErr = &httperrors.HTTPError{
|
||||
PublicHTTPError: types.PublicHTTPError{
|
||||
Code: swag.Int64(int64(echoHTTPError.Code)),
|
||||
Title: &msg,
|
||||
Type: types.PublicHTTPErrorTypeGeneric.Pointer(),
|
||||
},
|
||||
Internal: echoHTTPError.Internal,
|
||||
}
|
||||
}
|
||||
default:
|
||||
code = http.StatusInternalServerError
|
||||
if config.HideInternalServerErrorDetails {
|
||||
resultErr = &httperrors.HTTPError{
|
||||
PublicHTTPError: types.PublicHTTPError{
|
||||
Code: swag.Int64(int64(http.StatusInternalServerError)),
|
||||
Title: swag.String(http.StatusText(http.StatusInternalServerError)),
|
||||
Type: types.PublicHTTPErrorTypeGeneric.Pointer(),
|
||||
},
|
||||
|
||||
Internal: err,
|
||||
}
|
||||
} else {
|
||||
resultErr = &httperrors.HTTPError{
|
||||
PublicHTTPError: types.PublicHTTPError{
|
||||
Code: swag.Int64(int64(http.StatusInternalServerError)),
|
||||
Title: swag.String(err.Error()),
|
||||
Type: types.PublicHTTPErrorTypeGeneric.Pointer(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !c.Response().Committed {
|
||||
if c.Request().Method == http.MethodHead {
|
||||
err = c.NoContent(int(code))
|
||||
} else {
|
||||
err = c.JSON(int(code), resultErr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
util.LogFromEchoContext(c).Warn().Err(err).AnErr("http_err", err).Msg("Failed to handle HTTP error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NotFoundHandler(config config.Server) func(c echo.Context) error {
|
||||
return func(c echo.Context) error {
|
||||
accepted := accept.Parse(c.Request().Header.Get(echo.HeaderAccept))
|
||||
|
||||
if accepted.Accepts(echo.MIMETextHTML) {
|
||||
return c.HTML(http.StatusNotFound, fmt.Sprintf(`<html><body><h1>Page Not Found</h1><p>The page you are looking for does not exist. Did you mean to visit <a href="%s">%s</a>?</p></body></html>`, config.Frontend.BaseURL, config.Frontend.BaseURL))
|
||||
}
|
||||
|
||||
return echo.ErrNotFound
|
||||
}
|
||||
}
|
||||
252
internal/api/router/router.go
Normal file
252
internal/api/router/router.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||
"allaboutapps.dev/aw/go-starter/internal/api/handlers"
|
||||
"allaboutapps.dev/aw/go-starter/internal/api/handlers/constants"
|
||||
"allaboutapps.dev/aw/go-starter/internal/api/middleware"
|
||||
"allaboutapps.dev/aw/go-starter/internal/api/router/templates"
|
||||
"github.com/labstack/echo-contrib/echoprometheus"
|
||||
"github.com/labstack/echo/v4"
|
||||
echoMiddleware "github.com/labstack/echo/v4/middleware"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
// #nosec G108 - pprof handlers (conditionally made available via http.DefaultServeMux)
|
||||
"net/http/pprof"
|
||||
)
|
||||
|
||||
func Init(s *api.Server) error {
|
||||
s.Echo = echo.New()
|
||||
|
||||
viewsRenderer := &echoRenderer{
|
||||
templates: map[templates.ViewTemplate]*template.Template{},
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(s.Config.Echo.WebTemplatesViewsBaseDirAbs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read views templates dir: %w", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
templateName := file.Name()
|
||||
t, err := template.New(templateName).ParseGlob(filepath.Join(s.Config.Echo.WebTemplatesViewsBaseDirAbs, templateName))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse template file: %w", err)
|
||||
}
|
||||
|
||||
viewsRenderer.templates[templates.ViewTemplate(templateName)] = t
|
||||
}
|
||||
|
||||
s.Echo.Renderer = viewsRenderer
|
||||
|
||||
s.Echo.Debug = s.Config.Echo.Debug
|
||||
s.Echo.HideBanner = true
|
||||
s.Echo.Logger.SetOutput(&echoLogger{level: s.Config.Logger.RequestLevel, log: log.With().Str("component", "echo").Logger()})
|
||||
echo.NotFoundHandler = NotFoundHandler(s.Config)
|
||||
|
||||
s.Echo.HTTPErrorHandler = HTTPErrorHandlerWithConfig(HTTPErrorHandlerConfig{
|
||||
HideInternalServerErrorDetails: s.Config.Echo.HideInternalServerErrorDetails,
|
||||
})
|
||||
|
||||
// ---
|
||||
// General middleware
|
||||
if s.Config.Management.EnableMetrics {
|
||||
s.Echo.Use(echoprometheus.NewMiddleware(""))
|
||||
err := s.Metrics.RegisterMetrics(context.Background())
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to register metrics")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Warn().Msg("Disabling metrics middleware due to environment config")
|
||||
}
|
||||
|
||||
if s.Config.Echo.EnableTrailingSlashMiddleware {
|
||||
s.Echo.Pre(echoMiddleware.RemoveTrailingSlash())
|
||||
} else {
|
||||
log.Warn().Msg("Disabling trailing slash middleware due to environment config")
|
||||
}
|
||||
|
||||
if s.Config.Echo.EnableRecoverMiddleware {
|
||||
s.Echo.Use(echoMiddleware.RecoverWithConfig(echoMiddleware.RecoverConfig{
|
||||
LogErrorFunc: middleware.LogErrorFuncWithRequestInfo,
|
||||
}))
|
||||
} else {
|
||||
log.Warn().Msg("Disabling recover middleware due to environment config")
|
||||
}
|
||||
|
||||
if s.Config.Echo.EnableSecureMiddleware {
|
||||
s.Echo.Use(echoMiddleware.SecureWithConfig(echoMiddleware.SecureConfig{
|
||||
Skipper: echoMiddleware.DefaultSecureConfig.Skipper,
|
||||
XSSProtection: s.Config.Echo.SecureMiddleware.XSSProtection,
|
||||
ContentTypeNosniff: s.Config.Echo.SecureMiddleware.ContentTypeNosniff,
|
||||
XFrameOptions: s.Config.Echo.SecureMiddleware.XFrameOptions,
|
||||
HSTSMaxAge: s.Config.Echo.SecureMiddleware.HSTSMaxAge,
|
||||
HSTSExcludeSubdomains: s.Config.Echo.SecureMiddleware.HSTSExcludeSubdomains,
|
||||
ContentSecurityPolicy: s.Config.Echo.SecureMiddleware.ContentSecurityPolicy,
|
||||
CSPReportOnly: s.Config.Echo.SecureMiddleware.CSPReportOnly,
|
||||
HSTSPreloadEnabled: s.Config.Echo.SecureMiddleware.HSTSPreloadEnabled,
|
||||
ReferrerPolicy: s.Config.Echo.SecureMiddleware.ReferrerPolicy,
|
||||
}))
|
||||
} else {
|
||||
log.Warn().Msg("Disabling secure middleware due to environment config")
|
||||
}
|
||||
|
||||
if s.Config.Echo.EnableRequestIDMiddleware {
|
||||
s.Echo.Use(echoMiddleware.RequestID())
|
||||
} else {
|
||||
log.Warn().Msg("Disabling request ID middleware due to environment config")
|
||||
}
|
||||
|
||||
if s.Config.Echo.EnableLoggerMiddleware {
|
||||
s.Echo.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||
Level: s.Config.Logger.RequestLevel,
|
||||
LogRequestBody: s.Config.Logger.LogRequestBody,
|
||||
LogRequestHeader: s.Config.Logger.LogRequestHeader,
|
||||
LogRequestQuery: s.Config.Logger.LogRequestQuery,
|
||||
LogResponseBody: s.Config.Logger.LogResponseBody,
|
||||
LogResponseHeader: s.Config.Logger.LogResponseHeader,
|
||||
LogCaller: s.Config.Logger.LogCaller,
|
||||
RequestBodyLogSkipper: func(req *http.Request) bool {
|
||||
// We skip all body logging for auth endpoints as these might contain credentials
|
||||
if strings.HasPrefix(req.URL.Path, "/api/v1/auth") {
|
||||
return true
|
||||
}
|
||||
|
||||
return middleware.DefaultRequestBodyLogSkipper(req)
|
||||
},
|
||||
ResponseBodyLogSkipper: func(req *http.Request, res *echo.Response) bool {
|
||||
// We skip all body logging for auth endpoints as these might contain credentials
|
||||
if strings.HasPrefix(req.URL.Path, "/api/v1/auth") {
|
||||
return true
|
||||
}
|
||||
|
||||
return middleware.DefaultResponseBodyLogSkipper(req, res)
|
||||
},
|
||||
Skipper: func(c echo.Context) bool {
|
||||
// We skip logging of readiness and liveness endpoints
|
||||
switch c.Path() {
|
||||
case "/-/ready", "/-/healthy":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}))
|
||||
} else {
|
||||
log.Warn().Msg("Disabling logger middleware due to environment config")
|
||||
}
|
||||
|
||||
if s.Config.Echo.EnableCORSMiddleware {
|
||||
s.Echo.Use(echoMiddleware.CORS())
|
||||
} else {
|
||||
log.Warn().Msg("Disabling CORS middleware due to environment config")
|
||||
}
|
||||
|
||||
if s.Config.Echo.EnableCacheControlMiddleware {
|
||||
s.Echo.Use(middleware.CacheControl())
|
||||
} else {
|
||||
log.Warn().Msg("Disabling cache control middleware due to environment config")
|
||||
}
|
||||
|
||||
if s.Config.Pprof.Enable {
|
||||
pprofAuthMiddleware := middleware.Noop()
|
||||
|
||||
if s.Config.Pprof.EnableManagementKeyAuth {
|
||||
pprofAuthMiddleware = echoMiddleware.KeyAuthWithConfig(echoMiddleware.KeyAuthConfig{
|
||||
KeyLookup: "query:mgmt-secret",
|
||||
Validator: func(key string, _ echo.Context) (bool, error) {
|
||||
return key == s.Config.Management.Secret, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
s.Echo.GET("/debug/pprof", echo.WrapHandler(http.HandlerFunc(pprof.Index)), pprofAuthMiddleware)
|
||||
s.Echo.Any("/debug/pprof/*", echo.WrapHandler(http.DefaultServeMux), pprofAuthMiddleware)
|
||||
|
||||
log.Warn().Bool("EnableManagementKeyAuth", s.Config.Pprof.EnableManagementKeyAuth).Msg("Pprof http handlers are available at /debug/pprof")
|
||||
|
||||
if s.Config.Pprof.RuntimeBlockProfileRate != 0 {
|
||||
runtime.SetBlockProfileRate(s.Config.Pprof.RuntimeBlockProfileRate)
|
||||
log.Warn().Int("RuntimeBlockProfileRate", s.Config.Pprof.RuntimeBlockProfileRate).Msg("Pprof runtime.SetBlockProfileRate")
|
||||
}
|
||||
|
||||
if s.Config.Pprof.RuntimeMutexProfileFraction != 0 {
|
||||
runtime.SetMutexProfileFraction(s.Config.Pprof.RuntimeMutexProfileFraction)
|
||||
log.Warn().Int("RuntimeMutexProfileFraction", s.Config.Pprof.RuntimeMutexProfileFraction).Msg("Pprof runtime.SetMutexProfileFraction")
|
||||
}
|
||||
}
|
||||
|
||||
// Add your custom / additional middlewares here.
|
||||
// see https://echo.labstack.com/middleware
|
||||
|
||||
// ---
|
||||
// Initialize our general groups and set middleware to use above them
|
||||
s.Router = &api.Router{
|
||||
Routes: nil, // will be populated by handlers.AttachAllRoutes(s)
|
||||
|
||||
// Unsecured base group available at /**
|
||||
Root: s.Echo.Group(""),
|
||||
|
||||
// Management endpoints, uncacheable, secured by key auth (query param), available at /-/**
|
||||
Management: s.Echo.Group("/-", echoMiddleware.KeyAuthWithConfig(echoMiddleware.KeyAuthConfig{
|
||||
KeyLookup: "query:mgmt-secret",
|
||||
Validator: func(key string, _ echo.Context) (bool, error) {
|
||||
return key == s.Config.Management.Secret, nil
|
||||
},
|
||||
Skipper: func(c echo.Context) bool {
|
||||
//nolint:gocritic
|
||||
switch c.Path() {
|
||||
case "/-/ready":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}), middleware.NoCache()),
|
||||
|
||||
// OAuth2, unsecured or secured by bearer auth, available at /api/v1/auth/**
|
||||
APIV1Auth: s.Echo.Group("/api/v1/auth", middleware.AuthWithConfig(middleware.AuthConfig{
|
||||
S: s,
|
||||
Mode: middleware.AuthModeRequired,
|
||||
Skipper: func(c echo.Context) bool {
|
||||
switch c.Path() {
|
||||
case "/api/v1/auth/forgot-password",
|
||||
"/api/v1/auth/forgot-password/complete",
|
||||
"/api/v1/auth/login",
|
||||
"/api/v1/auth/refresh",
|
||||
"/api/v1/auth/register",
|
||||
fmt.Sprintf("/api/v1/auth/register/:%s", constants.RegistrationTokenParam):
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
})),
|
||||
WellKnown: s.Echo.Group("/.well-known"),
|
||||
|
||||
// Your other endpoints, typically secured by bearer auth, available at /api/v1/**
|
||||
APIV1Push: s.Echo.Group("/api/v1/push", middleware.Auth(s)),
|
||||
}
|
||||
|
||||
// ---
|
||||
// Finally attach our handlers
|
||||
handlers.AttachAllRoutes(s)
|
||||
|
||||
if s.Config.Management.EnableMetrics {
|
||||
log.Info().Msg("Metrics enabled and available under /metrics")
|
||||
s.Echo.GET("/metrics", echoprometheus.NewHandler())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
128
internal/api/router/router_test.go
Normal file
128
internal/api/router/router_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package router_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
"allaboutapps.dev/aw/go-starter/internal/metrics/users"
|
||||
"allaboutapps.dev/aw/go-starter/internal/models"
|
||||
"allaboutapps.dev/aw/go-starter/internal/test"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPprofEnabled(t *testing.T) {
|
||||
config := config.DefaultServiceConfigFromEnv()
|
||||
|
||||
// these are typically our default values, however we force set them here to ensure those are set while test execution.
|
||||
config.Pprof.Enable = true
|
||||
config.Pprof.EnableManagementKeyAuth = true
|
||||
|
||||
test.WithTestServerConfigurable(t, config, func(s *api.Server) {
|
||||
// heap (test any)
|
||||
res := test.PerformRequest(t, s, "GET", "/debug/pprof/heap?mgmt-secret="+s.Config.Management.Secret, nil, nil)
|
||||
require.Equal(t, 200, res.Result().StatusCode)
|
||||
|
||||
// index
|
||||
res = test.PerformRequest(t, s, "GET", "/debug/pprof?mgmt-secret="+s.Config.Management.Secret, nil, nil)
|
||||
require.Equal(t, 200, res.Result().StatusCode)
|
||||
|
||||
res = test.PerformRequest(t, s, "GET", "/debug/pprof/heap?mgmt-secret=wrongsecret", nil, nil)
|
||||
require.Equal(t, 401, res.Result().StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPprofEnabledNoAuth(t *testing.T) {
|
||||
config := config.DefaultServiceConfigFromEnv()
|
||||
|
||||
// these are typically our default values, however we force set them here to ensure those are set while test execution.
|
||||
config.Pprof.Enable = true
|
||||
config.Pprof.EnableManagementKeyAuth = false
|
||||
|
||||
test.WithTestServerConfigurable(t, config, func(s *api.Server) {
|
||||
res := test.PerformRequest(t, s, "GET", "/debug/pprof/heap?", nil, nil)
|
||||
require.Equal(t, 200, res.Result().StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPprofDisabled(t *testing.T) {
|
||||
config := config.DefaultServiceConfigFromEnv()
|
||||
config.Pprof.Enable = false
|
||||
|
||||
test.WithTestServerConfigurable(t, config, func(s *api.Server) {
|
||||
res := test.PerformRequest(t, s, "GET", "/debug/pprof/heap?mgmt-secret="+s.Config.Management.Secret, nil, nil)
|
||||
require.Equal(t, 404, res.Result().StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMiddlewaresDisabled(t *testing.T) {
|
||||
// disable all
|
||||
config := config.DefaultServiceConfigFromEnv()
|
||||
config.Echo.EnableCORSMiddleware = false
|
||||
config.Echo.EnableLoggerMiddleware = false
|
||||
config.Echo.EnableRecoverMiddleware = false
|
||||
config.Echo.EnableRequestIDMiddleware = false
|
||||
config.Echo.EnableSecureMiddleware = false
|
||||
config.Echo.EnableTrailingSlashMiddleware = false
|
||||
|
||||
test.WithTestServerConfigurable(t, config, func(s *api.Server) {
|
||||
res := test.PerformRequest(t, s, "GET", "/-/ready", nil, nil)
|
||||
require.Equal(t, 200, res.Result().StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMetricsEnabled(t *testing.T) {
|
||||
config := config.DefaultServiceConfigFromEnv()
|
||||
config.Management.EnableMetrics = true
|
||||
|
||||
test.WithTestServerConfigurable(t, config, func(s *api.Server) {
|
||||
res := test.PerformRequest(t, s, "GET", "/metrics", nil, nil)
|
||||
require.Equal(t, http.StatusOK, res.Result().StatusCode)
|
||||
|
||||
result := res.Body.String()
|
||||
|
||||
// expect custom metric for the total user count
|
||||
expectedTotalUserCount, err := models.Users().Count(t.Context(), s.DB)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, result, fmt.Sprintf("%s %d", users.MetricNameTotalUsers, expectedTotalUserCount))
|
||||
|
||||
// expect sqlstats metrics
|
||||
assert.Contains(t, result, "go_sql_stats_connections")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMetricsDisabled(t *testing.T) {
|
||||
test.WithTestServer(t, func(s *api.Server) {
|
||||
res := test.PerformRequest(t, s, "GET", "/metrics", nil, nil)
|
||||
require.Equal(t, http.StatusNotFound, res.Result().StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNotFound(t *testing.T) {
|
||||
test.WithTestServer(t, func(s *api.Server) {
|
||||
t.Run("AcceptApplicationJSON", func(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set(echo.HeaderAccept, echo.MIMEApplicationJSON)
|
||||
|
||||
res := test.PerformRequest(t, s, "GET", "/api/v1/unknown-path", nil, headers)
|
||||
require.Equal(t, http.StatusNotFound, res.Result().StatusCode)
|
||||
|
||||
test.Snapshoter.Save(t, res.Body.String())
|
||||
})
|
||||
|
||||
t.Run("AcceptTextHTML", func(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
headers.Set(echo.HeaderAccept, "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
|
||||
|
||||
res := test.PerformRequest(t, s, "GET", "/api/v1/unknown-path", nil, headers)
|
||||
require.Equal(t, http.StatusNotFound, res.Result().StatusCode)
|
||||
|
||||
test.Snapshoter.Save(t, res.Body.String())
|
||||
})
|
||||
})
|
||||
}
|
||||
11
internal/api/router/templates/templates.go
Normal file
11
internal/api/router/templates/templates.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package templates
|
||||
|
||||
type ViewTemplate string
|
||||
|
||||
const (
|
||||
ViewTemplateAccountConfirmation ViewTemplate = "account_confirmation.html.tmpl"
|
||||
)
|
||||
|
||||
func (vt ViewTemplate) String() string {
|
||||
return string(vt)
|
||||
}
|
||||
Reference in New Issue
Block a user