package touka
import (
"bytes"
"context"
"encoding/xml"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync"
"testing"
"time"
"github.com/fenthope/reco"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// --- Test Structures ---
// TestBindStruct is a common struct used for various binding tests.
type TestBindStruct struct {
Name string `json:"name" xml:"name" form:"name" query:"name" schema:"name"`
Age int `json:"age" xml:"age" form:"age" query:"age" schema:"age"`
IsActive bool `json:"isActive" xml:"isActive" form:"isActive" query:"isActive" schema:"isActive"`
// Add a nested struct for more complex scenarios if needed
// Nested TestNestedStruct `json:"nested" xml:"nested" form:"nested" query:"nested"`
}
// TestNestedStruct example for future use.
// type TestNestedStruct struct {
// Field string `json:"field" xml:"field" form:"field" query:"field"`
// }
// mockHTMLRender implements HTMLRender for testing Context.HTML.
type mockHTMLRender struct {
CalledWithWriter io.Writer
CalledWithName string
CalledWithData interface{}
CalledWithCtx *Context
ReturnError error
}
func (m *mockHTMLRender) Render(writer io.Writer, name string, data interface{}, c *Context) error {
m.CalledWithWriter = writer
m.CalledWithName = name
m.CalledWithData = data
m.CalledWithCtx = c
return m.ReturnError
}
// mockErrorHandler for testing ErrorUseHandle.
type mockErrorHandler struct {
CalledWithCtx *Context
CalledWithCode int
CalledWithErr error
mutex sync.Mutex
}
func (m *mockErrorHandler) Handle(c *Context, code int, err error) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.CalledWithCtx = c
m.CalledWithCode = code
m.CalledWithErr = err
}
func (m *mockErrorHandler) GetArgs() (*Context, int, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
return m.CalledWithCtx, m.CalledWithCode, m.CalledWithErr
}
// MockRecoLogger is a mock implementation of reco.Logger for testing.
type MockRecoLogger struct {
mock.Mock
}
func (m *MockRecoLogger) Debugf(format string, args ...any) { m.Called(format, args) }
func (m *MockRecoLogger) Infof(format string, args ...any) { m.Called(format, args) }
func (m *MockRecoLogger) Warnf(format string, args ...any) { m.Called(format, args) }
func (m *MockRecoLogger) Errorf(format string, args ...any) { m.Called(format, args) }
func (m *MockRecoLogger) Fatalf(format string, args ...any) { m.Called(format, args); panic("Fatalf called") } // Panic to simplify test flow
func (m *MockRecoLogger) Panicf(format string, args ...any) { m.Called(format, args); panic("Panicf called") }
func (m *MockRecoLogger) Debug(args ...any) { m.Called(args) }
func (m *MockRecoLogger) Info(args ...any) { m.Called(args) }
func (m *MockRecoLogger) Warn(args ...any) { m.Called(args) }
func (m *MockRecoLogger) Error(args ...any) { m.Called(args) }
func (m *MockRecoLogger) Fatal(args ...any) { m.Called(args); panic("Fatal called") }
func (m *MockRecoLogger) Panic(args ...any) { m.Called(args); panic("Panic called") }
func (m *MockRecoLogger) WithFields(fields map[string]any) *reco.Logger {
args := m.Called(fields)
if logger, ok := args.Get(0).(*reco.Logger); ok {
return logger
}
// In a real mock, you might return a new MockRecoLogger instance configured with these fields.
// For simplicity here, we assume the test won't heavily rely on chaining WithFields.
// Or, ensure your mock reco.Logger has its own WithFields that returns itself or a new mock.
// Fallback: create a new reco.Logger which might not be ideal for asserting chained calls.
fallbackLogger, _ := reco.New(reco.Config{Output: io.Discard})
return fallbackLogger
}
// --- Existing Tests (MaxRequestBodySize, etc.) ---
// (Keeping existing tests as they are valuable)
type TestJSON struct { // This was the original struct for some limit tests
Name string `json:"name"`
Value int `json:"value"`
}
func TestGetReqBodyFull_Limit(t *testing.T) {
smallLimit := int64(10)
largeBody := "this is a body larger than 10 bytes"
smallBody := "small"
t.Run("BodyLargerThanLimit", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(largeBody))
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(smallLimit)
_, err := c.GetReqBodyFull()
assert.Error(t, err)
assert.Contains(t, err.Error(), "request body size exceeds configured limit")
})
t.Run("ContentLengthLargerThanLimit", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(smallBody))
req.ContentLength = smallLimit + 1
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(smallLimit)
_, err := c.GetReqBodyFull()
assert.Error(t, err)
assert.Contains(t, err.Error(), "request body size")
assert.Contains(t, err.Error(), "exceeds configured limit")
})
t.Run("BodySmallerThanLimit", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(smallBody))
req.ContentLength = int64(len(smallBody))
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(smallLimit)
bodyBytes, err := c.GetReqBodyFull()
assert.NoError(t, err)
assert.Equal(t, smallBody, string(bodyBytes))
})
t.Run("BodySlightlyLargerNoContentLength", func(t *testing.T) {
slightlyLargeBody := "elevenbytes" // 11 bytes
req, _ := http.NewRequest("POST", "/", strings.NewReader(slightlyLargeBody))
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(smallLimit) // Limit is 10
_, err := c.GetReqBodyFull()
assert.Error(t, err)
assert.Contains(t, err.Error(), "request body size exceeds configured limit")
})
}
// Renamed original TestShouldBindJSON_Limit to avoid conflict with new comprehensive TestShouldBindJSON
func TestShouldBindJSON_MaxBodyLimit(t *testing.T) {
smallLimit := int64(20)
// Original TestJSON is fine here as we are testing limits, not field variety
largeJSON := `{"name":"this is a very long name that exceeds the small limit","value":12345}`
smallValidJSON := `{"name":"s","v":1}`
t.Run("JSONLargerThanLimit", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(largeJSON))
req.Header.Set("Content-Type", "application/json")
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(smallLimit)
var data TestJSON
err := c.ShouldBindJSON(&data)
assert.Error(t, err)
assert.Contains(t, err.Error(), "request body size exceeds configured limit")
})
t.Run("ContentLengthLargerThanLimitJSON", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(smallValidJSON))
req.Header.Set("Content-Type", "application/json")
req.ContentLength = smallLimit + 1
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(smallLimit)
var data TestJSON
err := c.ShouldBindJSON(&data)
assert.Error(t, err)
assert.Contains(t, err.Error(), "request body size")
assert.Contains(t, err.Error(), "exceeds configured limit")
})
// This test was a bit ambiguous, using TestJSON for a TestBindStruct scenario.
// Keeping it but clarifying it tests the limit, not comprehensive binding.
t.Run("JSONSmallerThanLimit_MaxBodyTest", func(t *testing.T) {
validJSONSpecific := `{"name":"test","value":1}` // This is TestJSON struct
req, _ := http.NewRequest("POST", "/", strings.NewReader(validJSONSpecific))
req.Header.Set("Content-Type", "application/json")
engineLimit := int64(len(validJSONSpecific) + 5)
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(engineLimit)
var data TestJSON
err := c.ShouldBindJSON(&data)
assert.NoError(t, err)
assert.Equal(t, "test", data.Name)
assert.Equal(t, 1, data.Value)
})
t.Run("JSONSlightlyLargerNoContentLength_MaxBodyTest", func(t *testing.T) {
slightlyLargeJSON := `{"name":"abcde","value":1}` // 24 bytes
req, _ := http.NewRequest("POST", "/", strings.NewReader(slightlyLargeJSON))
req.Header.Set("Content-Type", "application/json")
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(smallLimit) // Limit is 20
var data TestJSON // Using original TestJSON for this specific limit test
err := c.ShouldBindJSON(&data)
assert.Error(t, err)
assert.Contains(t, err.Error(), "request body size exceeds configured limit")
})
}
func TestMaxRequestBodySize_Disabled(t *testing.T) {
largeBody := strings.Repeat("a", 1*1024*1024) // 1MB, reduced for test speed
largeJSON := `{"name":"` + strings.Repeat("b", 1*1024*500) + `","value":1}`
t.Run("GetReqBodyFull_DisabledZero", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(largeBody))
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(0)
bodyBytes, err := c.GetReqBodyFull()
assert.NoError(t, err)
assert.Equal(t, largeBody, string(bodyBytes))
})
t.Run("GetReqBodyFull_DisabledNegative", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(largeBody))
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(-1)
bodyBytes, err := c.GetReqBodyFull()
assert.NoError(t, err)
assert.Equal(t, largeBody, string(bodyBytes))
})
t.Run("ShouldBindJSON_DisabledZero", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(largeJSON))
req.Header.Set("Content-Type", "application/json")
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(0)
var data TestJSON
err := c.ShouldBindJSON(&data)
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(data.Name, "bbb"))
assert.Equal(t, 1, data.Value)
})
t.Run("ShouldBindJSON_DisabledNegative", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(largeJSON))
req.Header.Set("Content-Type", "application/json")
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(-1)
var data TestJSON
err := c.ShouldBindJSON(&data)
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(data.Name, "bbb"))
assert.Equal(t, 1, data.Value)
})
}
func TestGetReqBodyBuffer_Limit(t *testing.T) {
smallLimit := int64(10)
largeBody := "this is a body larger than 10 bytes"
smallBody := "small"
t.Run("BufferBodyLargerThanLimit", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(largeBody))
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(smallLimit)
_, err := c.GetReqBodyBuffer()
assert.Error(t, err)
assert.Contains(t, err.Error(), "request body size exceeds configured limit")
})
t.Run("BufferContentLengthLargerThanLimit", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(smallBody))
req.ContentLength = smallLimit + 1
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(smallLimit)
_, err := c.GetReqBodyBuffer()
assert.Error(t, err)
assert.Contains(t, err.Error(), "request body size")
assert.Contains(t, err.Error(), "exceeds configured limit")
})
t.Run("BufferBodySmallerThanLimit", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(smallBody))
req.ContentLength = int64(len(smallBody))
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(smallLimit)
buffer, err := c.GetReqBodyBuffer()
assert.NoError(t, err)
assert.Equal(t, smallBody, buffer.String())
})
}
// --- Phase 1: Binding Methods Tests ---
func TestShouldBindJSON(t *testing.T) {
defaultLimit := int64(10 * 1024 * 1024) // Assuming default engine limit
testCases := []struct {
name string
contentType string
body string
maxBodySize *int64 // nil to use engine default, pointer to override
expectedError string // Substring of expected error
expectedData *TestBindStruct
}{
{
name: "Success",
contentType: "application/json",
body: `{"name":"John Doe","age":30,"isActive":true}`,
expectedData: &TestBindStruct{Name: "John Doe", Age: 30, IsActive: true},
},
{
name: "Malformed JSON",
contentType: "application/json",
body: `{"name":"John Doe",`,
expectedError: "json binding error",
},
{
name: "Empty request body",
contentType: "application/json",
body: "",
expectedError: "request body is empty", // Error from ShouldBindJSON
},
{
name: "MaxRequestBodySize exceeded",
contentType: "application/json",
body: `{"name":"This body is intentionally made larger than the small limit","age":99,"isActive":false}`,
maxBodySize: func(i int64) *int64 { return &i }(20),
expectedError: "request body size exceeds configured limit",
},
{
name: "Partial fields",
contentType: "application/json",
body: `{"name":"Jane"}`,
expectedData: &TestBindStruct{Name: "Jane", Age: 0, IsActive: false},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var reqBody io.Reader
if tc.body == "" && tc.name == "Empty request body" { // Special case for ShouldBindJSON expecting non-nil body
reqBody = http.NoBody // http.NewRequest will set body to nil if reader is nil
} else {
reqBody = strings.NewReader(tc.body)
}
req, _ := http.NewRequest("POST", "/", reqBody)
if tc.contentType != "" {
req.Header.Set("Content-Type", tc.contentType)
}
if tc.body != "" && tc.name != "Empty request body"{ // Set ContentLength if body is not empty (and not the specific empty body test)
req.ContentLength = int64(len(tc.body))
}
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
if tc.maxBodySize != nil {
engine.SetMaxRequestBodySize(*tc.maxBodySize)
} else {
engine.SetMaxRequestBodySize(defaultLimit) // Ensure a known default for tests not overriding
}
var data TestBindStruct
err := c.ShouldBindJSON(&data)
if tc.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedError)
if strings.Contains(tc.expectedError, "MaxBytesError") { // Check specific error type
var maxBytesErr *http.MaxBytesError
assert.ErrorAs(t, err, &maxBytesErr, "Error should be of type *http.MaxBytesError")
}
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedData, &data)
}
})
}
}
func TestShouldBindXML(t *testing.T) {
defaultLimit := int64(10 * 1024 * 1024)
testCases := []struct {
name string
contentType string
body string
maxBodySize *int64
expectedError string
expectedData *TestBindStruct
}{
{
name: "Success",
contentType: "application/xml",
body: `John Doe30true`,
expectedData: &TestBindStruct{Name: "John Doe", Age: 30, IsActive: true},
},
{
name: "Malformed XML",
contentType: "application/xml",
body: `John Doe`,
expectedError: "xml binding error",
},
{
name: "Empty request body",
contentType: "application/xml",
body: "",
expectedError: "request body is empty for XML binding",
},
{
name: "MaxRequestBodySize exceeded",
contentType: "application/xml",
body: `This body is intentionally made larger than the small limit for XML99false`,
maxBodySize: func(i int64) *int64 { return &i }(20),
expectedError: "request body size exceeds XML binding limit",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var reqBody io.Reader = strings.NewReader(tc.body)
if tc.body == "" {
// For empty body test, ensure ShouldBindXML gets nil or http.NoBody if that's how it's distinguished.
// Based on current ShouldBindXML, a non-nil but empty reader results in EOF, which is an xml error.
// To test the "request body is empty" error, Request.Body must be nil.
if tc.name == "Empty request body" {
reqBody = http.NoBody // http.NewRequest will set body to nil if reader is nil
}
}
req, _ := http.NewRequest("POST", "/", reqBody)
if tc.contentType != "" {
req.Header.Set("Content-Type", tc.contentType)
}
if tc.body != "" {
req.ContentLength = int64(len(tc.body))
}
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
if tc.maxBodySize != nil {
engine.SetMaxRequestBodySize(*tc.maxBodySize)
} else {
engine.SetMaxRequestBodySize(defaultLimit)
}
var data TestBindStruct
err := c.ShouldBindXML(&data)
if tc.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedError)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedData, &data)
}
})
}
}
func TestShouldBindForm(t *testing.T) {
defaultLimit := int64(10 * 1024 * 1024)
createFormRequest := func(contentType string, body io.Reader, contentLength ...int64) *http.Request {
req, _ := http.NewRequest("POST", "/", body)
req.Header.Set("Content-Type", contentType)
if len(contentLength) > 0 {
req.ContentLength = contentLength[0]
} else if s, ok := body.(interface{ Len() int }); ok {
req.ContentLength = int64(s.Len())
}
return req
}
// Helper to create multipart form body
createMultipartBody := func(values map[string]string) (io.Reader, string, error) {
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
for key, value := range values {
if err := writer.WriteField(key, value); err != nil {
return nil, "", err
}
}
if err := writer.Close(); err != nil {
return nil, "", err
}
return body, writer.FormDataContentType(), nil
}
testCases := []struct {
name string
contentType string // Explicitly set or derived from multipart helper
bodyBuilder func() (io.Reader, string, error) // string is boundary/contentType for multipart
isMultipart bool
formValues url.Values // For x-www-form-urlencoded
multipartValues map[string]string // For multipart/form-data
maxBodySize *int64
expectedError string
expectedData *TestBindStruct
}{
{
name: "x-www-form-urlencoded Success",
contentType: "application/x-www-form-urlencoded",
formValues: url.Values{"name": {"John Doe"}, "age": {"30"}, "isActive": {"true"}},
expectedData: &TestBindStruct{Name: "John Doe", Age: 30, IsActive: true},
},
{
name: "multipart/form-data Success",
isMultipart: true,
multipartValues: map[string]string{"name": "Jane Doe", "age": "25", "isActive": "false"},
expectedData: &TestBindStruct{Name: "Jane Doe", Age: 25, IsActive: false},
},
{
name: "Empty request body form", // gorilla/schema will bind zero values
contentType: "application/x-www-form-urlencoded",
formValues: url.Values{},
expectedData: &TestBindStruct{}, // Expect zero values
},
// MaxBodySize tests for forms are tricky because parsing happens before schema decoding.
// The http.MaxBytesReader would act on the raw body stream.
// For x-www-form-urlencoded, it's straightforward.
// For multipart, the error might come from ParseMultipartForm itself if a part is too large for memory,
// or from MaxBytesReader if the whole stream is too large.
{
name: "x-www-form-urlencoded MaxRequestBodySize exceeded",
contentType: "application/x-www-form-urlencoded",
formValues: url.Values{"name": {"This body is very long to exceed the limit"}, "age": {"30"}},
maxBodySize: func(i int64) *int64 { return &i }(20),
// Error comes from MaxBytesReader if ShouldBind applies it before ParseForm,
// or from ParseForm if it respects such a limit internally (less likely for gorilla/schema).
// Touka's current ShouldBindForm doesn't directly apply MaxBytesReader, but ParseMultipartForm does.
// Let's assume the check is before schema.
// The current implementation of ShouldBindForm calls ParseMultipartForm which respects defaultMemory for parts,
// but not MaxRequestBodySize for the whole form if not wrapped by MaxBytesReader in a higher level function (like ShouldBind).
// For this test to be effective for ShouldBindForm directly, MaxBytesReader would need to be part of it,
// or we test it via ShouldBind.
// For now, let's assume ShouldBindForm is tested in isolation and MaxRequestBodySize is not applied within it.
// To properly test MaxRequestBodySize with forms, test via `ShouldBind`.
// This test will likely pass without error if MaxRequestBodySize is not applied inside ShouldBindForm.
// expectedError: "request body size exceeds configured limit", // This would be ideal
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var req *http.Request
var err error
if tc.isMultipart {
body, contentType, err := createMultipartBody(tc.multipartValues)
assert.NoError(t, err)
req = createFormRequest(contentType, body)
} else {
req = createFormRequest(tc.contentType, strings.NewReader(tc.formValues.Encode()))
}
assert.NoError(t, err)
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
if tc.maxBodySize != nil {
engine.SetMaxRequestBodySize(*tc.maxBodySize)
} else {
engine.SetMaxRequestBodySize(defaultLimit)
}
var data TestBindStruct
bindErr := c.ShouldBindForm(&data)
if tc.expectedError != "" {
assert.Error(t, bindErr)
assert.Contains(t, bindErr.Error(), tc.expectedError)
} else {
assert.NoError(t, bindErr)
assert.Equal(t, tc.expectedData, &data)
}
})
}
}
func TestShouldBindQuery(t *testing.T) {
testCases := []struct {
name string
queryString string
expectedError string
expectedData *TestBindStruct
}{
{
name: "Success",
queryString: "name=John+Doe&age=30&isActive=true",
expectedData: &TestBindStruct{Name: "John Doe", Age: 30, IsActive: true},
},
{
name: "Empty query",
queryString: "",
expectedData: &TestBindStruct{}, // gorilla/schema decodes to zero values
},
{
name: "Partial fields",
queryString: "name=Jane&age=25",
expectedData: &TestBindStruct{Name: "Jane", Age: 25, IsActive: false},
},
{
name: "Type conversion error by schema",
queryString: "name=K&age=notanumber&isActive=true",
// gorilla/schema by default might set age to 0 or return an error.
// Let's check for a schema-specific error if it occurs.
// Often it might just result in a zero value for the field.
// For this example, we'll assume it results in a zero value and no direct error from Decode.
// If schema.Decode does error on type mismatch, this test needs adjustment.
// expectedError: "schema:", // Check if schema itself reports conversion errors
expectedData: &TestBindStruct{Name: "K", Age: 0, IsActive: true}, // Assuming 0 for failed int conversion
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", "/?"+tc.queryString, nil)
c, _ := CreateTestContextWithRequest(httptest.NewRecorder(), req)
var data TestBindStruct
err := c.ShouldBindQuery(&data)
if tc.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedError)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedData, &data)
}
})
}
}
func TestShouldBind_ContentTypeDispatch(t *testing.T) {
defaultLimit := int64(10 * 1024 * 1024)
// JSON success case for dispatch check
jsonBody := `{"name":"JsonMan","age":40,"isActive":true}`
expectedJsonData := &TestBindStruct{Name: "JsonMan", Age: 40, IsActive: true}
// XML success case
xmlBody := `XmlMan50false`
expectedXmlData := &TestBindStruct{Name: "XmlMan", Age: 50, IsActive: false}
// Form success case
formBody := "name=FormMan&age=60&isActive=true"
expectedFormData := &TestBindStruct{Name: "FormMan", Age: 60, IsActive: true}
testCases := []struct {
name string
method string
contentType string
body string
expectedError string // Substring
expectedData *TestBindStruct
}{
{name: "Dispatch JSON", method: "POST", contentType: "application/json", body: jsonBody, expectedData: expectedJsonData},
{name: "Dispatch XML", method: "POST", contentType: "application/xml", body: xmlBody, expectedData: expectedXmlData},
{name: "Dispatch text/xml", method: "POST", contentType: "text/xml", body: xmlBody, expectedData: expectedXmlData},
{name: "Dispatch FormURLEncoded", method: "POST", contentType: "application/x-www-form-urlencoded", body: formBody, expectedData: expectedFormData},
// Multipart/form-data test for ShouldBind (more complex to set up body here, ShouldBindForm tests cover its internals)
// For ShouldBind dispatch, just ensuring it routes is key.
{
name: "Dispatch Multipart (via ShouldBindForm)",
method: "POST",
contentType: func()string{ // Create a dummy multipart body to get content type
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
writer.WriteField("name", "MultipartMan")
writer.Close()
return writer.FormDataContentType()
}(),
body: func()string{ // Create a dummy multipart body
bodyBuf := new(bytes.Buffer)
writer := multipart.NewWriter(bodyBuf)
writer.WriteField("name", "MultipartMan")
writer.WriteField("age", "70")
writer.WriteField("isActive", "true")
writer.Close()
return bodyBuf.String()
}(),
expectedData: &TestBindStruct{Name: "MultipartMan", Age: 70, IsActive: true},
},
{name: "Unsupported Content-Type", method: "POST", contentType: "text/plain", body: "hello", expectedError: "unsupported Content-Type for binding: text/plain"},
{name: "Missing Content-Type with body", method: "POST", contentType: "", body: "some data", expectedError: "missing Content-Type header"},
{name: "Missing Content-Type no body (ContentLength 0)", method: "POST", contentType: "", body: "" /* ContentLength will be 0 */, expectedData: nil /* Should return nil error */},
{name: "No body (GET request)", method: "GET", contentType: "", body: "", expectedData: nil /* Should return nil error */},
{name: "No body (POST with http.NoBody)", method: "POST", contentType: "application/json", body: "NO_BODY_MARKER", expectedData: nil /* Should return nil error */},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var reqBody io.Reader
if tc.body == "NO_BODY_MARKER" {
reqBody = http.NoBody
} else if tc.body != "" {
reqBody = strings.NewReader(tc.body)
} // else reqBody is nil, http.NewRequest handles this
req, _ := http.NewRequest(tc.method, "/", reqBody)
if tc.contentType != "" {
req.Header.Set("Content-Type", tc.contentType)
}
// Set ContentLength for POST/PUT if body is present
if (tc.method == "POST" || tc.method == "PUT") && tc.body != "" && tc.body != "NO_BODY_MARKER" {
req.ContentLength = int64(len(tc.body))
}
c, engine := CreateTestContextWithRequest(httptest.NewRecorder(), req)
engine.SetMaxRequestBodySize(defaultLimit) // Use a default reasonable limit
var data TestBindStruct
err := c.ShouldBind(&data)
if tc.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedError)
} else {
assert.NoError(t, err)
if tc.expectedData != nil {
assert.Equal(t, tc.expectedData, &data)
} else {
// If expectedData is nil, it means we expect data to be zero-value struct
// This happens when body is nil or Content-Type implies no data to bind
assert.Equal(t, &TestBindStruct{}, &data)
}
}
})
}
}
// --- Phase 2: HTML Rendering ---
func TestContextHTML(t *testing.T) {
t.Run("HTMLRender not configured", func(t *testing.T) {
w := httptest.NewRecorder()
c, engine := CreateTestContext(w)
engine.HTMLRender = nil // Ensure it's nil
// Mock the error handler to capture its arguments
mockErrHandler := &mockErrorHandler{}
engine.SetErrorHandler(mockErrHandler.Handle)
c.HTML(http.StatusOK, "test.tpl", H{"name": "Touka"})
assert.Equal(t, http.StatusInternalServerError, w.Code) // ErrorUseHandle should set this
_, code, err := mockErrHandler.GetArgs()
assert.Equal(t, http.StatusInternalServerError, code)
assert.Error(t, err)
assert.Contains(t, err.Error(), "HTML renderer not configured")
})
t.Run("HTMLRender success", func(t *testing.T) {
w := httptest.NewRecorder()
c, engine := CreateTestContext(w)
mockRender := &mockHTMLRender{}
engine.HTMLRender = mockRender
templateData := H{"framework": "Touka"}
c.HTML(http.StatusCreated, "index.html", templateData)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
assert.Equal(t, w, mockRender.CalledWithWriter) // Check if writer is the same (or wrapped version)
assert.Equal(t, "index.html", mockRender.CalledWithName)
assert.Equal(t, templateData, mockRender.CalledWithData)
assert.Equal(t, c, mockRender.CalledWithCtx)
assert.Nil(t, mockRender.ReturnError) // Ensure no error was returned by mock
})
t.Run("HTMLRender returns error", func(t *testing.T) {
w := httptest.NewRecorder()
c, engine := CreateTestContext(w)
renderErr := errors.New("template execution failed")
mockRender := &mockHTMLRender{ReturnError: renderErr}
engine.HTMLRender = mockRender
// Mock the error handler
mockErrHandler := &mockErrorHandler{}
engine.SetErrorHandler(mockErrHandler.Handle)
c.HTML(http.StatusOK, "error.tpl", nil)
// ErrorUseHandle should be called
_, code, err := mockErrHandler.GetArgs()
assert.Equal(t, http.StatusInternalServerError, code) // Default code from ErrorUseHandle
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to render HTML template 'error.tpl'")
assert.True(t, errors.Is(err, renderErr)) // Check if it wraps the original error
// Check if the error was added to context errors
assert.NotEmpty(t, c.Errors)
assert.True(t, errors.Is(c.Errors[0], renderErr))
})
}
// --- Phase 3: State Management (Keys) ---
func TestContextKeys(t *testing.T) {
c, _ := CreateTestContext(nil)
// Test Set and Get
c.Set("myKey", "myValue")
val, exists := c.Get("myKey")
assert.True(t, exists)
assert.Equal(t, "myValue", val)
_, exists = c.Get("nonExistentKey")
assert.False(t, exists)
// Test MustGet
assert.Equal(t, "myValue", c.MustGet("myKey"))
assert.Panics(t, func() { c.MustGet("nonExistentKeyPanic") }, "MustGet should panic for non-existent key")
// Typed Getters
c.Set("stringVal", "hello")
c.Set("intVal", 123)
c.Set("boolVal", true)
c.Set("floatVal", 123.456)
timeVal := time.Now()
c.Set("timeVal", timeVal)
durationVal := time.Hour
c.Set("durationVal", durationVal)
c.Set("wrongTypeForString", 12345)
// GetString
sVal, sExists := c.GetString("stringVal")
assert.True(t, sExists)
assert.Equal(t, "hello", sVal)
_, sExists = c.GetString("wrongTypeForString")
assert.False(t, sExists)
_, sExists = c.GetString("noKey")
assert.False(t, sExists)
// GetInt
iVal, iExists := c.GetInt("intVal")
assert.True(t, iExists)
assert.Equal(t, 123, iVal)
_, iExists = c.GetInt("stringVal")
assert.False(t, iExists)
_, iExists = c.GetInt("noKey")
assert.False(t, iExists)
// GetBool
bVal, bExists := c.GetBool("boolVal")
assert.True(t, bExists)
assert.True(t, bVal)
_, bExists = c.GetBool("stringVal")
assert.False(t, bExists)
_, bExists = c.GetBool("noKey")
assert.False(t, bExists)
// GetFloat64
fVal, fExists := c.GetFloat64("floatVal")
assert.True(t, fExists)
assert.Equal(t, 123.456, fVal)
_, fExists = c.GetFloat64("stringVal")
assert.False(t, fExists)
_, fExists = c.GetFloat64("noKey")
assert.False(t, fExists)
// GetTime
tVal, tExists := c.GetTime("timeVal")
assert.True(t, tExists)
assert.Equal(t, timeVal, tVal)
_, tExists = c.GetTime("stringVal")
assert.False(t, tExists)
_, tExists = c.GetTime("noKey")
assert.False(t, tExists)
// GetDuration
dVal, dExists := c.GetDuration("durationVal")
assert.True(t, dExists)
assert.Equal(t, time.Hour, dVal)
_, dExists = c.GetDuration("stringVal")
assert.False(t, dExists)
_, dExists = c.GetDuration("noKey")
assert.False(t, dExists)
}
// --- Phase 4: Core Request/Response Functionality ---
func TestContext_QueryAndDefaultQuery(t *testing.T) {
req, _ := http.NewRequest("GET", "/test?name=touka&age=2&empty=", nil)
c, _ := CreateTestContextWithRequest(nil, req)
assert.Equal(t, "touka", c.Query("name"))
assert.Equal(t, "2", c.Query("age"))
assert.Equal(t, "", c.Query("empty"))
assert.Equal(t, "", c.Query("nonexistent"))
assert.Equal(t, "touka", c.DefaultQuery("name", "default_val"))
assert.Equal(t, "default_val", c.DefaultQuery("nonexistent", "default_val"))
assert.Equal(t, "", c.DefaultQuery("empty", "default_val"))
}
func TestContext_PostFormAndDefaultPostForm(t *testing.T) {
form := url.Values{}
form.Add("name", "touka_form")
form.Add("age", "3")
form.Add("empty_field", "")
req, _ := http.NewRequest("POST", "/", strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
c, _ := CreateTestContextWithRequest(nil, req)
// Test PostForm
assert.Equal(t, "touka_form", c.PostForm("name"))
assert.Equal(t, "3", c.PostForm("age"))
assert.Equal(t, "", c.PostForm("empty_field"))
assert.Equal(t, "", c.PostForm("nonexistent"))
// Test DefaultPostForm
assert.Equal(t, "touka_form", c.DefaultPostForm("name", "default_val"))
assert.Equal(t, "default_val", c.DefaultPostForm("nonexistent", "default_val"))
assert.Equal(t, "", c.DefaultPostForm("empty_field", "default_val"))
// Test again to ensure caching works (formCache is populated on first call)
assert.Equal(t, "touka_form", c.PostForm("name"))
}
func TestContext_Param(t *testing.T) {
c, _ := CreateTestContext(nil)
c.Params = Params{Param{Key: "id", Value: "123"}, Param{Key: "name", Value: "touka"}}
assert.Equal(t, "123", c.Param("id"))
assert.Equal(t, "touka", c.Param("name"))
assert.Equal(t, "", c.Param("nonexistent"))
}
func TestContext_ClientIP(t *testing.T) {
c, engine := CreateTestContext(nil) // Engine needed for ForwardByClientIP config
// Test with X-Forwarded-For
engine.ForwardByClientIP = true
engine.RemoteIPHeaders = []string{"X-Forwarded-For", "X-Real-IP"}
c.Request.Header.Set("X-Forwarded-For", "1.1.1.1, 2.2.2.2")
assert.Equal(t, "1.1.1.1", c.ClientIP())
c.Request.Header.Del("X-Forwarded-For")
// Test with X-Real-IP
c.Request.Header.Set("X-Real-IP", "3.3.3.3")
assert.Equal(t, "3.3.3.3", c.ClientIP())
c.Request.Header.Del("X-Real-IP")
// Test with multiple X-Forwarded-For, some invalid
c.Request.Header.Set("X-Forwarded-For", "invalid, 1.2.3.4, 5.6.7.8")
assert.Equal(t, "1.2.3.4", c.ClientIP())
// Test with RemoteAddr (no proxy headers, ForwardByClientIP = true)
c.Request.Header.Del("X-Forwarded-For") // Ensure it's clean
c.Request.RemoteAddr = "4.4.4.4:12345"
assert.Equal(t, "4.4.4.4", c.ClientIP())
// Test with RemoteAddr (ForwardByClientIP = false)
engine.ForwardByClientIP = false
c.Request.Header.Set("X-Forwarded-For", "1.1.1.1") // This should be ignored
c.Request.RemoteAddr = "5.5.5.5:8080"
assert.Equal(t, "5.5.5.5", c.ClientIP())
// Test with invalid RemoteAddr
engine.ForwardByClientIP = false
c.Request.RemoteAddr = "invalid_remote_addr"
assert.Equal(t, "", c.ClientIP()) // Expect empty or some default if parsing fails badly
}
func TestContext_Status(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := CreateTestContext(recorder)
c.Status(http.StatusTeapot)
assert.Equal(t, http.StatusTeapot, recorder.Code)
assert.True(t, c.Writer.Written())
// Test that calling status again doesn't change (WriteHeader should only be called once)
// Note: The current ResponseWriter doesn't prevent multiple calls to WriteHeader,
// but http.ResponseWriter standard behavior is that first call wins.
// Our wrapper might allow overwriting status if Write isn't called yet.
// c.Status(http.StatusOK)
// assert.Equal(t, http.StatusTeapot, recorder.Code)
}
func TestContext_Redirect(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := CreateTestContextWithRequest(recorder, httptest.NewRequest("GET", "/foo", nil))
c.Redirect(http.StatusMovedPermanently, "/bar")
assert.Equal(t, http.StatusMovedPermanently, recorder.Code)
assert.Equal(t, "/bar", recorder.Header().Get("Location"))
assert.True(t, c.IsAborted(), "Redirect should abort context")
}
func TestContext_ResponseHeaders(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := CreateTestContext(recorder)
// SetHeader
c.SetHeader("X-Test-Set", "Value1")
assert.Equal(t, "Value1", recorder.Header().Get("X-Test-Set"))
c.SetHeader("X-Test-Set", "Value2") // Overwrite
assert.Equal(t, "Value2", recorder.Header().Get("X-Test-Set"))
// AddHeader
c.AddHeader("X-Test-Add", "ValueA")
assert.Equal(t, "ValueA", recorder.Header().Get("X-Test-Add"))
c.AddHeader("X-Test-Add", "ValueB") // Add another value
assert.EqualValues(t, []string{"ValueA", "ValueB"}, recorder.Header()["X-Test-Add"])
// DelHeader
c.DelHeader("X-Test-Set")
assert.Empty(t, recorder.Header().Get("X-Test-Set"))
// Header (alias for SetHeader)
c.Header("X-Test-Alias", "AliasValue")
assert.Equal(t, "AliasValue", recorder.Header().Get("X-Test-Alias"))
}
func TestContext_Cookies(t *testing.T) {
t.Run("SetCookie", func(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := CreateTestContext(recorder)
c.SetCookie("myCookie", "value123", 3600, "/path", "example.com", true, true)
cookieHeader := recorder.Header().Get("Set-Cookie")
assert.Contains(t, cookieHeader, "myCookie=value123")
assert.Contains(t, cookieHeader, "Max-Age=3600")
assert.Contains(t, cookieHeader, "Path=/path")
assert.Contains(t, cookieHeader, "Domain=example.com")
assert.Contains(t, cookieHeader, "Secure")
assert.Contains(t, cookieHeader, "HttpOnly")
// Default SameSite might be set by http library if not specified, or by our default
// assert.Contains(t, cookieHeader, "SameSite=Lax") // Or whatever default
})
t.Run("GetCookie", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
escapedValue := url.QueryEscape("hello world /?&")
req.AddCookie(&http.Cookie{Name: "testCookie", Value: escapedValue})
c, _ := CreateTestContextWithRequest(nil, req)
val, err := c.GetCookie("testCookie")
assert.NoError(t, err)
assert.Equal(t, "hello world /?&", val)
_, err = c.GetCookie("nonExistentCookie")
assert.Error(t, err) // http.ErrNoCookie
})
t.Run("DeleteCookie", func(t *testing.T){
recorder := httptest.NewRecorder()
c, _ := CreateTestContext(recorder)
c.DeleteCookie("toBeDeleted")
cookieHeader := recorder.Header().Get("Set-Cookie")
assert.Contains(t, cookieHeader, "toBeDeleted=")
assert.Contains(t, cookieHeader, "Max-Age=-1")
})
t.Run("SetSameSite affects SetCookie", func(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := CreateTestContext(recorder)
c.SetSameSite(http.SameSiteStrictMode)
c.SetCookie("samesiteCookie", "strict", 0, "/", "", false, false)
assert.Contains(t, recorder.Header().Get("Set-Cookie"), "SameSite=Strict")
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("samesiteCookie2", "lax", 0, "/", "", false, false)
// Note: Browsers might default to Lax if SameSite is not specified or is DefaultMode.
// The test checks if explicitly setting it via SetSameSite works.
// Multiple Set-Cookie headers will be present.
cookies := recorder.Header()["Set-Cookie"]
var foundLax bool
for _, cookieStr := range cookies {
if strings.Contains(cookieStr, "samesiteCookie2=lax") && strings.Contains(cookieStr, "SameSite=Lax"){
foundLax = true
break
}
}
assert.True(t, foundLax, "Lax cookie not found or SameSite not Lax")
})
}
// --- Phase V: Response Writers ---
func TestContext_Raw(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := CreateTestContext(recorder)
testData := []byte("this is raw data")
c.Raw(http.StatusAccepted, "application/octet-stream", testData)
assert.Equal(t, http.StatusAccepted, recorder.Code)
assert.Equal(t, "application/octet-stream", recorder.Header().Get("Content-Type"))
assert.Equal(t, testData, recorder.Body.Bytes())
}
func TestContext_String(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := CreateTestContext(recorder)
c.String(http.StatusOK, "Hello, %s!", "Touka")
assert.Equal(t, http.StatusOK, recorder.Code)
// Default Content-Type for String is text/plain, but it's not explicitly set by c.String
// So we check if it's what http.ResponseWriter defaults to or if our wrapper sets one.
// For now, we'll assume text/plain is desirable if Content-Type is not set before String().
// If c.String should set it, that's a feature to add/verify.
// assert.Equal(t, "text/plain; charset=utf-8", recorder.Header().Get("Content-Type"))
assert.Equal(t, "Hello, Touka!", recorder.Body.String())
}
func TestContext_JSON(t *testing.T) {
t.Run("Success", func(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := CreateTestContext(recorder)
data := H{"name": "Touka", "version": 1.0}
c.JSON(http.StatusOK, data)
assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, "application/json; charset=utf-8", recorder.Header().Get("Content-Type"))
// We need to unmarshal the response to compare content accurately due to potential key order changes.
var responseData H
err := json.Unmarshal(recorder.Body.Bytes(), &responseData)
assert.NoError(t, err)
assert.Equal(t, data["name"], responseData["name"])
// JSON numbers are float64 by default when unmarshalled into interface{}
assert.Equal(t, data["version"].(float64), responseData["version"].(float64))
})
t.Run("Marshalling Error", func(t *testing.T) {
recorder := httptest.NewRecorder()
c, engine := CreateTestContext(recorder)
// Functions are not marshallable to JSON
data := H{"func": func() {}}
mockErrHandler := &mockErrorHandler{}
engine.SetErrorHandler(mockErrHandler.Handle)
c.JSON(http.StatusOK, data)
// Check that ErrorUseHandle was called
_, code, err := mockErrHandler.GetArgs()
assert.Equal(t, http.StatusInternalServerError, code)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to marshal JSON")
assert.NotEmpty(t, c.Errors, "Error should be added to context errors")
})
}
func TestContext_GOB(t *testing.T) {
// Note: GOB requires types to be registered if they are interfaces or concrete types
// are not known ahead of time by the decoder. For simple structs, it's often direct.
type GOBTestStruct struct {
ID int
Data string
}
t.Run("Success", func(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := CreateTestContext(recorder)
data := GOBTestStruct{ID: 1, Data: "Touka GOB Test"}
c.GOB(http.StatusOK, data)
assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, "application/octet-stream", recorder.Header().Get("Content-Type"))
var responseData GOBTestStruct
decoder := gob.NewDecoder(recorder.Body)
err := decoder.Decode(&responseData)
assert.NoError(t, err)
assert.Equal(t, data, responseData)
})
// GOB encoding itself rarely fails for valid Go types unless there's an underlying writer error.
// Testing marshalling error for GOB is harder than for JSON as most Go types are GOB-encodable.
// One way is to use a type that cannot be encoded, e.g., a channel.
t.Run("Marshalling Error (e.g. channel)", func(t *testing.T) {
recorder := httptest.NewRecorder()
c, engine := CreateTestContext(recorder)
data := H{"channel": make(chan int)} // Channels are not GOB encodable
mockErrHandler := &mockErrorHandler{}
engine.SetErrorHandler(mockErrHandler.Handle)
c.GOB(http.StatusOK, data)
_, code, err := mockErrHandler.GetArgs()
assert.Equal(t, http.StatusInternalServerError, code)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to encode GOB")
assert.NotEmpty(t, c.Errors, "Error should be added to context errors")
})
}
// --- Phase VI: Error Handling ---
func TestContext_ContextErrors(t *testing.T) {
c, _ := CreateTestContext(nil)
assert.Empty(t, c.GetErrors(), "New context should have no errors")
err1 := errors.New("first test error")
c.AddError(err1)
assert.Len(t, c.GetErrors(), 1)
assert.Equal(t, err1, c.GetErrors()[0])
err2 := errors.New("second test error")
c.AddError(err2)
assert.Len(t, c.GetErrors(), 2)
assert.Equal(t, err1, c.GetErrors()[0]) // Check order
assert.Equal(t, err2, c.GetErrors()[1])
}
func TestContext_ErrorUseHandle(t *testing.T) {
t.Run("Custom Error Handler", func(t *testing.T) {
recorder := httptest.NewRecorder()
c, engine := CreateTestContext(recorder)
mockErrHandler := &mockErrorHandler{}
engine.SetErrorHandler(mockErrHandler.Handle) // Set our mock
testErr := errors.New("custom handler test error")
c.ErrorUseHandle(http.StatusForbidden, testErr)
customCtx, customCode, customErr := mockErrHandler.GetArgs()
assert.Equal(t, c, customCtx)
assert.Equal(t, http.StatusForbidden, customCode)
assert.Equal(t, testErr, customErr)
assert.True(t, c.IsAborted(), "ErrorUseHandle should abort the context")
})
t.Run("Default Error Handler", func(t *testing.T) {
recorder := httptest.NewRecorder()
c, engine := CreateTestContext(recorder)
// Ensure default error handler is used (engine.errorHandle.useDefault = true)
// New() already sets up defaultErrorHandle.
// We can explicitly set it if we want to be super sure for this test.
engine.errorHandle.useDefault = true
engine.errorHandle.handler = defaultErrorHandle
testErr := errors.New("default handler test error")
c.ErrorUseHandle(http.StatusUnauthorized, testErr)
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
assert.Contains(t, recorder.Body.String(), `"error":"default handler test error"`)
assert.Contains(t, recorder.Body.String(), `"code":401`)
assert.Contains(t, recorder.Body.String(), `"message":"Unauthorized"`)
assert.True(t, c.IsAborted(), "ErrorUseHandle should abort the context with default handler")
})
}
// --- Phase VII: Request Header Accessors ---
// Note: Response header tests are in TestContext_ResponseHeaders
func TestContext_RequestHeaders(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Set("X-Custom-Header", "ToukaValue")
req.Header.Add("X-Multi-Value", "Value1")
req.Header.Add("X-Multi-Value", "Value2")
req.Header.Set("Content-Type", "application/test") // For c.ContentType()
req.Header.Set("User-Agent", "ToukaTestAgent/1.0") // For c.UserAgent()
c, _ := CreateTestContextWithRequest(nil, req)
// GetReqHeader
assert.Equal(t, "ToukaValue", c.GetReqHeader("X-Custom-Header"))
assert.Equal(t, "Value1", c.GetReqHeader("X-Multi-Value")) // Get returns the first value
assert.Empty(t, c.GetReqHeader("NonExistent"))
// GetAllReqHeader
allHeaders := c.GetAllReqHeader()
assert.Equal(t, "ToukaValue", allHeaders.Get("X-Custom-Header"))
assert.EqualValues(t, []string{"Value1", "Value2"}, allHeaders["X-Multi-Value"])
// ContentType
assert.Equal(t, "application/test", c.ContentType())
// UserAgent
assert.Equal(t, "ToukaTestAgent/1.0", c.UserAgent())
}
// --- Phase IX: Streaming & Body Access ---
func TestContext_GetReqBodyFull_and_Buffer_SuccessCases(t *testing.T) {
bodyContent := "Hello Touka Body"
t.Run("GetReqBodyFull Success", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(bodyContent))
c, _ := CreateTestContextWithRequest(nil, req)
fullBody, err := c.GetReqBodyFull()
assert.NoError(t, err)
assert.Equal(t, bodyContent, string(fullBody))
})
t.Run("GetReqBodyBuffer Success", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/", strings.NewReader(bodyContent))
c, _ := CreateTestContextWithRequest(nil, req)
bufferBody, err := c.GetReqBodyBuffer()
assert.NoError(t, err)
assert.Equal(t, bodyContent, bufferBody.String())
})
t.Run("GetReqBody when Body is nil", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil) // No body
c, _ := CreateTestContextWithRequest(nil, req)
// GetReqBodyFull should handle nil body gracefully (returns nil, nil)
fullBody, err := c.GetReqBodyFull()
assert.NoError(t, err, "GetReqBodyFull with nil body should not error")
assert.Nil(t, fullBody, "GetReqBodyFull with nil body should return nil data")
// GetReqBodyBuffer should also handle nil body gracefully
bufferBody, err := c.GetReqBodyBuffer()
assert.NoError(t, err, "GetReqBodyBuffer with nil body should not error")
assert.Nil(t, bufferBody, "GetReqBodyBuffer with nil body should return nil data")
})
}
func TestContext_WriteStream_and_SetBodyStream(t *testing.T) {
streamContent := "This is data to be streamed."
t.Run("WriteStream", func(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := CreateTestContext(recorder)
reader := strings.NewReader(streamContent)
written, err := c.WriteStream(reader)
assert.NoError(t, err)
assert.Equal(t, int64(len(streamContent)), written)
assert.Equal(t, http.StatusOK, recorder.Code) // Default by WriteStream
assert.Equal(t, streamContent, recorder.Body.String())
})
t.Run("SetBodyStream with known content size", func(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := CreateTestContext(recorder)
reader := strings.NewReader(streamContent)
c.SetBodyStream(reader, len(streamContent))
assert.Equal(t, http.StatusOK, recorder.Code) // Default by SetBodyStream
assert.Equal(t, streamContent, recorder.Body.String())
assert.Equal(t, fmt.Sprintf("%d", len(streamContent)), recorder.Header().Get("Content-Length"))
})
t.Run("SetBodyStream with unknown content size (-1)", func(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := CreateTestContext(recorder)
reader := strings.NewReader(streamContent)
c.SetBodyStream(reader, -1) // Unknown size
assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, streamContent, recorder.Body.String())
assert.Empty(t, recorder.Header().Get("Content-Length"), "Content-Length should be absent for chunked/unknown size")
// Depending on server implementation, Transfer-Encoding: chunked might be set.
// httptest.ResponseRecorder might not reflect this header automatically.
})
}
// --- Phase X: Native Context Methods ---
func TestContext_GoContext(t *testing.T) {
goCtx, cancel := context.WithCancel(context.Background())
req, _ := http.NewRequestWithContext(goCtx, "GET", "/", nil)
c, _ := CreateTestContextWithRequest(nil, req)
assert.NoError(t, c.Err(), "Context error should be nil initially")
select {
case <-c.Done():
t.Fatal("Context should not be done yet")
default:
}
// Test Value from Go context
type ctxKey string
const testCtxKey ctxKey = "goCtxKey"
goCtxWithValue := context.WithValue(goCtx, testCtxKey, "goCtxValue")
reqWithValue, _ := http.NewRequestWithContext(goCtxWithValue, "GET", "/", nil)
cWithValue, _ := CreateTestContextWithRequest(nil, reqWithValue)
valFromCtx := cWithValue.Value(testCtxKey)
assert.Equal(t, "goCtxValue", valFromCtx, "Should get value from underlying Go context")
// Test Value from Touka context (Keys)
cWithValue.Set("toukaKey", "toukaValue")
valFromToukaKeys := cWithValue.Value("toukaKey")
assert.Equal(t, "toukaValue", valFromToukaKeys, "Should get value from Touka's Keys map")
// Cancel the context
cancel()
<-c.Done() // Wait for Done channel to be closed
assert.Error(t, c.Err(), "Context error should be non-nil after cancellation")
assert.Equal(t, context.Canceled, c.Err())
}
// --- Phase XI: Logging ---
func TestContext_Logger(t *testing.T) {
c, engine := CreateTestContext(nil)
mockLogger := new(MockRecoLogger) // Using testify mock
engine.LogReco = mockLogger.Mock // Assign the mock.Mock part of MockRecoLogger
// Prepare expected calls for non-panicking methods
mockLogger.On("Debugf", "Debug: %s", []interface{}{"test_debug"}).Return()
mockLogger.On("Infof", "Info: %s", []interface{}{"test_info"}).Return()
mockLogger.On("Warnf", "Warn: %s", []interface{}{"test_warn"}).Return()
mockLogger.On("Errorf", "Error: %s", []interface{}{"test_error"}).Return()
c.Debugf("Debug: %s", "test_debug")
c.Infof("Info: %s", "test_info")
c.Warnf("Warn: %s", "test_warn")
c.Errorf("Error: %s", "test_error")
mockLogger.AssertCalled(t, "Debugf", "Debug: %s", []interface{}{"test_debug"})
mockLogger.AssertCalled(t, "Infof", "Info: %s", []interface{}{"test_info"})
mockLogger.AssertCalled(t, "Warnf", "Warn: %s", []interface{}{"test_warn"})
mockLogger.AssertCalled(t, "Errorf", "Error: %s", []interface{}{"test_error"})
// Test Panicf
mockLogger.On("Panicf", "Panic: %s", []interface{}{"test_panic"}).Run(func(args mock.Arguments) {
// This Run func allows us to simulate the panic after logging,
// or just assert it was called if the actual panic is problematic for testing.
// For this test, we'll let the mock definition's Panicf actually panic.
}).Return() // .Return() is needed for .Run to be configured for testify/mock
assert.PanicsWithValue(t, "Panicf called", func() {
c.Panicf("Panic: %s", "test_panic")
}, "c.Panicf should call logger's Panicf and then panic")
mockLogger.AssertCalled(t, "Panicf", "Panic: %s", []interface{}{"test_panic"})
// Fatalf is harder to test without os.Exit. We'll just check if the method is called.
// The mock's Fatalf is set to panic to prevent test termination via os.Exit.
mockLogger.On("Fatalf", "Fatal: %s", []interface{}{"test_fatal"}).Run(func(args mock.Arguments) {}).Return()
assert.PanicsWithValue(t, "Fatalf called", func() {
c.Fatalf("Fatal: %s", "test_fatal")
}, "c.Fatalf should call logger's Fatalf and then panic (due to mock setup)")
mockLogger.AssertCalled(t, "Fatalf", "Fatal: %s", []interface{}{"test_fatal"})
}
// End of tests for now. Some categories from the plan might still need more specific tests.