package middleware import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestRequestID_ValidID_Preserved(t *testing.T) { e := echo.New() mw := RequestIDMiddleware() handler := mw(func(c echo.Context) error { return c.String(http.StatusOK, GetRequestID(c)) }) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(HeaderXRequestID, "abc-123-def") rec := httptest.NewRecorder() c := e.NewContext(req, rec) err := handler(c) assert.NoError(t, err) assert.Equal(t, "abc-123-def", rec.Body.String()) assert.Equal(t, "abc-123-def", rec.Header().Get(HeaderXRequestID)) } func TestRequestID_Empty_GeneratesNew(t *testing.T) { e := echo.New() mw := RequestIDMiddleware() handler := mw(func(c echo.Context) error { return c.String(http.StatusOK, GetRequestID(c)) }) req := httptest.NewRequest(http.MethodGet, "/", nil) // No X-Request-ID header rec := httptest.NewRecorder() c := e.NewContext(req, rec) err := handler(c) assert.NoError(t, err) // Should be a UUID (36 chars: 8-4-4-4-12) assert.Len(t, rec.Body.String(), 36) } func TestRequestID_ControlChars_Sanitized(t *testing.T) { // SEC-29: Client-supplied X-Request-ID with control characters must be rejected. tests := []struct { name string inputID string }{ {"newline injection", "abc\ndef"}, {"carriage return", "abc\rdef"}, {"null byte", "abc\x00def"}, {"tab character", "abc\tdef"}, {"html tags", "abc"}, {"spaces", "abc def"}, {"semicolons", "abc;def"}, {"unicode", "abc\u200bdef"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := echo.New() mw := RequestIDMiddleware() handler := mw(func(c echo.Context) error { return c.String(http.StatusOK, GetRequestID(c)) }) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(HeaderXRequestID, tt.inputID) rec := httptest.NewRecorder() c := e.NewContext(req, rec) err := handler(c) assert.NoError(t, err) // The malicious ID should be replaced with a generated UUID assert.NotEqual(t, tt.inputID, rec.Body.String(), "control chars should be rejected, got original ID back") assert.Len(t, rec.Body.String(), 36, "should be a generated UUID") }) } } func TestRequestID_TooLong_Sanitized(t *testing.T) { // SEC-29: X-Request-ID longer than 64 chars should be rejected. e := echo.New() mw := RequestIDMiddleware() handler := mw(func(c echo.Context) error { return c.String(http.StatusOK, GetRequestID(c)) }) longID := strings.Repeat("a", 65) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(HeaderXRequestID, longID) rec := httptest.NewRecorder() c := e.NewContext(req, rec) err := handler(c) assert.NoError(t, err) assert.NotEqual(t, longID, rec.Body.String(), "overly long ID should be replaced") assert.Len(t, rec.Body.String(), 36, "should be a generated UUID") } func TestRequestID_MaxLength_Accepted(t *testing.T) { // Exactly 64 chars of valid characters should be accepted e := echo.New() mw := RequestIDMiddleware() handler := mw(func(c echo.Context) error { return c.String(http.StatusOK, GetRequestID(c)) }) maxID := strings.Repeat("a", 64) req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set(HeaderXRequestID, maxID) rec := httptest.NewRecorder() c := e.NewContext(req, rec) err := handler(c) assert.NoError(t, err) assert.Equal(t, maxID, rec.Body.String(), "64-char valid ID should be accepted") }