Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package engine

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
Expand Down Expand Up @@ -306,6 +308,50 @@ func (ctx *Context) Write(data []byte) (int, error) {
return ctx.res.Write(data)
}

// String writes a string to the response body.
func (ctx *Context) String(s string) (int, error) {
if ctx.GetHeader("Content-Type") == "" {
ctx.SetHeader("Content-Type", "text/plain; charset=utf-8")
}

return ctx.res.Write([]byte(s))
}

// JSON serializes the given value to JSON and writes it to the response body.
func (ctx *Context) JSON(v any) (int, error) {
jsonData, err := json.Marshal(v)
if err != nil {
return 0, err
}

if ctx.GetHeader("Content-Type") == "" {
ctx.SetHeader("Content-Type", "application/json")
}

return ctx.res.Write(jsonData)
}

// Redirect sets the Location header and writes a redirect response with the specified status code,
// defaulting to 302 Found if no code is provided.
func (ctx *Context) Redirect(link string, code ...int) error {
statusCode := http.StatusFound
if len(code) > 0 {
statusCode = code[0]
}
switch statusCode {
case http.StatusMultipleChoices, http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther,
http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
// the status code is always valid
_ = ctx.Status(statusCode)
default:
return fmt.Errorf("invalid redirect status code: %d", statusCode)
}

ctx.SetHeader("Location", link)

return nil
}
Comment on lines +336 to +353

// Request returns the wrapped Request object associated with the context.
func (ctx *Context) Request() core.Request {
return ctx.req
Expand Down
251 changes: 251 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,257 @@ func TestContext_Write(t *testing.T) {
}
}

func TestContext_String(t *testing.T) {
rr := httptest.NewRecorder()
resp := NewResponse(rr)
ctx := getDefaultContext(nil, resp)
n, err := ctx.String("hello")
if err != nil {
t.Fatalf("String returned error: %v", err)
}
if n != 5 {
t.Errorf("Expected String to write 5 bytes, got %d", n)
}

resp.send()

if rr.Body.String() != "hello" {
t.Errorf("Expected sent body 'hello', got %v", rr.Body.String())
}
if rr.Header().Get("Content-Type") != "text/plain; charset=utf-8" {
t.Errorf("Expected Content-Type 'text/plain; charset=utf-8', got %v", rr.Header().Get("Content-Type"))
}
}

func TestContext_String_PreserveContentType(t *testing.T) {
rr := httptest.NewRecorder()
resp := NewResponse(rr)
ctx := getDefaultContext(nil, resp)
ctx.SetHeader("Content-Type", "text/html; charset=utf-8")
n, err := ctx.String("<p>hello</p>")
if err != nil {
t.Fatalf("String returned error: %v", err)
}
if n != 12 {
t.Errorf("Expected String to write 12 bytes, got %d", n)
}

resp.send()

if rr.Header().Get("Content-Type") != "text/html; charset=utf-8" {
t.Errorf("Expected Content-Type to be preserved as 'text/html; charset=utf-8', got %v", rr.Header().Get("Content-Type"))
}
}

func TestContext_JSON(t *testing.T) {
rr := httptest.NewRecorder()
resp := NewResponse(rr)
ctx := getDefaultContext(nil, resp)
data := map[string]string{"key": "value"}
n, err := ctx.JSON(data)
if err != nil {
t.Fatalf("JSON returned error: %v", err)
}
expectedJSON := `{"key":"value"}`
if n != len(expectedJSON) {
t.Errorf("Expected JSON to write %d bytes, got %d", len(expectedJSON), n)
}

resp.send()

if rr.Body.String() != expectedJSON {
t.Errorf("Expected sent body '%s', got '%s'", expectedJSON, rr.Body.String())
}
if rr.Header().Get("Content-Type") != "application/json" {
t.Errorf("Expected Content-Type 'application/json', got %v", rr.Header().Get("Content-Type"))
}
}

func TestContext_JSON_PreserveContentType(t *testing.T) {
rr := httptest.NewRecorder()
resp := NewResponse(rr)
ctx := getDefaultContext(nil, resp)
ctx.SetHeader("Content-Type", "application/json; charset=utf-8")
data := map[string]string{"key": "value"}
_, err := ctx.JSON(data)
if err != nil {
t.Fatalf("JSON returned error: %v", err)
}

resp.send()

if rr.Header().Get("Content-Type") != "application/json; charset=utf-8" {
t.Errorf("Expected Content-Type to be preserved as 'application/json; charset=utf-8', got %v", rr.Header().Get("Content-Type"))
}
}

func TestContext_JSON_MarshalError(t *testing.T) {
resp := NewResponse(httptest.NewRecorder())
ctx := getDefaultContext(nil, resp)
// channel and func types cannot be marshaled to JSON
_, err := ctx.JSON(make(chan int))
if err == nil {
t.Fatalf("Expected JSON to return error for unmarshalable type")
}
Comment on lines +713 to +716
}

