diff --git a/context.go b/context.go
index 376cfd4..0620c41 100644
--- a/context.go
+++ b/context.go
@@ -4,13 +4,11 @@ import (
"bytes"
"context"
"encoding/gob"
- "encoding/xml" // Added for XML binding
"errors"
"fmt"
"html/template"
"io"
"math"
- "mime" // Added for Content-Type parsing in ShouldBind
"net"
"net/http"
"net/netip"
@@ -21,7 +19,6 @@ import (
"github.com/fenthope/reco"
"github.com/go-json-experiment/json"
- "github.com/gorilla/schema" // Added for form binding
"github.com/WJQSERVER-STUDIO/go-utils/copyb"
"github.com/WJQSERVER-STUDIO/httpc"
@@ -301,24 +298,21 @@ func (c *Context) HTML(code int, name string, obj interface{}) {
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
c.Writer.WriteHeader(code)
- if c.engine == nil || c.engine.HTMLRender == nil {
- errMsg := "HTML renderer not configured"
- if c.engine != nil && c.engine.LogReco != nil {
- c.engine.LogReco.Error("[Context.HTML] HTMLRender not configured on engine")
- } else {
- // Fallback logging if LogReco is also nil, though unlikely if engine is not nil
- // log.Println("[Context.HTML] HTMLRender not configured on engine")
+ if c.engine != nil && c.engine.HTMLRender != nil {
+ // 假设 HTMLRender 是一个 *template.Template 实例
+ if tpl, ok := c.engine.HTMLRender.(*template.Template); ok {
+ err := tpl.ExecuteTemplate(c.Writer, name, obj)
+ if err != nil {
+ c.AddError(fmt.Errorf("failed to render HTML template '%s': %w", name, err))
+ //c.String(http.StatusInternalServerError, "Internal Server Error: Failed to render HTML template")
+ c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to render HTML template '%s': %w", name, err))
+ }
+ return
}
- c.ErrorUseHandle(http.StatusInternalServerError, errors.New(errMsg))
- return
- }
-
- err := c.engine.HTMLRender.Render(c.Writer, name, obj, c)
- if err != nil {
- renderErr := fmt.Errorf("failed to render HTML template '%s': %w", name, err)
- c.AddError(renderErr)
- c.ErrorUseHandle(http.StatusInternalServerError, renderErr)
+ // 可以扩展支持其他渲染器接口
}
+ // 默认简单输出,用于未配置 HTMLRender 的情况
+ c.Writer.Write([]byte(fmt.Sprintf("\n
%v
", name, obj)))
}
// Redirect 执行 HTTP 重定向
@@ -336,207 +330,34 @@ func (c *Context) ShouldBindJSON(obj interface{}) error {
if c.Request.Body == nil {
return errors.New("request body is empty")
}
- // defer c.Request.Body.Close() // 通常由调用方或中间件确保关闭,但如果这里是唯一消耗点,可以考虑
-
- var reader io.Reader = c.Request.Body
- if c.engine != nil && c.engine.MaxRequestBodySize > 0 {
- if c.Request.ContentLength != -1 && c.Request.ContentLength > c.engine.MaxRequestBodySize {
- return fmt.Errorf("request body size (%d bytes) exceeds configured limit (%d bytes)", c.Request.ContentLength, c.engine.MaxRequestBodySize)
+ /*
+ decoder := json.NewDecoder(c.Request.Body)
+ if err := decoder.Decode(obj); err != nil {
+ return fmt.Errorf("json binding error: %w", err)
}
- // 注意:http.MaxBytesReader(nil, ...) 中的 nil ResponseWriter 参数意味着当超出限制时,
- // MaxBytesReader 会直接返回错误,而不会尝试写入 HTTP 错误响应。这对于 API 来说是合适的。
- reader = http.MaxBytesReader(nil, c.Request.Body, c.engine.MaxRequestBodySize)
- }
-
- err := json.UnmarshalRead(reader, obj)
+ */
+ err := json.UnmarshalRead(c.Request.Body, obj)
if err != nil {
- // 检查错误类型是否为 http.MaxBytesError
- var maxBytesErr *http.MaxBytesError
- if errors.As(err, &maxBytesErr) {
- return fmt.Errorf("request body size exceeds configured limit (%d bytes): %w", c.engine.MaxRequestBodySize, err)
- }
- // 检查是否是 json 相关的错误,可能需要更细致的错误处理
- // 例如,json.SyntaxError, json.UnmarshalTypeError 等
return fmt.Errorf("json binding error: %w", err)
}
return nil
}
-// ShouldBindXML 尝试将请求体中的 XML 数据绑定到 obj。
-func (c *Context) ShouldBindXML(obj interface{}) error {
- if c.Request == nil || c.Request.Body == nil {
- return errors.New("request body is empty for XML binding")
- }
- // defer c.Request.Body.Close() // Caller is responsible for closing the body
-
- var reader io.Reader = c.Request.Body
- if c.engine != nil && c.engine.MaxRequestBodySize > 0 {
- if c.Request.ContentLength != -1 && c.Request.ContentLength > c.engine.MaxRequestBodySize {
- return fmt.Errorf("request body size (%d bytes) exceeds XML binding limit (%d bytes)", c.Request.ContentLength, c.engine.MaxRequestBodySize)
- }
- reader = http.MaxBytesReader(nil, c.Request.Body, c.engine.MaxRequestBodySize)
- }
-
- decoder := xml.NewDecoder(reader)
- if err := decoder.Decode(obj); err != nil {
- // Check for MaxBytesError specifically
- var maxBytesErr *http.MaxBytesError
- if errors.As(err, &maxBytesErr) {
- return fmt.Errorf("request body size exceeds XML binding limit (%d bytes): %w", c.engine.MaxRequestBodySize, err)
- }
- return fmt.Errorf("xml binding error: %w", err)
- }
- return nil
-}
-
-// ShouldBindQuery 尝试将 URL 查询参数绑定到 obj。
-// 它使用 gorilla/schema 来执行绑定。
-func (c *Context) ShouldBindQuery(obj interface{}) error {
- if c.Request == nil {
- return errors.New("request is nil")
- }
-
- if c.queryCache == nil {
- c.queryCache = c.Request.URL.Query()
- }
- values := c.queryCache
-
- if len(values) == 0 {
- // No query parameters to bind
- return nil
- }
-
- decoder := schema.NewDecoder()
- // decoder.IgnoreUnknownKeys(true) // Optional
-
- if err := decoder.Decode(obj, values); err != nil {
- return fmt.Errorf("query parameter binding error using schema: %w", err)
- }
-
- return nil
-}
-
// ShouldBind 尝试将请求体绑定到各种类型(JSON, Form, XML 等)
-// 根据请求的 Content-Type 自动选择合适的绑定器。
+// 这是一个复杂的通用绑定接口,通常根据 Content-Type 或其他头部来判断绑定方式
+// 预留接口,可根据项目需求进行扩展
func (c *Context) ShouldBind(obj interface{}) error {
- if c.Request == nil {
- return errors.New("request is nil for binding")
- }
-
- // If there's no body, no binding from body can occur.
- if c.Request.Body == nil || c.Request.Body == http.NoBody {
- // Consider if query binding should be attempted for GET requests by default.
- // For now, if no body, assume successful (empty) binding from body perspective.
- return nil
- }
-
- contentType := c.ContentType() // This uses c.GetReqHeader("Content-Type")
- if contentType == "" {
- // If there is a body (ContentLength > 0 or chunked) but no Content-Type, this is an issue.
- if c.Request.ContentLength > 0 || len(c.Request.TransferEncoding) > 0 {
- return errors.New("missing Content-Type header for request body binding")
- }
- // If no Content-Type and no actual body content indicated, effectively no body to bind.
- return nil
- }
-
- mimeType, _, err := mime.ParseMediaType(contentType)
- if err != nil {
- return fmt.Errorf("error parsing Content-Type header '%s': %w", contentType, err)
- }
-
- switch mimeType {
- case "application/json":
- return c.ShouldBindJSON(obj)
- case "application/xml", "text/xml":
- return c.ShouldBindXML(obj)
- case "application/x-www-form-urlencoded":
- return c.ShouldBindForm(obj)
- case "multipart/form-data":
- return c.ShouldBindForm(obj) // ShouldBindForm handles multipart fields
- default:
- return fmt.Errorf("unsupported Content-Type for binding: %s", mimeType)
- }
-}
-
-// ShouldBindForm 尝试将请求体中的表单数据绑定到 obj。
-// 它使用 gorilla/schema 来执行绑定。
-// 注意:此方法期望请求体是 application/x-www-form-urlencoded 或 multipart/form-data 类型。
-// 调用此方法前,应确保请求的 Content-Type 是合适的。
-func (c *Context) ShouldBindForm(obj interface{}) error {
- if c.Request == nil {
- return errors.New("request is nil")
- }
-
- // ParseMultipartForm populates c.Request.PostForm and c.Request.MultipartForm.
- // defaultMemory is used to limit the size of memory used for storing file parts.
- // If the form data exceeds this, it will be stored in temporary files.
- // MaxBytesReader applied earlier (if any) would have limited the total body size.
- if err := c.Request.ParseMultipartForm(defaultMemory); err != nil {
- // Ignore "http: multipart handled by ParseMultipartForm" error, which means it was already parsed.
- // This can happen if a middleware or previous ShouldBind call already parsed the form.
- // Other errors (e.g., I/O errors during parsing) should be returned.
- // Note: Gorilla schema might not need this if it directly uses r.Form or r.PostForm
- // which are populated by ParseForm/ParseMultipartForm.
- // For now, we ensure it's parsed. A more specific check might be needed if this causes issues.
- // A common error to ignore here is `http.ErrNotMultipart` if the content type isn't multipart,
- // as ParseMultipartForm expects that. ParseForm might be more general if we only expect
- // x-www-form-urlencoded, but ParseMultipartForm handles both.
- // Let's proceed and let schema decoder handle empty PostForm if parsing wasn't applicable.
- // However, a direct "request body too large" from MaxBytesReader should have priority if it happened before.
- // This specific error from ParseMultipartForm might relate to parts of a valid multipart form being too large for memory,
- // not the overall request size.
- // For simplicity in this step, we'll return the error unless it's a known "already parsed" scenario (which is not standard).
- // A better approach for "already parsed" would be to check if c.Request.PostForm is already populated.
- if c.formCache == nil && c.Request.PostForm == nil { // Attempt to parse only if not already cached or populated
- if perr := c.Request.ParseMultipartForm(defaultMemory); perr != nil {
- // http.ErrNotMultipart is returned if Content-Type is not multipart/form-data
- // For x-www-form-urlencoded, ParseForm() is implicitly called by accessing PostForm
- // Let's try to populate PostForm if it's not already
- if c.Request.PostForm == nil {
- if perr2 := c.Request.ParseForm(); perr2 != nil {
- return fmt.Errorf("form parse error (ParseForm): %w", perr2)
- }
- }
- // If it was not multipart and ParseForm also failed or PostForm is still nil,
- // then we might have an issue. However, gorilla/schema works on `url.Values`
- // which `c.Request.PostForm` provides.
- // If `Content-Type` was not form-like, `PostForm` would be empty.
- }
- }
- // If `c.formCache` is not nil, `PostForm()` would have already tried parsing.
- // We will use `c.Request.PostForm` which gets populated by `ParseMultipartForm` or `ParseForm`.
- }
-
- // Initialize schema decoder
- decoder := schema.NewDecoder()
- // decoder.IgnoreUnknownKeys(true) // Optional: if you want to ignore fields in form not in struct
-
- // Get form values. c.Request.PostForm includes values from both
- // application/x-www-form-urlencoded and multipart/form-data bodies.
- // It needs to be called after ParseMultipartForm or ParseForm.
- // Accessing c.Request.PostForm itself can trigger ParseForm if not already parsed and content type is x-www-form-urlencoded.
- if err := c.Request.ParseForm(); err != nil && c.Request.PostForm == nil {
- // If ParseForm itself errors and PostForm is still nil, then return error.
- // This ensures that for x-www-form-urlencoded, parsing is attempted.
- // ParseMultipartForm handles multipart, and its error is handled above.
- return fmt.Errorf("form parse error (PostForm init): %w", err)
- }
-
- values := c.Request.PostForm
- if len(values) == 0 {
- // If PostForm is empty, there's nothing to bind from the POST body.
- // This is not necessarily an error for schema.Decode, it will just bind zero values.
- // Depending on requirements, one might want to return an error here if binding is mandatory.
- // For now, we let schema.Decode handle it (it will likely do nothing or bind zero values).
- }
-
- // Decode the form values into the object
- if err := decoder.Decode(obj, values); err != nil {
- return fmt.Errorf("form binding error using schema: %w", err)
- }
-
- return nil
+ // TODO: 完整的通用绑定逻辑
+ // 可以根据 c.Request.Header.Get("Content-Type") 来判断是 JSON, Form, XML 等
+ // 例如:
+ // contentType := c.Request.Header.Get("Content-Type")
+ // if strings.HasPrefix(contentType, "application/json") {
+ // return c.ShouldBindJSON(obj)
+ // }
+ // if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") {
+ // return c.ShouldBindForm(obj) // 需要实现 ShouldBindForm
+ // }
+ return errors.New("generic binding not fully implemented yet, implement based on Content-Type")
}
// AddError 添加一个错误到 Context
@@ -620,31 +441,13 @@ func (c *Context) GetReqBody() io.ReadCloser {
// 注意:请求体只能读取一次
func (c *Context) GetReqBodyFull() ([]byte, error) {
if c.Request.Body == nil {
- return nil, nil // 或者返回一个错误: errors.New("request body is nil")
+ return nil, nil
}
defer c.Request.Body.Close() // 确保请求体被关闭
-
- var reader io.Reader = c.Request.Body
- if c.engine != nil && c.engine.MaxRequestBodySize > 0 {
- if c.Request.ContentLength != -1 && c.Request.ContentLength > c.engine.MaxRequestBodySize {
- err := fmt.Errorf("request body size (%d bytes) exceeds configured limit (%d bytes)", c.Request.ContentLength, c.engine.MaxRequestBodySize)
- c.AddError(err)
- return nil, err
- }
- reader = http.MaxBytesReader(nil, c.Request.Body, c.engine.MaxRequestBodySize)
- }
-
- data, err := io.ReadAll(reader)
+ data, err := io.ReadAll(c.Request.Body)
if err != nil {
- // 检查错误类型是否为 http.MaxBytesError,如果是,则表示超出了限制
- var maxBytesErr *http.MaxBytesError
- if errors.As(err, &maxBytesErr) {
- err = fmt.Errorf("request body size exceeds configured limit (%d bytes): %w", c.engine.MaxRequestBodySize, err)
- } else {
- err = fmt.Errorf("failed to read request body: %w", err)
- }
- c.AddError(err)
- return nil, err
+ c.AddError(fmt.Errorf("failed to read request body: %w", err))
+ return nil, fmt.Errorf("failed to read request body: %w", err)
}
return data, nil
}
@@ -652,30 +455,13 @@ func (c *Context) GetReqBodyFull() ([]byte, error) {
// 类似 GetReqBodyFull, 返回 *bytes.Buffer
func (c *Context) GetReqBodyBuffer() (*bytes.Buffer, error) {
if c.Request.Body == nil {
- return nil, nil // 或者返回一个错误: errors.New("request body is nil")
+ return nil, nil
}
defer c.Request.Body.Close() // 确保请求体被关闭
-
- var reader io.Reader = c.Request.Body
- if c.engine != nil && c.engine.MaxRequestBodySize > 0 {
- if c.Request.ContentLength != -1 && c.Request.ContentLength > c.engine.MaxRequestBodySize {
- err := fmt.Errorf("request body size (%d bytes) exceeds configured limit (%d bytes)", c.Request.ContentLength, c.engine.MaxRequestBodySize)
- c.AddError(err)
- return nil, err
- }
- reader = http.MaxBytesReader(nil, c.Request.Body, c.engine.MaxRequestBodySize)
- }
-
- data, err := io.ReadAll(reader)
+ data, err := io.ReadAll(c.Request.Body)
if err != nil {
- var maxBytesErr *http.MaxBytesError
- if errors.As(err, &maxBytesErr) {
- err = fmt.Errorf("request body size exceeds configured limit (%d bytes): %w", c.engine.MaxRequestBodySize, err)
- } else {
- err = fmt.Errorf("failed to read request body: %w", err)
- }
- c.AddError(err)
- return nil, err
+ c.AddError(fmt.Errorf("failed to read request body: %w", err))
+ return nil, fmt.Errorf("failed to read request body: %w", err)
}
return bytes.NewBuffer(data), nil
}
diff --git a/context_test.go b/context_test.go
deleted file mode 100644
index f470b9f..0000000
--- a/context_test.go
+++ /dev/null
@@ -1,1491 +0,0 @@
-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.
diff --git a/engine.go b/engine.go
index 7a01815..ff47fc3 100644
--- a/engine.go
+++ b/engine.go
@@ -9,31 +9,15 @@ import (
"runtime"
"strings"
- "html/template"
- "io"
"net/http"
"path"
+
"sync"
"github.com/WJQSERVER-STUDIO/httpc"
"github.com/fenthope/reco"
)
-// HTMLRender defines the interface for HTML rendering.
-type HTMLRender interface {
- Render(writer io.Writer, name string, data interface{}, c *Context) error
-}
-
-// DefaultHTMLRenderer is a basic implementation of HTMLRender using html/template.
-type DefaultHTMLRenderer struct {
- Templates *template.Template
-}
-
-// Render executes the template and writes to the writer.
-func (r *DefaultHTMLRenderer) Render(writer io.Writer, name string, data interface{}, c *Context) error {
- return r.Templates.ExecuteTemplate(writer, name, data)
-}
-
// Last 返回链中的最后一个处理函数
// 如果链为空,则返回 nil
func (c HandlersChain) Last() HandlerFunc {
@@ -66,7 +50,7 @@ type Engine struct {
LogReco *reco.Logger
- HTMLRender HTMLRender // 用于 HTML 模板渲染
+ HTMLRender interface{} // 用于 HTML 模板渲染,可以设置为 *template.Template 或自定义渲染器接口
routesInfo []RouteInfo // 存储所有注册的路由信息
@@ -90,8 +74,6 @@ type Engine struct {
// 如果设置了此回调,它将优先于 ServerConfigurator 被用于 HTTPS 服务器
// 如果未设置,HTTPS 服务器将回退使用 ServerConfigurator (如果已设置)
TLSServerConfigurator func(*http.Server)
-
- MaxRequestBodySize int64 // 限制读取Body的最大字节数
}
type ErrorHandle struct {
@@ -105,39 +87,12 @@ type ErrorHandler func(c *Context, code int, err error)
func defaultErrorHandle(c *Context, code int, err error) { // 检查客户端是否已断开连接
select {
case <-c.Request.Context().Done():
- // 客户端断开连接,无需进一步处理
+
return
default:
- // 检查响应是否已经写入
if c.Writer.Written() {
return
}
-
- // 收集错误信息用于日志记录
- primaryErrStr := "none"
- if err != nil {
- primaryErrStr = err.Error()
- }
-
- var collectedErrors []string
- for _, e := range c.GetErrors() {
- collectedErrors = append(collectedErrors, e.Error())
- }
- collectedErrorsStr := strings.Join(collectedErrors, "; ")
- if collectedErrorsStr == "" {
- collectedErrorsStr = "none"
- }
-
- // 记录错误日志
- logMessage := fmt.Sprintf("[Touka ErrorHandler] Request: [%s] %s | Primary Error: %s | Collected Errors: %s",
- c.Request.Method, c.Request.URL.Path, primaryErrStr, collectedErrorsStr)
-
- if c.engine != nil && c.engine.LogReco != nil {
- c.engine.LogReco.Error(logMessage)
- } else {
- log.Println(logMessage) // Fallback to standard logger
- }
-
// 输出json 状态码与状态码对应描述
var errMsg string
if err != nil {
@@ -205,7 +160,6 @@ func New() *Engine {
noRoutes: make(HandlersChain, 0),
ServerConfigurator: nil,
TLSServerConfigurator: nil,
- MaxRequestBodySize: 10 * 1024 * 1024, // 默认 10MB
}
//engine.SetProtocols(GetDefaultProtocolsConfig())
engine.SetDefaultProtocols()
@@ -235,23 +189,6 @@ func Default() *Engine {
// === 外部操作方法 ===
-// LoadHTMLGlob loads HTML templates from a glob pattern and sets them as the HTML renderer.
-func (engine *Engine) LoadHTMLGlob(pattern string) {
- tpl := template.Must(template.ParseGlob(pattern))
- engine.HTMLRender = &DefaultHTMLRenderer{Templates: tpl}
-}
-
-// SetHTMLTemplate sets a custom *template.Template as the HTML renderer.
-// This will wrap the *template.Template with the DefaultHTMLRenderer.
-func (engine *Engine) SetHTMLTemplate(tpl *template.Template) {
- engine.HTMLRender = &DefaultHTMLRenderer{Templates: tpl}
-}
-
-// SetMaxRequestBodySize 设置读取Body的最大字节数
-func (engine *Engine) SetMaxRequestBodySize(size int64) {
- engine.MaxRequestBodySize = size
-}
-
// SetServerConfigurator 设置一个函数,该函数将在任何 HTTP 或 HTTPS 服务器
// (通过 RunShutdown, RunTLS, RunTLSRedir) 启动前被调用,
// 允许用户对底层的 *http.Server 实例进行自定义配置
diff --git a/go.mod b/go.mod
index ab4d4e8..74c947b 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,4 @@ require (
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8
)
-require (
- github.com/gorilla/schema v1.4.1 // indirect
- github.com/valyala/bytebufferpool v1.0.0 // indirect
-)
+require github.com/valyala/bytebufferpool v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index 6e077ab..22fb00b 100644
--- a/go.sum
+++ b/go.sum
@@ -6,7 +6,5 @@ github.com/fenthope/reco v0.0.3 h1:RmnQ0D9a8PWtwOODawitTe4BztTnS9wYwrDbipISNq4=
github.com/fenthope/reco v0.0.3/go.mod h1:mDkGLHte5udWTIcjQTxrABRcf56SSdxBOCLgrRDwI/Y=
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk=
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
-github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
-github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=