init push
This commit is contained in:
586
internal/app/api/middleware_test.go
Normal file
586
internal/app/api/middleware_test.go
Normal file
@@ -0,0 +1,586 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
|
||||
"wucher/internal/config"
|
||||
"wucher/internal/shared/pkg/apperrorsx"
|
||||
"wucher/internal/transport/http/jsonapi"
|
||||
"wucher/internal/transport/http/response"
|
||||
)
|
||||
|
||||
func testHTTPConfig() config.HTTPConfig {
|
||||
return config.HTTPConfig{
|
||||
CORSAllowOrigins: "http://localhost:3000",
|
||||
CORSAllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS",
|
||||
CORSAllowHeaders: "Origin,Content-Type,Accept,Authorization",
|
||||
CORSExposeHeaders: "X-Request-ID",
|
||||
CORSAllowCredentials: true,
|
||||
CORSMaxAge: 300,
|
||||
RateLimitMax: 100,
|
||||
RateLimitWindow: time.Minute,
|
||||
RequestTimeout: 0,
|
||||
RequestTimeoutByPath: map[string]time.Duration{},
|
||||
HSTSMaxAge: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_CORSHeaders(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/ping", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
||||
req.Header.Set("Origin", "http://localhost:3000")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "http://localhost:3000" {
|
||||
t.Fatalf("expected allow-origin http://localhost:3000, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Credentials"); got != "true" {
|
||||
t.Fatalf("expected allow-credentials true, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("X-Frame-Options"); got != "DENY" {
|
||||
t.Fatalf("expected X-Frame-Options DENY, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("X-Request-Id"); got == "" {
|
||||
t.Fatalf("expected request id header to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_BlockTrace(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.All("/ping", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
||||
|
||||
req, _ := http.NewRequest(http.MethodTrace, "/ping", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("expected 405, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_RateLimiter(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.RateLimitMax = 1
|
||||
cfg.RateLimitWindow = time.Minute
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/limited", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
||||
|
||||
firstReq, _ := http.NewRequest(http.MethodGet, "/limited", nil)
|
||||
firstResp, err := app.Test(firstReq)
|
||||
if err != nil {
|
||||
t.Fatalf("first request app.Test: %v", err)
|
||||
}
|
||||
if firstResp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected first request 200, got %d", firstResp.StatusCode)
|
||||
}
|
||||
|
||||
secondReq, _ := http.NewRequest(http.MethodGet, "/limited", nil)
|
||||
secondResp, err := app.Test(secondReq)
|
||||
if err != nil {
|
||||
t.Fatalf("second request app.Test: %v", err)
|
||||
}
|
||||
if secondResp.StatusCode != http.StatusTooManyRequests {
|
||||
t.Fatalf("expected second request 429, got %d", secondResp.StatusCode)
|
||||
}
|
||||
if got := secondResp.Header.Get("Retry-After"); got == "" {
|
||||
t.Fatalf("expected Retry-After header on 429 response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_EndpointRateLimiter(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.RateLimitMax = 100
|
||||
cfg.RateLimitByEndpoint = map[string]config.RateLimitRule{
|
||||
"POST /auth/login": {
|
||||
Max: 1,
|
||||
Window: time.Minute,
|
||||
},
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Post("/auth/login", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
||||
app.Post("/auth/refresh", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
||||
|
||||
req1, _ := http.NewRequest(http.MethodPost, "/auth/login", nil)
|
||||
resp1, err := app.Test(req1)
|
||||
if err != nil {
|
||||
t.Fatalf("first login request app.Test: %v", err)
|
||||
}
|
||||
if resp1.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected first login request 200, got %d", resp1.StatusCode)
|
||||
}
|
||||
|
||||
req2, _ := http.NewRequest(http.MethodPost, "/auth/login", nil)
|
||||
resp2, err := app.Test(req2)
|
||||
if err != nil {
|
||||
t.Fatalf("second login request app.Test: %v", err)
|
||||
}
|
||||
if resp2.StatusCode != http.StatusTooManyRequests {
|
||||
t.Fatalf("expected second login request 429, got %d", resp2.StatusCode)
|
||||
}
|
||||
|
||||
otherReq, _ := http.NewRequest(http.MethodPost, "/auth/refresh", nil)
|
||||
otherResp, err := app.Test(otherReq)
|
||||
if err != nil {
|
||||
t.Fatalf("other endpoint request app.Test: %v", err)
|
||||
}
|
||||
if otherResp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected other endpoint request 200, got %d", otherResp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_WildcardOriginDisablesCredentials(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.CORSAllowOrigins = "*"
|
||||
cfg.CORSAllowCredentials = true
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/ping", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
||||
req.Header.Set("Origin", "http://localhost:3000")
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "*" {
|
||||
t.Fatalf("expected allow-origin *, got %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Allow-Credentials"); got != "" {
|
||||
t.Fatalf("expected allow-credentials to be omitted, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_RequestContextDetachedFromShutdown(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/ctx", func(c *fiber.Ctx) error {
|
||||
if c.UserContext() == nil {
|
||||
return c.SendStatus(http.StatusInternalServerError)
|
||||
}
|
||||
if c.UserContext().Done() != nil {
|
||||
return c.SendStatus(http.StatusInternalServerError)
|
||||
}
|
||||
return c.SendStatus(http.StatusOK)
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/ctx", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_CompressesLargeJSONWhenGzipAccepted(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
|
||||
largePayload := map[string]string{
|
||||
"data": strings.Repeat("x", 8*1024),
|
||||
}
|
||||
app.Get("/large-json", func(c *fiber.Ctx) error {
|
||||
return c.JSON(largePayload)
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/large-json", nil)
|
||||
req.Header.Set("Accept-Encoding", "gzip")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Content-Encoding"); got != "gzip" {
|
||||
t.Fatalf("expected Content-Encoding gzip, got %q", got)
|
||||
}
|
||||
gz, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("gzip reader: %v", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
var payload map[string]string
|
||||
if err := json.NewDecoder(gz).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode json: %v", err)
|
||||
}
|
||||
if payload["data"] != largePayload["data"] {
|
||||
t.Fatalf("unexpected payload body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_RequestTimeoutExceeded(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.RequestTimeout = 10 * time.Millisecond
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/slow", func(c *fiber.Ctx) error {
|
||||
select {
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
return c.SendStatus(http.StatusOK)
|
||||
case <-c.UserContext().Done():
|
||||
return c.SendStatus(http.StatusRequestTimeout)
|
||||
}
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/slow", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusRequestTimeout {
|
||||
t.Fatalf("expected 408, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_RequestTimeoutNormalRequestUnaffected(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.RequestTimeout = 200 * time.Millisecond
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/fast", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusOK)
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/fast", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_RequestTimeoutPerEndpointOverride(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.RequestTimeout = 10 * time.Millisecond
|
||||
cfg.RequestTimeoutByPath = map[string]time.Duration{
|
||||
"GET /slow": 120 * time.Millisecond,
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/slow", func(c *fiber.Ctx) error {
|
||||
select {
|
||||
case <-time.After(40 * time.Millisecond):
|
||||
return c.SendStatus(http.StatusOK)
|
||||
case <-c.UserContext().Done():
|
||||
return c.SendStatus(http.StatusRequestTimeout)
|
||||
}
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/slow", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200 with override timeout, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_RequestTimeoutPerEndpointOverrideDynamicRoute(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
cfg.RequestTimeout = 10 * time.Millisecond
|
||||
cfg.RequestTimeoutByPath = map[string]time.Duration{
|
||||
"GET /users/:id": 120 * time.Millisecond,
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/users/:id", func(c *fiber.Ctx) error {
|
||||
select {
|
||||
case <-time.After(40 * time.Millisecond):
|
||||
return c.SendStatus(http.StatusOK)
|
||||
case <-c.UserContext().Done():
|
||||
return c.SendStatus(http.StatusRequestTimeout)
|
||||
}
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/users/123", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200 with dynamic route override timeout, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPAccessLogMiddlewareIncludesErrorDetail(t *testing.T) {
|
||||
var logBuf bytes.Buffer
|
||||
prev := slog.Default()
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(&logBuf, &slog.HandlerOptions{})))
|
||||
t.Cleanup(func() {
|
||||
slog.SetDefault(prev)
|
||||
})
|
||||
|
||||
app := fiber.New()
|
||||
app.Use(httpAccessLogMiddleware())
|
||||
app.Get("/bad", func(c *fiber.Ctx) error {
|
||||
return response.WriteErrors(c, http.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
||||
Title: "Validation error",
|
||||
Detail: "name is required",
|
||||
}})
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/bad", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if strings.Contains(string(body), "name is required") {
|
||||
t.Fatalf("expected sanitized response detail, got %s", string(body))
|
||||
}
|
||||
if !strings.Contains(string(body), `"detail":"validation failed"`) {
|
||||
t.Fatalf("expected generic response detail, got %s", string(body))
|
||||
}
|
||||
if !strings.Contains(logBuf.String(), "name is required") {
|
||||
t.Fatalf("expected access log to include original detail, got %s", logBuf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPAccessLogMiddlewareIncludesResponseBodyOnServerError(t *testing.T) {
|
||||
var logBuf bytes.Buffer
|
||||
prev := slog.Default()
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(&logBuf, &slog.HandlerOptions{})))
|
||||
t.Cleanup(func() {
|
||||
slog.SetDefault(prev)
|
||||
})
|
||||
|
||||
app := fiber.New()
|
||||
app.Use(httpAccessLogMiddleware())
|
||||
app.Get("/boom", func(c *fiber.Ctx) error {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{{
|
||||
"status": "500",
|
||||
"title": "Update failed",
|
||||
"detail": "db deadlock on takeover update",
|
||||
}},
|
||||
})
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/boom", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusInternalServerError {
|
||||
t.Fatalf("expected 500, got %d", resp.StatusCode)
|
||||
}
|
||||
if !strings.Contains(logBuf.String(), "db deadlock on takeover update") {
|
||||
t.Fatalf("expected access log to include response body detail, got %s", logBuf.String())
|
||||
}
|
||||
if !strings.Contains(logBuf.String(), "response_body") {
|
||||
t.Fatalf("expected access log to include response_body field, got %s", logBuf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPAccessLogMiddlewareIncludesInternalErrorDetail(t *testing.T) {
|
||||
var logBuf bytes.Buffer
|
||||
prev := slog.Default()
|
||||
slog.SetDefault(slog.New(slog.NewJSONHandler(&logBuf, &slog.HandlerOptions{})))
|
||||
t.Cleanup(func() {
|
||||
slog.SetDefault(prev)
|
||||
})
|
||||
|
||||
app := fiber.New()
|
||||
app.Use(httpAccessLogMiddleware())
|
||||
app.Get("/boom", func(c *fiber.Ctx) error {
|
||||
c.Locals(apperrorsx.ContextKeyHTTPErrorInternalDetail, "action=UpdateTakeover | error=reserve update failed | chain=*errors.errorString: reserve update failed")
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
|
||||
"errors": []fiber.Map{{
|
||||
"status": "500",
|
||||
"title": "Update failed",
|
||||
"detail": "internal server error",
|
||||
}},
|
||||
})
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/boom", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusInternalServerError {
|
||||
t.Fatalf("expected 500, got %d", resp.StatusCode)
|
||||
}
|
||||
if !strings.Contains(logBuf.String(), "internal_detail") {
|
||||
t.Fatalf("expected access log to include internal_detail field, got %s", logBuf.String())
|
||||
}
|
||||
if !strings.Contains(logBuf.String(), "reserve update failed") {
|
||||
t.Fatalf("expected access log to include internal error detail, got %s", logBuf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultMiddlewares_HTTPMetricsUsesRoutePatternLabels(t *testing.T) {
|
||||
cfg := testHTTPConfig()
|
||||
app := fiber.New()
|
||||
UseDefaultMiddlewares(app, cfg)
|
||||
app.Get("/users/:id", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(http.StatusCreated)
|
||||
})
|
||||
app.Get("/metrics", func(c *fiber.Ctx) error {
|
||||
metricsHandler(c.Context())
|
||||
return nil
|
||||
})
|
||||
|
||||
beforeCounter := metricCounterValue(t, httpRequestsTotal, "GET", "/users/:id", "201")
|
||||
beforeHistogram := metricHistogramCount(t, httpRequestDurationSeconds, "GET", "/users/:id", "201")
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/users/123", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("app.Test: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
afterCounter := metricCounterValue(t, httpRequestsTotal, "GET", "/users/:id", "201")
|
||||
afterHistogram := metricHistogramCount(t, httpRequestDurationSeconds, "GET", "/users/:id", "201")
|
||||
if afterCounter <= beforeCounter {
|
||||
t.Fatalf("expected counter to increase, before=%v after=%v", beforeCounter, afterCounter)
|
||||
}
|
||||
if afterHistogram <= beforeHistogram {
|
||||
t.Fatalf("expected histogram count to increase, before=%v after=%v", beforeHistogram, afterHistogram)
|
||||
}
|
||||
|
||||
metricsReq, _ := http.NewRequest(http.MethodGet, "/metrics", nil)
|
||||
metricsResp, err := app.Test(metricsReq)
|
||||
if err != nil {
|
||||
t.Fatalf("metrics app.Test: %v", err)
|
||||
}
|
||||
if metricsResp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected /metrics status 200, got %d", metricsResp.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(metricsResp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read /metrics body: %v", err)
|
||||
}
|
||||
metricsBody := string(body)
|
||||
if !strings.Contains(metricsBody, "http_requests_total") {
|
||||
t.Fatalf("expected http_requests_total in /metrics output")
|
||||
}
|
||||
if !strings.Contains(metricsBody, "http_request_duration_seconds") {
|
||||
t.Fatalf("expected http_request_duration_seconds in /metrics output")
|
||||
}
|
||||
if !strings.Contains(metricsBody, "path=\"/users/:id\"") {
|
||||
t.Fatalf("expected route-pattern path label in /metrics output")
|
||||
}
|
||||
|
||||
beforeMetricsCounter := metricCounterValue(t, httpRequestsTotal, "GET", "/metrics", "200")
|
||||
beforeMetricsHistogram := metricHistogramCount(t, httpRequestDurationSeconds, "GET", "/metrics", "200")
|
||||
metricsReq2, _ := http.NewRequest(http.MethodGet, "/metrics", nil)
|
||||
metricsResp2, err := app.Test(metricsReq2)
|
||||
if err != nil {
|
||||
t.Fatalf("metrics second app.Test: %v", err)
|
||||
}
|
||||
if metricsResp2.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected second /metrics status 200, got %d", metricsResp2.StatusCode)
|
||||
}
|
||||
afterMetricsCounter := metricCounterValue(t, httpRequestsTotal, "GET", "/metrics", "200")
|
||||
afterMetricsHistogram := metricHistogramCount(t, httpRequestDurationSeconds, "GET", "/metrics", "200")
|
||||
if afterMetricsCounter != beforeMetricsCounter {
|
||||
t.Fatalf("expected /metrics to be excluded from counter, before=%v after=%v", beforeMetricsCounter, afterMetricsCounter)
|
||||
}
|
||||
if afterMetricsHistogram != beforeMetricsHistogram {
|
||||
t.Fatalf("expected /metrics to be excluded from histogram, before=%v after=%v", beforeMetricsHistogram, afterMetricsHistogram)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeHTTPMethod(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output string
|
||||
}{
|
||||
{name: "get", input: "GET", output: "GET"},
|
||||
{name: "lowercase", input: "post", output: "POST"},
|
||||
{name: "trimmed", input: " put ", output: "PUT"},
|
||||
{name: "empty", input: "", output: "UNKNOWN"},
|
||||
{name: "custom", input: "GETT", output: "OTHER"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := normalizeHTTPMethod(tt.input); got != tt.output {
|
||||
t.Fatalf("expected %q, got %q", tt.output, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func metricCounterValue(t *testing.T, vec *prometheus.CounterVec, labels ...string) float64 {
|
||||
t.Helper()
|
||||
m := &dto.Metric{}
|
||||
if err := vec.WithLabelValues(labels...).Write(m); err != nil {
|
||||
t.Fatalf("read counter metric: %v", err)
|
||||
}
|
||||
return m.GetCounter().GetValue()
|
||||
}
|
||||
|
||||
func metricHistogramCount(t *testing.T, vec *prometheus.HistogramVec, labels ...string) uint64 {
|
||||
t.Helper()
|
||||
observer, err := vec.GetMetricWithLabelValues(labels...)
|
||||
if err != nil {
|
||||
t.Fatalf("load histogram metric: %v", err)
|
||||
}
|
||||
metric, ok := observer.(prometheus.Metric)
|
||||
if !ok {
|
||||
t.Fatalf("histogram observer does not implement prometheus.Metric")
|
||||
}
|
||||
m := &dto.Metric{}
|
||||
if err := metric.Write(m); err != nil {
|
||||
t.Fatalf("read histogram metric: %v", err)
|
||||
}
|
||||
if m.GetHistogram() == nil {
|
||||
t.Fatalf("histogram metric not available for labels %v", labels)
|
||||
}
|
||||
return m.GetHistogram().GetSampleCount()
|
||||
}
|
||||
Reference in New Issue
Block a user