func TestContext_String_Empty(t *testing.T) {
rr := httptest.NewRecorder()
resp := NewResponse(rr)
ctx := getDefaultContext(nil, resp)
n, err := ctx.String("")
if err != nil {
t.Fatalf("String returned error: %v", err)
}
if n != 0 {
t.Errorf("Expected String to write 0 bytes, got %d", n)
}

resp.send()

if rr.Body.String() != "" {
t.Errorf("Expected empty body, got '%s'", rr.Body.String())
}
if rr.Header().Get("Content-Type") != "text/plain; charset=utf-8" {
t.Errorf("Expected Content-Type 'text/plain; charset=utf-8', got %v", rr.Header().Get("Content-Type"))
}
}

func TestContext_JSON_Nil(t *testing.T) {
rr := httptest.NewRecorder()
resp := NewResponse(rr)
ctx := getDefaultContext(nil, resp)
n, err := ctx.JSON(nil)
if err != nil {
t.Fatalf("JSON returned error: %v", err)
}
expectedJSON := "null"
if n != len(expectedJSON) {
t.Errorf("Expected JSON to write %d bytes, got %d", len(expectedJSON), n)
}

resp.send()

if rr.Body.String() != expectedJSON {
t.Errorf("Expected sent body 'null', got '%s'", rr.Body.String())
}
if rr.Header().Get("Content-Type") != "application/json" {
t.Errorf("Expected Content-Type 'application/json', got %v", rr.Header().Get("Content-Type"))
}
}

func TestContext_JSON_MarshalError_NoContentTypeSideEffect(t *testing.T) {
resp := NewResponse(httptest.NewRecorder())
ctx := getDefaultContext(nil, resp)
// channel types cannot be marshaled — Content-Type must NOT be set on failure
_, err := ctx.JSON(make(chan int))
if err == nil {
t.Fatalf("Expected JSON to return error for unmarshalable type")
}
if ctx.GetHeader("Content-Type") != "" {
t.Errorf("Expected no Content-Type header to be set on marshal error, got '%s'", ctx.GetHeader("Content-Type"))
}
}

func TestContext_Redirect(t *testing.T) {
rr := httptest.NewRecorder()
resp := NewResponse(rr)
ctx := getDefaultContext(nil, resp)

err := ctx.Redirect("https://example.com", http.StatusMovedPermanently)
if err != nil {
t.Fatalf("Redirect returned error: %v", err)
}

resp.send()

if rr.Header().Get("Location") != "https://example.com" {
t.Errorf("Expected Location header to be set to 'https://example.com', got %v", rr.Header().Get("Location"))
}
if rr.Code != http.StatusMovedPermanently {
t.Errorf("Expected status code %d, got %d", http.StatusMovedPermanently, rr.Code)
}
}

func TestContext_Redirect_DefaultStatus(t *testing.T) {
rr := httptest.NewRecorder()
resp := NewResponse(rr)
ctx := getDefaultContext(nil, resp)

err := ctx.Redirect("https://example.com")
if err != nil {
t.Fatalf("Redirect returned error: %v", err)
}

resp.send()

if rr.Header().Get("Location") != "https://example.com" {
t.Errorf("Expected Location header to be set to 'https://example.com', got %v", rr.Header().Get("Location"))
}
if rr.Code != http.StatusFound {
t.Errorf("Expected status code %d, got %d", http.StatusFound, rr.Code)
}
}

func TestContext_Redirect_InvalidCode(t *testing.T) {
rr := httptest.NewRecorder()
resp := NewResponse(rr)
ctx := getDefaultContext(nil, resp)

err := ctx.Redirect("https://example.com", http.StatusBadRequest)
if err == nil {
t.Fatalf("Expected Redirect to return error for non-redirect status code 400")
}

err = ctx.Redirect("https://example.com", 200)
if err == nil {
t.Fatalf("Expected Redirect to return error for non-redirect status code 200")
}

err = ctx.Redirect("https://example.com", 500)
if err == nil {
t.Fatalf("Expected Redirect to return error for non-redirect status code 500")
}

// Verify no headers were set on error
if rr.Header().Get("Location") != "" {
t.Errorf("Expected no Location header to be set on error")
}
}

func TestContext_Redirect_BodyOnStatus(t *testing.T) {
tests := []struct {
code int
statusText string
}{
{http.StatusMultipleChoices, "Multiple Choices"},
{http.StatusMovedPermanently, "Moved Permanently"},
{http.StatusFound, "Found"},
{http.StatusSeeOther, "See Other"},
{http.StatusTemporaryRedirect, "Temporary Redirect"},
{http.StatusPermanentRedirect, "Permanent Redirect"},
}

for _, tt := range tests {
t.Run(tt.statusText, func(t *testing.T) {
rr := httptest.NewRecorder()
resp := NewResponse(rr)
ctx := getDefaultContext(nil, resp)

err := ctx.Redirect("https://example.com/path", tt.code)
if err != nil {
t.Fatalf("Redirect returned error: %v", err)
}

resp.send()

if rr.Code != tt.code {
t.Errorf("Expected status code %d, got %d", tt.code, rr.Code)
}
})
}
}

func getDefaultContext(r *http.Request, res *Response) *engine.Context {
if r == nil {
r = httptest.NewRequest(http.MethodGet, "/", nil)
Expand Down