This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestCreateBackgroundAppContext(t *testing.T) {
|
||||
ac := CreateBackgroundAppContext()
|
||||
if ac == nil {
|
||||
t.Fatalf("expected non-nil context")
|
||||
}
|
||||
if ac.GinContext != nil {
|
||||
t.Fatalf("background context should have no gin context")
|
||||
}
|
||||
if ac.Err() != nil {
|
||||
t.Fatalf("expected no error")
|
||||
}
|
||||
if _, ok := ac.Deadline(); ok {
|
||||
t.Fatalf("background context should have no deadline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAppContext_CopiesGinKeys(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
g, _ := gin.CreateTestContext(rec)
|
||||
g.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
g.Set("foo", "bar")
|
||||
g.Set("num", 42)
|
||||
|
||||
inner, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ac := CreateAppContext(g, inner, cancel)
|
||||
|
||||
if ac.Value("foo") != "bar" {
|
||||
t.Fatalf("expected key foo to be copied")
|
||||
}
|
||||
if ac.Value("num") != 42 {
|
||||
t.Fatalf("expected key num to be copied")
|
||||
}
|
||||
if ac.GinContext != g {
|
||||
t.Fatalf("expected GinContext to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppContext_Set(t *testing.T) {
|
||||
ac := CreateBackgroundAppContext()
|
||||
ac.Set("k", "v")
|
||||
if ac.Value("k") != "v" {
|
||||
t.Fatalf("expected Set to store value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppContext_Cancel(t *testing.T) {
|
||||
called := false
|
||||
cancel := func() { called = true }
|
||||
ac := &AppContext{
|
||||
inner: context.Background(),
|
||||
cancelFunc: cancel,
|
||||
}
|
||||
ac.Cancel()
|
||||
if !called {
|
||||
t.Fatalf("expected cancel function to be invoked")
|
||||
}
|
||||
if !ac.cancelled {
|
||||
t.Fatalf("expected cancelled flag set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppContext_DeadlineDoneErr(t *testing.T) {
|
||||
deadline := time.Now().Add(1 * time.Hour)
|
||||
inner, cancel := context.WithDeadline(context.Background(), deadline)
|
||||
defer cancel()
|
||||
|
||||
ac := &AppContext{inner: inner, cancelFunc: cancel}
|
||||
d, ok := ac.Deadline()
|
||||
if !ok {
|
||||
t.Fatalf("expected deadline ok")
|
||||
}
|
||||
if !d.Equal(deadline) {
|
||||
t.Fatalf("deadline mismatch")
|
||||
}
|
||||
if ac.Done() == nil {
|
||||
t.Fatalf("expected non-nil Done channel")
|
||||
}
|
||||
if ac.Err() != nil {
|
||||
t.Fatalf("expected no err yet")
|
||||
}
|
||||
|
||||
cancel()
|
||||
// After cancel, Err should return Canceled
|
||||
if !errors.Is(ac.Err(), context.Canceled) {
|
||||
t.Fatalf("expected context.Canceled, got %v", ac.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppContext_RequestURI(t *testing.T) {
|
||||
bg := CreateBackgroundAppContext()
|
||||
if bg.RequestURI() != "" {
|
||||
t.Fatalf("expected empty for background context")
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
g, _ := gin.CreateTestContext(rec)
|
||||
g.Request = httptest.NewRequest(http.MethodPost, "/foo/bar", nil)
|
||||
|
||||
inner, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ac := CreateAppContext(g, inner, cancel)
|
||||
|
||||
uri := ac.RequestURI()
|
||||
if uri != "POST :: /foo/bar" {
|
||||
t.Fatalf("expected POST :: /foo/bar, got %q", uri)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRedirectFound(t *testing.T) {
|
||||
hf := RedirectFound("/x")
|
||||
resp := hf(PreContext{})
|
||||
if resp == nil {
|
||||
t.Fatalf("expected response")
|
||||
}
|
||||
if resp.(InspectableHTTPResponse).Statuscode() != http.StatusFound {
|
||||
t.Fatalf("expected 302")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectTemporary(t *testing.T) {
|
||||
hf := RedirectTemporary("/x")
|
||||
resp := hf(PreContext{})
|
||||
if resp.(InspectableHTTPResponse).Statuscode() != http.StatusTemporaryRedirect {
|
||||
t.Fatalf("expected 307")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectPermanent(t *testing.T) {
|
||||
hf := RedirectPermanent("/x")
|
||||
resp := hf(PreContext{})
|
||||
if resp.(InspectableHTTPResponse).Statuscode() != http.StatusPermanentRedirect {
|
||||
t.Fatalf("expected 308")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestBodyBuffer_WrapsBody(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/", strings.NewReader("payload"))
|
||||
|
||||
original := c.Request.Body
|
||||
BodyBuffer(c)
|
||||
if c.Request.Body == original {
|
||||
t.Fatalf("expected body to be replaced with buffered reader")
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read err: %v", err)
|
||||
}
|
||||
if !bytes.Equal(data, []byte("payload")) {
|
||||
t.Fatalf("body mismatch: %q", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodyBuffer_NilBody(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
c.Request.Body = nil
|
||||
|
||||
// Should not panic
|
||||
BodyBuffer(c)
|
||||
if c.Request.Body != nil {
|
||||
t.Fatalf("expected nil body to remain nil")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestCorsMiddleware_SetsHeaders(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
mw := CorsMiddleware([]string{"X-Foo", "X-Bar"}, []string{"X-Exposed"})
|
||||
mw(c)
|
||||
|
||||
h := rec.Header()
|
||||
if h.Get("Access-Control-Allow-Origin") != "*" {
|
||||
t.Fatalf("expected Allow-Origin *")
|
||||
}
|
||||
if h.Get("Access-Control-Allow-Credentials") != "true" {
|
||||
t.Fatalf("expected Allow-Credentials true")
|
||||
}
|
||||
if h.Get("Access-Control-Allow-Headers") != "X-Foo, X-Bar" {
|
||||
t.Fatalf("expected Allow-Headers X-Foo, X-Bar got %q", h.Get("Access-Control-Allow-Headers"))
|
||||
}
|
||||
if h.Get("Access-Control-Expose-Headers") != "X-Exposed" {
|
||||
t.Fatalf("expected Expose-Headers X-Exposed got %q", h.Get("Access-Control-Expose-Headers"))
|
||||
}
|
||||
allowMethods := h.Get("Access-Control-Allow-Methods")
|
||||
for _, want := range []string{"OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE", "COUNT"} {
|
||||
if !strings.Contains(allowMethods, want) {
|
||||
t.Errorf("expected Allow-Methods to contain %q, got %q", want, allowMethods)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorsMiddleware_NoExposeHeader(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
mw := CorsMiddleware([]string{"X-Foo"}, []string{})
|
||||
mw(c)
|
||||
|
||||
if _, ok := rec.Header()["Access-Control-Expose-Headers"]; ok {
|
||||
t.Fatalf("expected Expose-Headers to be unset when empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorsMiddleware_OptionsAborts(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodOptions, "/", nil)
|
||||
|
||||
mw := CorsMiddleware([]string{"X-Foo"}, nil)
|
||||
mw(c)
|
||||
|
||||
if !c.IsAborted() {
|
||||
t.Fatalf("expected context aborted on OPTIONS")
|
||||
}
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 on OPTIONS, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorsMiddleware_NonOptionsContinues(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
mw := CorsMiddleware([]string{"X-Foo"}, nil)
|
||||
mw(c)
|
||||
|
||||
if c.IsAborted() {
|
||||
t.Fatalf("non-OPTIONS request should not be aborted")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestNewEngine_DefaultsApplied(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
if w == nil {
|
||||
t.Fatalf("expected non-nil wrapper")
|
||||
}
|
||||
if w.engine == nil {
|
||||
t.Fatalf("expected gin engine")
|
||||
}
|
||||
if w.allowCors {
|
||||
t.Fatalf("expected allowCors default false")
|
||||
}
|
||||
if w.bufferBody {
|
||||
t.Fatalf("expected bufferBody default false")
|
||||
}
|
||||
if !w.ginDebug {
|
||||
t.Fatalf("expected ginDebug default true")
|
||||
}
|
||||
if w.requestTimeout != 24*time.Hour {
|
||||
t.Fatalf("expected default 24h timeout, got %s", w.requestTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEngine_OptionsHonored(t *testing.T) {
|
||||
allowCors := true
|
||||
bufferBody := true
|
||||
suppress := true
|
||||
debug := false
|
||||
timeout := 5 * time.Second
|
||||
|
||||
w := NewEngine(Options{
|
||||
AllowCors: &allowCors,
|
||||
BufferBody: &bufferBody,
|
||||
SuppressGinLogs: &suppress,
|
||||
GinDebug: &debug,
|
||||
Timeout: &timeout,
|
||||
CorsAllowHeader: &[]string{"X-Custom"},
|
||||
})
|
||||
|
||||
if !w.allowCors {
|
||||
t.Fatalf("allowCors")
|
||||
}
|
||||
if !w.bufferBody {
|
||||
t.Fatalf("bufferBody")
|
||||
}
|
||||
if !w.suppressGinLogs {
|
||||
t.Fatalf("suppressGinLogs")
|
||||
}
|
||||
if w.ginDebug {
|
||||
t.Fatalf("ginDebug should be false")
|
||||
}
|
||||
if w.requestTimeout != timeout {
|
||||
t.Fatalf("timeout mismatch")
|
||||
}
|
||||
if !langext.ArrEqualsExact(w.corsAllowHeader, []string{"X-Custom"}) {
|
||||
t.Fatalf("expected custom allow header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEngine_BuildRequestBindError_DefaultIsErrorWrapper(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
if w.buildRequestBindError == nil {
|
||||
t.Fatalf("expected default builder")
|
||||
}
|
||||
resp := w.buildRequestBindError(nil, "URI", http.ErrAbortHandler)
|
||||
if resp == nil {
|
||||
t.Fatalf("expected response")
|
||||
}
|
||||
if resp.IsSuccess() {
|
||||
t.Fatalf("expected error response, not success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEngine_BuildRequestBindError_Custom(t *testing.T) {
|
||||
called := false
|
||||
custom := func(c *gin.Context, fieldtype string, err error) HTTPResponse {
|
||||
called = true
|
||||
return Status(http.StatusTeapot)
|
||||
}
|
||||
_ = custom // referenced below to avoid unused warning if signature mismatch
|
||||
w := NewEngine(Options{BuildRequestBindError: custom})
|
||||
resp := w.buildRequestBindError(nil, "URI", http.ErrAbortHandler)
|
||||
if !called {
|
||||
t.Fatalf("expected custom builder to be invoked")
|
||||
}
|
||||
if resp.(InspectableHTTPResponse).Statuscode() != http.StatusTeapot {
|
||||
t.Fatalf("expected 418 from custom builder")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeHTTP_RoundTrip(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.Routes().GET("/hello").Handle(func(p PreContext) HTTPResponse {
|
||||
return Text(http.StatusOK, "world")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/hello", nil)
|
||||
rec := w.ServeHTTP(req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
if rec.Body.String() != "world" {
|
||||
t.Fatalf("expected world, got %q", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestForwardRequest(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.Routes().GET("/fwd").Handle(func(p PreContext) HTTPResponse {
|
||||
return Text(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/fwd", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
w.ForwardRequest(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRoutes(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.Routes().GET("/a").Handle(func(p PreContext) HTTPResponse { return Status(200) })
|
||||
w.Routes().POST("/b").Handle(func(p PreContext) HTTPResponse { return Status(200) })
|
||||
|
||||
rs := w.ListRoutes()
|
||||
if len(rs) < 2 {
|
||||
t.Fatalf("expected at least 2 routes, got %d", len(rs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugPrintRoutes_NoPanic(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.Routes().GET("/x").Handle(func(p PreContext) HTTPResponse { return Status(200) })
|
||||
// just verify it doesn't panic
|
||||
w.DebugPrintRoutes()
|
||||
}
|
||||
|
||||
func TestCleanMiddlewareName(t *testing.T) {
|
||||
w := NewEngine(Options{
|
||||
DebugTrimHandlerPrefixes: []string{"customprefix."},
|
||||
DebugReplaceHandlerNames: map[string]string{"BadName": "GoodName"},
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"ginext.BodyBuffer", "[BodyBuffer]"},
|
||||
{"foo.(*GinRoutesWrapper).WithJSONFilter", "[JSONFilter]"},
|
||||
{"ginext.someThing", "someThing"},
|
||||
{"api.someThing", "someThing"},
|
||||
{"customprefix.thing", "thing"},
|
||||
{"BadName", "GoodName"},
|
||||
{"badname", "GoodName"},
|
||||
{"some.pkg.Func.func1", "some.pkg.Func"},
|
||||
{"some.pkg.Func.func1.2", "some.pkg.Func"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := w.cleanMiddlewareName(tc.in); got != tc.want {
|
||||
t.Errorf("cleanMiddlewareName(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestSetJSONFilter_StoresInContext(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
SetJSONFilter(c, "myfilter")
|
||||
|
||||
v := c.GetString(jsonFilterKey)
|
||||
if v != "myfilter" {
|
||||
t.Fatalf("expected filter to be stored, got %q", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetJSONFilter_Empty(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
SetJSONFilter(c, "")
|
||||
|
||||
v := c.GetString(jsonFilterKey)
|
||||
if v != "" {
|
||||
t.Fatalf("expected empty filter")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
|
||||
func newTestCtx() (*gin.Context, *httptest.ResponseRecorder) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
return c, rec
|
||||
}
|
||||
|
||||
func TestJSONResponse_Basics(t *testing.T) {
|
||||
r := JSON(http.StatusOK, map[string]any{"a": 1})
|
||||
if !r.IsSuccess() {
|
||||
t.Fatalf("expected IsSuccess true for 200")
|
||||
}
|
||||
ir, ok := r.(InspectableHTTPResponse)
|
||||
if !ok {
|
||||
t.Fatalf("expected InspectableHTTPResponse")
|
||||
}
|
||||
if ir.Statuscode() != http.StatusOK {
|
||||
t.Fatalf("statuscode mismatch: %d", ir.Statuscode())
|
||||
}
|
||||
if ir.ContentType() != "application/json" {
|
||||
t.Fatalf("expected content-type application/json, got %q", ir.ContentType())
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONResponse_Write(t *testing.T) {
|
||||
c, rec := newTestCtx()
|
||||
r := JSON(http.StatusCreated, map[string]any{"hello": "world"})
|
||||
r.Write(c)
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d", rec.Code)
|
||||
}
|
||||
if !bytes.Contains(rec.Body.Bytes(), []byte("\"hello\":\"world\"")) {
|
||||
t.Fatalf("unexpected body: %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONResponse_BodyString(t *testing.T) {
|
||||
c, _ := newTestCtx()
|
||||
r := JSON(http.StatusOK, map[string]any{"x": 42})
|
||||
ir := r.(InspectableHTTPResponse)
|
||||
body := ir.BodyString(c)
|
||||
if body == nil {
|
||||
t.Fatalf("expected body, got nil")
|
||||
}
|
||||
if !bytes.Contains([]byte(*body), []byte("\"x\":42")) {
|
||||
t.Fatalf("unexpected body: %q", *body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONResponse_FilterFromContext(t *testing.T) {
|
||||
c, _ := newTestCtx()
|
||||
SetJSONFilter(c, "abc")
|
||||
r := JSON(http.StatusOK, map[string]any{"x": 1})
|
||||
body := r.(InspectableHTTPResponse).BodyString(c)
|
||||
if body == nil {
|
||||
t.Fatalf("expected body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONResponse_FilterOverride(t *testing.T) {
|
||||
c, _ := newTestCtx()
|
||||
SetJSONFilter(c, "abc")
|
||||
r := JSONWithFilter(http.StatusOK, map[string]any{"x": 1}, "override")
|
||||
if r == nil {
|
||||
t.Fatalf("expected response")
|
||||
}
|
||||
if !r.IsSuccess() {
|
||||
t.Fatalf("expected success")
|
||||
}
|
||||
body := r.(InspectableHTTPResponse).BodyString(c)
|
||||
if body == nil {
|
||||
t.Fatalf("expected body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponse_IsSuccessRanges(t *testing.T) {
|
||||
cases := []struct {
|
||||
code int
|
||||
ok bool
|
||||
}{
|
||||
{100, false},
|
||||
{199, false},
|
||||
{200, true},
|
||||
{201, true},
|
||||
{299, true},
|
||||
{300, true},
|
||||
{399, true},
|
||||
{400, false},
|
||||
{500, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
r := JSON(tc.code, nil)
|
||||
if r.IsSuccess() != tc.ok {
|
||||
t.Errorf("status %d: expected IsSuccess=%v", tc.code, tc.ok)
|
||||
}
|
||||
r2 := Status(tc.code)
|
||||
if r2.IsSuccess() != tc.ok {
|
||||
t.Errorf("Status(%d): expected IsSuccess=%v", tc.code, tc.ok)
|
||||
}
|
||||
r3 := Text(tc.code, "x")
|
||||
if r3.IsSuccess() != tc.ok {
|
||||
t.Errorf("Text(%d): expected IsSuccess=%v", tc.code, tc.ok)
|
||||
}
|
||||
r4 := Data(tc.code, "text/plain", []byte("x"))
|
||||
if r4.IsSuccess() != tc.ok {
|
||||
t.Errorf("Data(%d): expected IsSuccess=%v", tc.code, tc.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponse_WithHeader(t *testing.T) {
|
||||
r := JSON(http.StatusOK, nil).
|
||||
WithHeader("X-Foo", "bar").
|
||||
WithHeader("X-Baz", "qux")
|
||||
headers := r.(InspectableHTTPResponse).Headers()
|
||||
if len(headers) != 2 {
|
||||
t.Fatalf("expected 2 headers, got %d", len(headers))
|
||||
}
|
||||
if headers[0] != "X-Foo=bar" || headers[1] != "X-Baz=qux" {
|
||||
t.Fatalf("headers wrong: %v", headers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponse_WithCookie_DoesNotPanic(t *testing.T) {
|
||||
r := JSON(http.StatusOK, nil).
|
||||
WithCookie("session", "abc", 3600, "/", "example.com", true, true)
|
||||
if r == nil {
|
||||
t.Fatalf("expected response")
|
||||
}
|
||||
c, rec := newTestCtx()
|
||||
r.Write(c)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
cookies := rec.Result().Cookies()
|
||||
if len(cookies) != 1 {
|
||||
t.Fatalf("expected 1 cookie, got %d", len(cookies))
|
||||
}
|
||||
if cookies[0].Name != "session" || cookies[0].Value != "abc" {
|
||||
t.Fatalf("cookie wrong: %+v", cookies[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextResponse(t *testing.T) {
|
||||
c, rec := newTestCtx()
|
||||
r := Text(http.StatusOK, "hello")
|
||||
if r.(InspectableHTTPResponse).ContentType() != "text/plain" {
|
||||
t.Fatalf("expected text/plain")
|
||||
}
|
||||
body := r.(InspectableHTTPResponse).BodyString(c)
|
||||
if body == nil || *body != "hello" {
|
||||
t.Fatalf("body mismatch")
|
||||
}
|
||||
r.Write(c)
|
||||
if rec.Body.String() != "hello" {
|
||||
t.Fatalf("write body mismatch: %q", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataResponse(t *testing.T) {
|
||||
c, rec := newTestCtx()
|
||||
payload := []byte{0x01, 0x02, 0x03}
|
||||
r := Data(http.StatusOK, "application/octet-stream", payload)
|
||||
if r.(InspectableHTTPResponse).ContentType() != "application/octet-stream" {
|
||||
t.Fatalf("contenttype mismatch")
|
||||
}
|
||||
r.Write(c)
|
||||
if !bytes.Equal(rec.Body.Bytes(), payload) {
|
||||
t.Fatalf("body mismatch")
|
||||
}
|
||||
body := r.(InspectableHTTPResponse).BodyString(c)
|
||||
if body == nil || *body != string(payload) {
|
||||
t.Fatalf("BodyString mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusResponse(t *testing.T) {
|
||||
c, rec := newTestCtx()
|
||||
r := Status(http.StatusNoContent)
|
||||
if r.(InspectableHTTPResponse).ContentType() != "" {
|
||||
t.Fatalf("expected empty content type")
|
||||
}
|
||||
if r.(InspectableHTTPResponse).BodyString(c) != nil {
|
||||
t.Fatalf("expected nil body")
|
||||
}
|
||||
r.Write(c)
|
||||
if c.Writer.Status() != http.StatusNoContent {
|
||||
t.Fatalf("expected 204, got %d", c.Writer.Status())
|
||||
}
|
||||
_ = rec
|
||||
}
|
||||
|
||||
func TestRedirectResponse(t *testing.T) {
|
||||
c, rec := newTestCtx()
|
||||
r := Redirect(http.StatusFound, "/elsewhere")
|
||||
if r.(InspectableHTTPResponse).Statuscode() != http.StatusFound {
|
||||
t.Fatalf("status mismatch")
|
||||
}
|
||||
if r.(InspectableHTTPResponse).ContentType() != "" {
|
||||
t.Fatalf("expected empty content type")
|
||||
}
|
||||
if r.(InspectableHTTPResponse).BodyString(c) != nil {
|
||||
t.Fatalf("expected nil body")
|
||||
}
|
||||
r.Write(c)
|
||||
if rec.Code != http.StatusFound {
|
||||
t.Fatalf("expected 302, got %d", rec.Code)
|
||||
}
|
||||
if rec.Header().Get("Location") != "/elsewhere" {
|
||||
t.Fatalf("expected Location header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotImplemented(t *testing.T) {
|
||||
r := NotImplemented()
|
||||
if r == nil {
|
||||
t.Fatalf("expected response")
|
||||
}
|
||||
if r.IsSuccess() {
|
||||
t.Fatalf("NotImplemented must not be success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_NotSuccess(t *testing.T) {
|
||||
r := Error(http.ErrAbortHandler)
|
||||
if r.IsSuccess() {
|
||||
t.Fatalf("error response must not be success")
|
||||
}
|
||||
herr, ok := r.(HTTPErrorResponse)
|
||||
if !ok {
|
||||
t.Fatalf("expected HTTPErrorResponse")
|
||||
}
|
||||
if herr.Error() == nil {
|
||||
t.Fatalf("expected non-nil err")
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_ContentTypeJSON(t *testing.T) {
|
||||
r := Error(http.ErrAbortHandler)
|
||||
if r.(InspectableHTTPResponse).ContentType() != "application/json" {
|
||||
t.Fatalf("expected application/json")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadData(t *testing.T) {
|
||||
c, rec := newTestCtx()
|
||||
payload := []byte("file content")
|
||||
r := DownloadData(http.StatusOK, "text/plain", "f.txt", payload)
|
||||
if r.(InspectableHTTPResponse).ContentType() != "text/plain" {
|
||||
t.Fatalf("contenttype mismatch")
|
||||
}
|
||||
if r.(InspectableHTTPResponse).Statuscode() != http.StatusOK {
|
||||
t.Fatalf("status mismatch")
|
||||
}
|
||||
body := r.(InspectableHTTPResponse).BodyString(c)
|
||||
if body == nil || *body != string(payload) {
|
||||
t.Fatalf("body mismatch")
|
||||
}
|
||||
r.Write(c)
|
||||
if rec.Header().Get("Content-Disposition") == "" {
|
||||
t.Fatalf("expected Content-Disposition header")
|
||||
}
|
||||
if !bytes.Contains([]byte(rec.Header().Get("Content-Disposition")), []byte("f.txt")) {
|
||||
t.Fatalf("Content-Disposition does not contain filename: %q", rec.Header().Get("Content-Disposition"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeekable(t *testing.T) {
|
||||
r := Seekable("foo.bin", "application/octet-stream", bytes.NewReader([]byte("xyz")))
|
||||
if !r.IsSuccess() {
|
||||
t.Fatalf("seekable must be success")
|
||||
}
|
||||
ir := r.(InspectableHTTPResponse)
|
||||
if ir.Statuscode() != 200 {
|
||||
t.Fatalf("expected 200")
|
||||
}
|
||||
if ir.ContentType() != "application/octet-stream" {
|
||||
t.Fatalf("contenttype mismatch")
|
||||
}
|
||||
body := ir.BodyString(nil)
|
||||
if body == nil || *body != "(seekable)" {
|
||||
t.Fatalf("BodyString mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFile_Builders(t *testing.T) {
|
||||
r := File("text/plain", "/tmp/this-file-should-not-exist-xyz")
|
||||
if !r.IsSuccess() {
|
||||
t.Fatalf("File must IsSuccess true")
|
||||
}
|
||||
if r.(InspectableHTTPResponse).Statuscode() != 200 {
|
||||
t.Fatalf("expected 200")
|
||||
}
|
||||
if r.(InspectableHTTPResponse).ContentType() != "text/plain" {
|
||||
t.Fatalf("contenttype mismatch")
|
||||
}
|
||||
r2 := Download("application/pdf", "/tmp/this-file-should-not-exist-xyz", "doc.pdf")
|
||||
if r2.(InspectableHTTPResponse).ContentType() != "application/pdf" {
|
||||
t.Fatalf("contenttype mismatch")
|
||||
}
|
||||
body := r.(InspectableHTTPResponse).BodyString(nil)
|
||||
if body != nil {
|
||||
t.Fatalf("expected nil body for nonexistent file")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package ginext
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestJoinPaths(t *testing.T) {
|
||||
cases := []struct {
|
||||
abs, rel, want string
|
||||
}{
|
||||
{"", "", ""},
|
||||
{"/api", "", "/api"},
|
||||
{"/api", "users", "/api/users"},
|
||||
{"/api/", "users", "/api/users"},
|
||||
{"/api", "/users", "/api/users"},
|
||||
{"/api/", "/users/", "/api/users/"},
|
||||
{"/api", "users/", "/api/users/"},
|
||||
{"", "/users", "/users"},
|
||||
{"/", "/", "/"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := joinPaths(tc.abs, tc.rel)
|
||||
if got != tc.want {
|
||||
t.Errorf("joinPaths(%q,%q)=%q want %q", tc.abs, tc.rel, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastChar(t *testing.T) {
|
||||
if lastChar("hello") != 'o' {
|
||||
t.Fatalf("expected 'o'")
|
||||
}
|
||||
if lastChar("/") != '/' {
|
||||
t.Fatalf("expected '/'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastChar_PanicsOnEmpty(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatalf("expected panic on empty string")
|
||||
}
|
||||
}()
|
||||
lastChar("")
|
||||
}
|
||||
|
||||
func sampleHandler(_ *gin.Context) {}
|
||||
|
||||
func TestNameOfFunction(t *testing.T) {
|
||||
name := nameOfFunction(sampleHandler)
|
||||
if name == "" {
|
||||
t.Fatalf("expected non-empty name")
|
||||
}
|
||||
// nameOfFunction strips path prefix, expecting form "ginext.sampleHandler" or similar
|
||||
if name != "ginext.sampleHandler" {
|
||||
t.Errorf("expected ginext.sampleHandler, got %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
type sampleStruct struct{}
|
||||
|
||||
func (s sampleStruct) Method(_ *gin.Context) {}
|
||||
|
||||
func TestNameOfFunction_StripsFmSuffix(t *testing.T) {
|
||||
s := sampleStruct{}
|
||||
// Method values get a "-fm" suffix that nameOfFunction should strip
|
||||
name := nameOfFunction(s.Method)
|
||||
if name == "" {
|
||||
t.Fatalf("expected non-empty name")
|
||||
}
|
||||
if got := name; got[len(got)-3:] == "-fm" {
|
||||
t.Errorf("expected -fm suffix to be stripped, got %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_GroupAndAbsPath(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes()
|
||||
if rw.absPath != "" {
|
||||
t.Fatalf("expected empty absPath")
|
||||
}
|
||||
g1 := rw.Group("/api")
|
||||
if g1.absPath != "/api" {
|
||||
t.Fatalf("expected /api, got %q", g1.absPath)
|
||||
}
|
||||
g2 := g1.Group("/v1")
|
||||
if g2.absPath != "/api/v1" {
|
||||
t.Fatalf("expected /api/v1, got %q", g2.absPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_UseAccumulatesMiddleware(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes()
|
||||
|
||||
mw1 := func(c *gin.Context) {}
|
||||
mw2 := func(c *gin.Context) {}
|
||||
|
||||
r1 := rw.Use(mw1)
|
||||
if len(r1.defaultHandler) != 1 {
|
||||
t.Fatalf("expected 1 handler after Use, got %d", len(r1.defaultHandler))
|
||||
}
|
||||
r2 := r1.Use(mw2)
|
||||
if len(r2.defaultHandler) != 2 {
|
||||
t.Fatalf("expected 2 handlers, got %d", len(r2.defaultHandler))
|
||||
}
|
||||
// Original parent should be unchanged
|
||||
if len(rw.defaultHandler) != 0 {
|
||||
t.Fatalf("expected parent to remain unchanged, got %d", len(rw.defaultHandler))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_GroupCopiesMiddleware(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes().Use(func(c *gin.Context) {})
|
||||
g := rw.Group("/x")
|
||||
if len(g.defaultHandler) != 1 {
|
||||
t.Fatalf("expected group to inherit middleware")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_MethodBuilders(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
build func(string) *GinRouteBuilder
|
||||
want string
|
||||
}{
|
||||
{"GET", rw.GET, http.MethodGet},
|
||||
{"POST", rw.POST, http.MethodPost},
|
||||
{"PUT", rw.PUT, http.MethodPut},
|
||||
{"PATCH", rw.PATCH, http.MethodPatch},
|
||||
{"DELETE", rw.DELETE, http.MethodDelete},
|
||||
{"HEAD", rw.HEAD, http.MethodHead},
|
||||
{"OPTIONS", rw.OPTIONS, http.MethodOptions},
|
||||
{"COUNT", rw.COUNT, "COUNT"},
|
||||
{"Any", rw.Any, "*"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
b := tc.build("/foo")
|
||||
if b.method != tc.want {
|
||||
t.Errorf("%s: expected method %q, got %q", tc.name, tc.want, b.method)
|
||||
}
|
||||
if b.relPath != "/foo" {
|
||||
t.Errorf("%s: expected relPath /foo, got %q", tc.name, b.relPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_RouteBuilderUseAppends(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes()
|
||||
b := rw.GET("/x")
|
||||
startCount := len(b.handlers)
|
||||
b.Use(func(c *gin.Context) {})
|
||||
if len(b.handlers) != startCount+1 {
|
||||
t.Fatalf("expected handlers to grow by 1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_RouteBuilderInheritsDefaultHandlers(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes().Use(func(c *gin.Context) {})
|
||||
b := rw.GET("/x")
|
||||
if len(b.handlers) != 1 {
|
||||
t.Fatalf("expected route to inherit default handler")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_WithJSONFilter_AddsHandler(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
rw := w.Routes().WithJSONFilter("xyz")
|
||||
if len(rw.defaultHandler) != 1 {
|
||||
t.Fatalf("expected json filter middleware to be added")
|
||||
}
|
||||
// invoke it to verify it sets the key
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rw.defaultHandler[0](c)
|
||||
if c.GetString(jsonFilterKey) != "xyz" {
|
||||
t.Fatalf("expected jsonFilterKey to be set to xyz")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_RouteBuilderWithJSONFilter(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
b := w.Routes().GET("/x").WithJSONFilter("abc")
|
||||
if len(b.handlers) != 1 {
|
||||
t.Fatalf("expected handler to be added")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_HandleRegistersAndStoresSpec(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.Routes().GET("/foo").Handle(func(p PreContext) HTTPResponse {
|
||||
return Status(http.StatusOK)
|
||||
})
|
||||
|
||||
if len(w.routeSpecs) != 1 {
|
||||
t.Fatalf("expected 1 route spec, got %d", len(w.routeSpecs))
|
||||
}
|
||||
spec := w.routeSpecs[0]
|
||||
if spec.Method != http.MethodGet {
|
||||
t.Fatalf("expected GET, got %s", spec.Method)
|
||||
}
|
||||
if spec.URL != "/foo" {
|
||||
t.Fatalf("expected /foo, got %s", spec.URL)
|
||||
}
|
||||
|
||||
// Hitting the route should serve our handler
|
||||
req := httptest.NewRequest(http.MethodGet, "/foo", nil)
|
||||
rec := w.ServeHTTP(req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_AnyRegistersAllMethods(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.Routes().Any("/wild").Handle(func(p PreContext) HTTPResponse {
|
||||
return Status(http.StatusOK)
|
||||
})
|
||||
|
||||
for _, m := range anyMethods {
|
||||
req := httptest.NewRequest(m, "/wild", nil)
|
||||
rec := w.ServeHTTP(req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("method %s: expected 200, got %d", m, rec.Code)
|
||||
}
|
||||
}
|
||||
if w.routeSpecs[0].Method != "ANY" {
|
||||
t.Fatalf("expected method label ANY, got %s", w.routeSpecs[0].Method)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_GroupedRoutes(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
api := w.Routes().Group("/api")
|
||||
api.GET("/ping").Handle(func(p PreContext) HTTPResponse {
|
||||
return Text(http.StatusOK, "pong")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/ping", nil)
|
||||
rec := w.ServeHTTP(req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rec.Code)
|
||||
}
|
||||
if rec.Body.String() != "pong" {
|
||||
t.Fatalf("expected pong, got %q", rec.Body.String())
|
||||
}
|
||||
if w.routeSpecs[0].URL != "/api/ping" {
|
||||
t.Fatalf("expected absPath /api/ping in spec, got %s", w.routeSpecs[0].URL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutes_NoRoute(t *testing.T) {
|
||||
w := NewEngine(Options{})
|
||||
w.NoRoute(func(p PreContext) HTTPResponse {
|
||||
return Status(http.StatusTeapot)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/missing", nil)
|
||||
rec := w.ServeHTTP(req)
|
||||
if rec.Code != http.StatusTeapot {
|
||||
t.Fatalf("expected 418, got %d", rec.Code)
|
||||
}
|
||||
|
||||
if len(w.routeSpecs) != 1 || w.routeSpecs[0].URL != "[NO_ROUTE]" {
|
||||
t.Fatalf("expected NO_ROUTE spec to be recorded")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user