mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
Add Party.ResetRouterFilters
relative to: https://github.com/kataras/iris/issues/1604#issuecomment-680410131
This commit is contained in:
parent
1780d97d44
commit
d44b69faed
|
@ -466,7 +466,7 @@ func main() {
|
||||||
|
|
||||||
- `Party.UseError(...Handler)` - to register handlers to run before any http errors (e.g. before `OnErrorCode/OnAnyErrorCode` or default error codes when no handler is responsible to handle a specific http status code).
|
- `Party.UseError(...Handler)` - to register handlers to run before any http errors (e.g. before `OnErrorCode/OnAnyErrorCode` or default error codes when no handler is responsible to handle a specific http status code).
|
||||||
|
|
||||||
- `Party.UseRouter(...Handler)` - to register handlers before the main router, useful on handlers that should control whether the router itself should ran or not. Independently of the incoming request's method and path values. These handlers will be executed ALWAYS against ALL incoming matched requests. Example of use-case: CORS.
|
- `Party.UseRouter(...Handler) and Party.ResetRouterFilters()` - to register handlers before the main router, useful on handlers that should control whether the router itself should ran or not. Independently of the incoming request's method and path values. These handlers will be executed ALWAYS against ALL incoming matched requests. Example of use-case: CORS.
|
||||||
|
|
||||||
- `*versioning.Group` type is a full `Party` now.
|
- `*versioning.Group` type is a full `Party` now.
|
||||||
|
|
||||||
|
|
|
@ -158,7 +158,7 @@ type APIBuilder struct {
|
||||||
logger *golog.Logger
|
logger *golog.Logger
|
||||||
// parent is the creator of this Party.
|
// parent is the creator of this Party.
|
||||||
// It is nil on Root.
|
// It is nil on Root.
|
||||||
parent *APIBuilder // currently it's used only on UseRouter feature.
|
parent *APIBuilder // currently it's not used anywhere.
|
||||||
|
|
||||||
// the per-party APIBuilder with DI.
|
// the per-party APIBuilder with DI.
|
||||||
apiBuilderDI *APIContainer
|
apiBuilderDI *APIContainer
|
||||||
|
@ -169,7 +169,8 @@ type APIBuilder struct {
|
||||||
routes *repository
|
routes *repository
|
||||||
|
|
||||||
// the per-party handlers, order
|
// the per-party handlers, order
|
||||||
// of handlers registration matters.
|
// of handlers registration matters,
|
||||||
|
// inherited by children unless Reset is called.
|
||||||
middleware context.Handlers
|
middleware context.Handlers
|
||||||
middlewareErrorCode context.Handlers
|
middlewareErrorCode context.Handlers
|
||||||
// the global middleware handlers, order of call doesn't matters, order
|
// the global middleware handlers, order of call doesn't matters, order
|
||||||
|
@ -197,6 +198,10 @@ type APIBuilder struct {
|
||||||
// the per-party (and its children) route registration rule, see `SetRegisterRule`.
|
// the per-party (and its children) route registration rule, see `SetRegisterRule`.
|
||||||
routeRegisterRule RouteRegisterRule
|
routeRegisterRule RouteRegisterRule
|
||||||
|
|
||||||
|
// routerFilterHandlers holds a reference
|
||||||
|
// of the handlers used by the current and its parent Party's registered
|
||||||
|
// router filters. Inherited by children unless `Reset` (see `UseRouter`),
|
||||||
|
routerFilterHandlers context.Handlers
|
||||||
// routerFilters field is shared across Parties. Each Party registers
|
// routerFilters field is shared across Parties. Each Party registers
|
||||||
// one or more middlewares to run before the router itself using the `UseRouter` method.
|
// one or more middlewares to run before the router itself using the `UseRouter` method.
|
||||||
// Each Party calls the shared filter (`partyMatcher`) that decides if its `UseRouter` handlers
|
// Each Party calls the shared filter (`partyMatcher`) that decides if its `UseRouter` handlers
|
||||||
|
@ -665,13 +670,15 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P
|
||||||
routes: api.routes,
|
routes: api.routes,
|
||||||
beginGlobalHandlers: api.beginGlobalHandlers,
|
beginGlobalHandlers: api.beginGlobalHandlers,
|
||||||
doneGlobalHandlers: api.doneGlobalHandlers,
|
doneGlobalHandlers: api.doneGlobalHandlers,
|
||||||
routerFilters: api.routerFilters, // shared.
|
|
||||||
partyMatcher: api.partyMatcher, // shared.
|
|
||||||
// per-party/children
|
// per-party/children
|
||||||
parent: api,
|
parent: api,
|
||||||
middleware: middleware,
|
middleware: middleware,
|
||||||
middlewareErrorCode: context.JoinHandlers(api.middlewareErrorCode, context.Handlers{}),
|
middlewareErrorCode: context.JoinHandlers(api.middlewareErrorCode, context.Handlers{}),
|
||||||
doneHandlers: api.doneHandlers[0:],
|
doneHandlers: api.doneHandlers[0:],
|
||||||
|
routerFilters: api.routerFilters,
|
||||||
|
routerFilterHandlers: api.routerFilterHandlers,
|
||||||
|
partyMatcher: api.partyMatcher,
|
||||||
relativePath: fullpath,
|
relativePath: fullpath,
|
||||||
allowMethods: allowMethods,
|
allowMethods: allowMethods,
|
||||||
handlerExecutionRules: api.handlerExecutionRules,
|
handlerExecutionRules: api.handlerExecutionRules,
|
||||||
|
@ -817,7 +824,7 @@ type (
|
||||||
// PartyMatcherFunc used to build a filter which decides
|
// PartyMatcherFunc used to build a filter which decides
|
||||||
// if the given Party is responsible to fire its `UseRouter` handlers or not.
|
// if the given Party is responsible to fire its `UseRouter` handlers or not.
|
||||||
// Can be customized through `SetPartyMatcher` method. See `Match` method too.
|
// Can be customized through `SetPartyMatcher` method. See `Match` method too.
|
||||||
PartyMatcherFunc func(Party, *context.Context) bool
|
PartyMatcherFunc func(*context.Context, Party) bool
|
||||||
// PartyMatcher decides if `UseRouter` handlers should be executed or not.
|
// PartyMatcher decides if `UseRouter` handlers should be executed or not.
|
||||||
// A different interface becauwe we want to separate
|
// A different interface becauwe we want to separate
|
||||||
// the Party's public API from `UseRouter` internals.
|
// the Party's public API from `UseRouter` internals.
|
||||||
|
@ -828,8 +835,8 @@ type (
|
||||||
// for its Party's fullpath, subdomain the Party's
|
// for its Party's fullpath, subdomain the Party's
|
||||||
// matcher and the associated handlers to be executed before main router's request handler.
|
// matcher and the associated handlers to be executed before main router's request handler.
|
||||||
Filter struct {
|
Filter struct {
|
||||||
Party Party // the Party itself
|
Matcher PartyMatcher // it's a Party, for freedom that can be changed through a custom matcher which accepts the same filter.
|
||||||
Matcher PartyMatcher // it's a Party, for freedom that can be changed through a custom matcher which accepts the same filter.
|
Skippers map[*APIBuilder]struct{} // skip execution on these builders ( see `Reset`)
|
||||||
Subdomain string
|
Subdomain string
|
||||||
Path string
|
Path string
|
||||||
Handlers context.Handlers
|
Handlers context.Handlers
|
||||||
|
@ -855,10 +862,10 @@ func (api *APIBuilder) SetPartyMatcher(matcherFunc PartyMatcherFunc) {
|
||||||
// Calls its parent's Match if possible.
|
// Calls its parent's Match if possible.
|
||||||
// Implements the `PartyMatcher` interface.
|
// Implements the `PartyMatcher` interface.
|
||||||
func (api *APIBuilder) Match(ctx *context.Context) bool {
|
func (api *APIBuilder) Match(ctx *context.Context) bool {
|
||||||
return api.partyMatcher(api, ctx)
|
return api.partyMatcher(ctx, api)
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultPartyMatcher(p Party, ctx *context.Context) bool {
|
func defaultPartyMatcher(ctx *context.Context, p Party) bool {
|
||||||
subdomain, path := splitSubdomainAndPath(p.GetRelPath())
|
subdomain, path := splitSubdomainAndPath(p.GetRelPath())
|
||||||
staticPath := staticPath(path)
|
staticPath := staticPath(path)
|
||||||
hosts := subdomain != ""
|
hosts := subdomain != ""
|
||||||
|
@ -915,23 +922,16 @@ func (api *APIBuilder) UseRouter(handlers ...context.Handler) {
|
||||||
beginHandlers := context.Handlers(handlers)
|
beginHandlers := context.Handlers(handlers)
|
||||||
// respect any execution rules (begin).
|
// respect any execution rules (begin).
|
||||||
api.handlerExecutionRules.Begin.apply(&beginHandlers)
|
api.handlerExecutionRules.Begin.apply(&beginHandlers)
|
||||||
|
beginHandlers = context.JoinHandlers(api.routerFilterHandlers, beginHandlers)
|
||||||
|
|
||||||
if f := api.routerFilters[api]; f != nil && len(f.Handlers) > 0 { // exists.
|
if f := api.routerFilters[api]; f != nil && len(f.Handlers) > 0 { // exists.
|
||||||
beginHandlers = context.UpsertHandlers(f.Handlers, beginHandlers) // remove dupls.
|
beginHandlers = context.UpsertHandlers(f.Handlers, beginHandlers) // remove dupls.
|
||||||
} else {
|
|
||||||
// Note(@kataras): we don't add the parent's filter handlers
|
|
||||||
// on `Party` method because we need to know if a `UseRouter` call exist
|
|
||||||
// before prepending the parent's ones and fill a new Filter on `routerFilters`,
|
|
||||||
// that key should NOT exist on a Party without `UseRouter` handlers (see router.go).
|
|
||||||
// That's the only reason we need the `parent` field.
|
|
||||||
if api.parent != nil {
|
|
||||||
// If it's not root, add the parent's handlers here.
|
|
||||||
if root, ok := api.routerFilters[api.parent]; ok {
|
|
||||||
beginHandlers = context.UpsertHandlers(root.Handlers, beginHandlers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we are not using the parent field here,
|
||||||
|
// we need to have control over those values in order to be able to `Reset`.
|
||||||
|
api.routerFilterHandlers = beginHandlers
|
||||||
|
|
||||||
subdomain, path := splitSubdomainAndPath(api.relativePath)
|
subdomain, path := splitSubdomainAndPath(api.relativePath)
|
||||||
api.routerFilters[api] = &Filter{
|
api.routerFilters[api] = &Filter{
|
||||||
Matcher: api,
|
Matcher: api,
|
||||||
|
@ -1033,13 +1033,43 @@ func (api *APIBuilder) DoneGlobal(handlers ...context.Handler) {
|
||||||
func (api *APIBuilder) Reset() Party {
|
func (api *APIBuilder) Reset() Party {
|
||||||
api.middleware = api.middleware[0:0]
|
api.middleware = api.middleware[0:0]
|
||||||
api.middlewareErrorCode = api.middlewareErrorCode[0:0]
|
api.middlewareErrorCode = api.middlewareErrorCode[0:0]
|
||||||
|
api.ResetRouterFilters()
|
||||||
|
|
||||||
api.doneHandlers = api.doneHandlers[0:0]
|
api.doneHandlers = api.doneHandlers[0:0]
|
||||||
api.handlerExecutionRules = ExecutionRules{}
|
api.handlerExecutionRules = ExecutionRules{}
|
||||||
api.routeRegisterRule = RouteOverride
|
api.routeRegisterRule = RouteOverride
|
||||||
|
|
||||||
// keep container as it's.
|
// keep container as it's.
|
||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetRouterFilters deactivates any pervious registered
|
||||||
|
// router filters and the parents ones for this Party.
|
||||||
|
//
|
||||||
|
// Returns this Party.
|
||||||
|
func (api *APIBuilder) ResetRouterFilters() Party {
|
||||||
|
api.routerFilterHandlers = api.routerFilterHandlers[0:0]
|
||||||
|
delete(api.routerFilters, api)
|
||||||
|
|
||||||
|
if api.parent == nil {
|
||||||
|
// it's the root, stop, nothing else to do here.
|
||||||
|
return api
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a filter with empty handlers, the router will find it, execute nothing
|
||||||
|
// and continue with the request handling. This works on Reset() and no UseRouter
|
||||||
|
// and with Reset().UseRouter.
|
||||||
|
subdomain, path := splitSubdomainAndPath(api.relativePath)
|
||||||
|
api.routerFilters[api] = &Filter{
|
||||||
|
Matcher: api,
|
||||||
|
Handlers: nil,
|
||||||
|
Subdomain: subdomain,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
return api
|
||||||
|
}
|
||||||
|
|
||||||
// None registers an "offline" route
|
// None registers an "offline" route
|
||||||
// see context.ExecRoute(routeName) and
|
// see context.ExecRoute(routeName) and
|
||||||
// party.Routes().Online(handleResultRouteInfo, "GET") and
|
// party.Routes().Online(handleResultRouteInfo, "GET") and
|
||||||
|
|
|
@ -121,6 +121,11 @@ type Party interface {
|
||||||
//
|
//
|
||||||
// Returns this Party.
|
// Returns this Party.
|
||||||
Reset() Party
|
Reset() Party
|
||||||
|
// ResetRouterFilters deactivates any pervious registered
|
||||||
|
// router filters and the parents ones for this Party.
|
||||||
|
//
|
||||||
|
// Returns this Party.
|
||||||
|
ResetRouterFilters() Party
|
||||||
|
|
||||||
// AllowMethods will re-register the future routes that will be registered
|
// AllowMethods will re-register the future routes that will be registered
|
||||||
// via `Handle`, `Get`, `Post`, ... to the given "methods" on that Party and its children "Parties",
|
// via `Handle`, `Get`, `Post`, ... to the given "methods" on that Party and its children "Parties",
|
||||||
|
|
|
@ -149,14 +149,14 @@ func (router *Router) buildMainHandlerWithFilters(routerFilters map[Party]*Filte
|
||||||
ctx := cPool.Acquire(w, r)
|
ctx := cPool.Acquire(w, r)
|
||||||
|
|
||||||
filterExecuted := false
|
filterExecuted := false
|
||||||
for _, f := range sortedFilters {
|
for _, f := range sortedFilters { // from subdomain, largest path to shortest.
|
||||||
// fmt.Printf("Sorted filter execution: [%s] [%s]\n", f.Subdomain, f.Path)
|
// fmt.Printf("Sorted filter execution: [%s] [%s]\n", f.Subdomain, f.Path)
|
||||||
if f.Matcher.Match(ctx) {
|
if f.Matcher.Match(ctx) {
|
||||||
// fmt.Printf("Matched [%s] and execute [%d] handlers [%s]\n\n", ctx.Path(), len(f.Handlers), context.HandlersNames(f.Handlers))
|
// fmt.Printf("Matched [%s] and execute [%d] handlers [%s]\n\n", ctx.Path(), len(f.Handlers), context.HandlersNames(f.Handlers))
|
||||||
filterExecuted = true
|
filterExecuted = true
|
||||||
// execute the final handlers chain.
|
// execute the final handlers chain.
|
||||||
ctx.Do(f.Handlers)
|
ctx.Do(f.Handlers)
|
||||||
break
|
break // and break on first found.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -198,7 +198,7 @@ func TestUseRouterParentDisallow(t *testing.T) {
|
||||||
ctx.WriteString(expectedResponse)
|
ctx.WriteString(expectedResponse)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.SetPartyMatcher(func(p iris.Party, ctx iris.Context) bool {
|
app.SetPartyMatcher(func(ctx iris.Context, p iris.Party) bool {
|
||||||
// modifies the PartyMatcher to not match any UseRouter,
|
// modifies the PartyMatcher to not match any UseRouter,
|
||||||
// tests should receive the handlers response alone.
|
// tests should receive the handlers response alone.
|
||||||
return false
|
return false
|
||||||
|
|
131
middleware/basicauth/basicauth_test.go
Normal file
131
middleware/basicauth/basicauth_test.go
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
// Package basicauth_tests performs black-box testing of the basicauth middleware.
|
||||||
|
// Note that, a secondary test is also available at: _examples/auth/basicauth/main_test.go
|
||||||
|
package basicauth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/httptest"
|
||||||
|
"github.com/kataras/iris/v12/middleware/basicauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBasicAuthUseRouter(t *testing.T) {
|
||||||
|
app := iris.New()
|
||||||
|
users := map[string]string{
|
||||||
|
"usr": "pss",
|
||||||
|
"admin": "admin",
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseRouter(basicauth.Default(users))
|
||||||
|
|
||||||
|
app.Get("/", func(ctx iris.Context) {
|
||||||
|
username, _, _ := ctx.Request().BasicAuth()
|
||||||
|
ctx.Writef("Hello, %s!", username)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Subdomain("static").Get("/", func(ctx iris.Context) {
|
||||||
|
username, _, _ := ctx.Request().BasicAuth()
|
||||||
|
ctx.Writef("Static, %s", username)
|
||||||
|
})
|
||||||
|
|
||||||
|
resetWithUseRouter := app.Subdomain("reset_with_use_router").ResetRouterFilters()
|
||||||
|
resetWithUseRouter.UseRouter(func(ctx iris.Context) {
|
||||||
|
ctx.Record()
|
||||||
|
ctx.Writef("with use router\n")
|
||||||
|
ctx.Next()
|
||||||
|
})
|
||||||
|
resetWithUseRouter.Get("/", func(ctx iris.Context) {
|
||||||
|
username, _, _ := ctx.Request().BasicAuth()
|
||||||
|
ctx.Writef("%s", username) // username should be empty.
|
||||||
|
})
|
||||||
|
// ^ order of these should not matter.
|
||||||
|
app.Subdomain("reset").ResetRouterFilters().Get("/", func(ctx iris.Context) {
|
||||||
|
username, _, _ := ctx.Request().BasicAuth()
|
||||||
|
ctx.Writef("%s", username) // username should be empty.
|
||||||
|
})
|
||||||
|
|
||||||
|
e := httptest.New(t, app.Configure(
|
||||||
|
iris.WithFireMethodNotAllowed,
|
||||||
|
iris.WithResetOnFireErrorCode,
|
||||||
|
))
|
||||||
|
|
||||||
|
for username, password := range users {
|
||||||
|
// Test pass authentication and route found.
|
||||||
|
e.GET("/").WithBasicAuth(username, password).Expect().
|
||||||
|
Status(httptest.StatusOK).Body().Equal(fmt.Sprintf("Hello, %s!", username))
|
||||||
|
|
||||||
|
// Test empty auth.
|
||||||
|
e.GET("/").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
|
// Test invalid auth.
|
||||||
|
e.GET("/").WithBasicAuth(username, "invalid_password").Expect().
|
||||||
|
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
|
e.GET("/").WithBasicAuth("invaid_username", password).Expect().
|
||||||
|
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
|
|
||||||
|
// Test different method, it should pass the authentication (no stop on 401)
|
||||||
|
// but it doesn't fire the GET route, instead it gives 405.
|
||||||
|
e.POST("/").WithBasicAuth(username, password).Expect().
|
||||||
|
Status(httptest.StatusMethodNotAllowed).Body().Equal("Method Not Allowed")
|
||||||
|
|
||||||
|
// Test pass the authentication but route not found.
|
||||||
|
e.GET("/notfound").WithBasicAuth(username, password).Expect().
|
||||||
|
Status(httptest.StatusNotFound).Body().Equal("Not Found")
|
||||||
|
|
||||||
|
// Test empty auth.
|
||||||
|
e.GET("/notfound").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
|
// Test invalid auth.
|
||||||
|
e.GET("/notfound").WithBasicAuth(username, "invalid_password").Expect().
|
||||||
|
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
|
e.GET("/notfound").WithBasicAuth("invaid_username", password).Expect().
|
||||||
|
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
|
|
||||||
|
// Test subdomain inherited.
|
||||||
|
sub := e.Builder(func(req *httptest.Request) {
|
||||||
|
req.WithURL("http://static.mydomain.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test pass and route found.
|
||||||
|
sub.GET("/").WithBasicAuth(username, password).Expect().
|
||||||
|
Status(httptest.StatusOK).Body().Equal(fmt.Sprintf("Static, %s", username))
|
||||||
|
|
||||||
|
// Test empty auth.
|
||||||
|
sub.GET("/").Expect().Status(httptest.StatusUnauthorized)
|
||||||
|
// Test invalid auth.
|
||||||
|
sub.GET("/").WithBasicAuth(username, "invalid_password").Expect().
|
||||||
|
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
|
sub.GET("/").WithBasicAuth("invaid_username", password).Expect().
|
||||||
|
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
|
|
||||||
|
// Test pass the authentication but route not found.
|
||||||
|
sub.GET("/notfound").WithBasicAuth(username, password).Expect().
|
||||||
|
Status(httptest.StatusNotFound).Body().Equal("Not Found")
|
||||||
|
|
||||||
|
// Test empty auth.
|
||||||
|
sub.GET("/notfound").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
|
// Test invalid auth.
|
||||||
|
sub.GET("/notfound").WithBasicAuth(username, "invalid_password").Expect().
|
||||||
|
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
|
sub.GET("/notfound").WithBasicAuth("invaid_username", password).Expect().
|
||||||
|
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
|
|
||||||
|
// Test a reset-ed Party with a single one UseRouter
|
||||||
|
// which writes on matched routes and reset and send the error on errors.
|
||||||
|
// (all should pass without auth).
|
||||||
|
sub = e.Builder(func(req *httptest.Request) {
|
||||||
|
req.WithURL("http://reset_with_use_router.mydomain.com")
|
||||||
|
})
|
||||||
|
sub.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("with use router\n")
|
||||||
|
sub.POST("/").Expect().Status(httptest.StatusMethodNotAllowed).Body().Equal("Method Not Allowed")
|
||||||
|
sub.GET("/notfound").Expect().Status(httptest.StatusNotFound).Body().Equal("Not Found")
|
||||||
|
|
||||||
|
// Test a reset-ed Party (all should pass without auth).
|
||||||
|
sub = e.Builder(func(req *httptest.Request) {
|
||||||
|
req.WithURL("http://reset.mydomain.com")
|
||||||
|
})
|
||||||
|
sub.GET("/").Expect().Status(httptest.StatusOK).Body().Empty()
|
||||||
|
sub.POST("/").Expect().Status(httptest.StatusMethodNotAllowed).Body().Equal("Method Not Allowed")
|
||||||
|
sub.GET("/notfound").Expect().Status(httptest.StatusNotFound).Body().Equal("Not Found")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user