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=