In this example we are going to create a custom router which is based on Golang's standard library. It comes with middleware support and route groups. Example contains many example handlers and middlewares to prove that our custom router works as expected. Make sure you run the test to everything works fine.


Structure


├── main.go
├── pkg
│   ├── xhttp
│   │   ├── router.go
│   │   └── router_test.go
│   └── xmiddleware
│   ├── api.go
│   ├── audit.go
│   ├── global.go
│   └── log.go
└── src
└── app
├── api.go
├── home.go
├── v1.go
└── v2.go

This is what we are going to get at the end.


-------------------------------------------------------------------------------
| # | METHOD | PATH | HANDLE | MIDDLEWARE |
-------------------------------------------------------------------------------
| 1 | GET | /{$} | app.Home.Default | xmiddleware.Global1 |
| | | | | xmiddleware.Global2 |
-------------------------------------------------------------------------------
| 2 | GET | /api | app.API.Default | xmiddleware.Global1 |
| | | | | xmiddleware.Global2 |
| | | | | xmiddleware.API |
-------------------------------------------------------------------------------
| 3 | GET | /api/v1 | app.V1.Default | xmiddleware.Global1 |
| | | | | xmiddleware.Global2 |
| | | | | xmiddleware.API |
| | | | | xmiddleware.V1 |
-------------------------------------------------------------------------------
| 4 | POST | /api/v1/admins | app.V1.CreateAdmin | xmiddleware.Global1 |
| | | | | xmiddleware.Global2 |
| | | | | xmiddleware.API |
| | | | | xmiddleware.V1 |
-------------------------------------------------------------------------------
| 5 | DELETE | /api/v1/admins/{id} | app.V1.DeleteAdmin | xmiddleware.Global1 |
| | | | | xmiddleware.Global2 |
| | | | | xmiddleware.API |
| | | | | xmiddleware.V1 |
| | | | | xmiddleware.Audit |
-------------------------------------------------------------------------------
| 6 | GET | /api/v1/logs | app.V1.ListLogs | xmiddleware.Global1 |
| | | | | xmiddleware.Global2 |
| | | | | xmiddleware.API |
| | | | | xmiddleware.V1 |
| | | | | xmiddleware.Log |
-------------------------------------------------------------------------------
| 7 | GET | /api/v2 | app.V2.Default | xmiddleware.Global1 |
| | | | | xmiddleware.Global2 |
| | | | | xmiddleware.API |
| | | | | xmiddleware.V2 |
-------------------------------------------------------------------------------
| 8 | POST | /api/v2/admins | app.V2.CreateAdmin | xmiddleware.Global1 |
| | | | | xmiddleware.Global2 |
| | | | | xmiddleware.API |
| | | | | xmiddleware.V2 |
-------------------------------------------------------------------------------
| 9 | DELETE | /api/v2/admins/{id} | app.V2.DeleteAdmin | xmiddleware.Global1 |
| | | | | xmiddleware.Global2 |
| | | | | xmiddleware.API |
| | | | | xmiddleware.V2 |
| | | | | xmiddleware.Audit |
-------------------------------------------------------------------------------

Files


main.go


package main

import (
"fmt"
"net/http"

"rest/pkg/xhttp"
"rest/pkg/xmiddleware"
"rest/src/app"
)

func main() {
// Base --------------------------------------------------------------------
home := app.Home{}
baseRouter := xhttp.NewRouter()
baseRouter.Use(xmiddleware.Global1)
baseRouter.Use(xmiddleware.Global2)
baseRouter.Add("GET", "/", home.Default) // GET /

// API ---------------------------------------------------------------------
api := app.API{}
apiRouter := baseRouter.Group("/api")
apiRouter.Use(xmiddleware.API)
apiRouter.Add("GET", "/", api.Default) // GET /api
baseRouter.Bind(apiRouter)

// V1 ----------------------------------------------------------------------
v1 := app.V1{}
v1Router := apiRouter.Group("/v1")
v1Router.Use(xmiddleware.V1)
v1Router.Add("GET", "/", v1.Default) // GET /api/v1
v1Router.Add("POST", "/admins", v1.CreateAdmin) // POST /api/v1/admins
v1Router.Add("DELETE", "/admins/{id}", v1.DeleteAdmin, xmiddleware.Audit) // DELET /api/v1/admins/1
baseRouter.Bind(v1Router)

v1RouterLogs := v1Router.Group("/logs")
v1RouterLogs.Use(xmiddleware.Log)
v1RouterLogs.Add("GET", "/", v1.ListLogs) // GET /api/v1/logs
baseRouter.Bind(v1RouterLogs)

// V2 ----------------------------------------------------------------------
v2 := app.V2{}
v2Router := apiRouter.Group("/v2")
v2Router.Use(xmiddleware.V2)
v2Router.Add("GET", "/", v2.Default) // GET /api/v2
v2Router.Add("POST", "/admins", v2.CreateAdmin) // POST /api/v2/admins
v2Router.Add("DELETE", "/admins/{id}", v2.DeleteAdmin, xmiddleware.Audit) // DELET /api/v2/admins/1
baseRouter.Bind(v2Router)

// Dump roting table -------------------------------------------------------
fmt.Println(baseRouter.RouteTable())

// Server ------------------------------------------------------------------
http.ListenAndServe(":1234", baseRouter.Handler())
}

home.go


package app

import (
"fmt"
"net/http"
)

type Home struct{}

func (h Home) Default(w http.ResponseWriter, r *http.Request) {
fmt.Println("home: default")
w.Write([]byte(`home: default`))
}

api.go


package app

import (
"fmt"
"net/http"
)

type API struct{}

func (a API) Default(w http.ResponseWriter, r *http.Request) {
fmt.Println("API: default")
w.Write([]byte(`API: default`))
}

v1.go


package app

import (
"fmt"
"net/http"
)

type V1 struct{}

func (v V1) Default(w http.ResponseWriter, r *http.Request) {
fmt.Println("V1: default")
w.Write([]byte(`V1: default`))
}

func (v V1) CreateAdmin(w http.ResponseWriter, r *http.Request) {
fmt.Println("V1: admin create")
w.Write([]byte(`V1: admin create`))
}

func (v V1) DeleteAdmin(w http.ResponseWriter, r *http.Request) {
fmt.Println("V1: admin delete", r.PathValue("id"))
w.Write([]byte(`V1: admin delete ` + r.PathValue("id")))
}

func (v V1) ListLogs(w http.ResponseWriter, r *http.Request) {
fmt.Println("V1: logs list")
w.Write([]byte(`V1: logs list`))
}

v2.go


package app

import (
"fmt"
"net/http"
)

type V2 struct{}

func (v V2) Default(w http.ResponseWriter, r *http.Request) {
fmt.Println("V2: default")
w.Write([]byte(`V2: default`))
}

func (v V2) CreateAdmin(w http.ResponseWriter, r *http.Request) {
fmt.Println("V2: admin create")
w.Write([]byte(`V2: admin create`))
}

func (v V2) DeleteAdmin(w http.ResponseWriter, r *http.Request) {
fmt.Println("V2: admin delete", r.PathValue("id"))
w.Write([]byte(`V2: admin delete ` + r.PathValue("id")))
}

global.go


package xmiddleware

import (
"fmt"
"net/http"
)

func Global1(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println(":::", r.Pattern)
fmt.Println("Global 1")

next.ServeHTTP(w, r)
})
}

func Global2(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Global 2")

next.ServeHTTP(w, r)
})
}

api.go


package xmiddleware

import (
"fmt"
"net/http"
)

func API(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("API")

next.ServeHTTP(w, r)
})
}

func V1(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("V1")

next.ServeHTTP(w, r)
})
}

func V2(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("V2")

next.ServeHTTP(w, r)
})
}

audit.go


package xmiddleware

import (
"fmt"
"net/http"
)

func Audit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Audit")

next.ServeHTTP(w, r)
})
}

log.go


package xmiddleware

import (
"fmt"
"net/http"
)

func Log(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Log")

next.ServeHTTP(w, r)
})
}

router.go


package xhttp

import (
"fmt"
"net/http"
"reflect"
"runtime"
"slices"
"strconv"
"strings"
)

type route struct {
path string
handler http.HandlerFunc
middlewares []func(http.Handler) http.Handler
}

type Router struct {
handler *http.ServeMux
prefix string
routes []route
middlewares []func(http.Handler) http.Handler
}

// NewRouter returns a root level router.
func NewRouter() *Router {
return &Router{
handler: http.NewServeMux(),
}
}

// Group creates a new router group or sub group split away from its base.
func (r *Router) Group(prefix string) *Router {
return &Router{
handler: r.handler,
prefix: r.prefix + prefix,
middlewares: r.middlewares,
}
}

// Bind wires group and sub group routes to root level router.
func (r *Router) Bind(router *Router) {
r.routes = append(r.routes, router.routes...)
}

// Use registers global middleware(s). If it is used on the root level router,
// middleware(s) would apply to all routes even for groups. If it is used on a
// group level router, middleware(s) would apply to all routes even for its sub
// groups.
func (r *Router) Use(middlewares ...func(http.Handler) http.Handler) {
r.middlewares = append(r.middlewares, middlewares...)
}

// Add registers a new route and optionally middleware(s) to go with it.
func (r *Router) Add(method, path string, handler http.HandlerFunc, middlewares ...func(http.Handler) http.Handler) {
if method == "" || path == "" {
return
}

path = method + " " + r.prefix + path

if val := path[len(path)-2:]; val == " /" {
r.routes = append(r.routes, route{
path: path + "{$}",
handler: handler,
middlewares: slices.Concat(r.middlewares, middlewares),
})

return
}

r.routes = append(r.routes, route{
path: strings.TrimRight(path, "/"),
handler: handler,
middlewares: slices.Concat(r.middlewares, middlewares),
})
}

// Handler wires all registered handlers and middlewares then returns a final
// handler for server to use.
func (r *Router) Handler() http.Handler {
for _, route := range r.routes {
for i := len(route.middlewares) - 1; i >= 0; i-- {
hnd, ok := route.middlewares[i](route.handler).(http.HandlerFunc)
if !ok {
continue
}

route.handler = hnd
}

r.handler.HandleFunc(route.path, route.handler)
}

return r.handler
}

// RouteTable builds and returns a table in tabular form. This table contains
// all registered routes and their associated middleware(s) if there is any.
func (r *Router) RouteTable() string {
var (
res string
met = 6
pat = 4
han = 6
mid = 10
rot = len(strconv.Itoa(len(r.routes)))
pattern = "| %-*s | %-*s | %-*s | %-*s | %-*s |\n"
)

for _, route := range r.routes {
parts := strings.Split(route.path, " ")

if val := len(parts[0]); val > met {
met = val
}
if val := len(parts[1]); val > pat {
pat = val
}
if val := len(r.name(route.handler)); val > han {
han = val
}

for _, middleware := range route.middlewares {
if val := len(r.name(middleware)); val > mid {
mid = val
}
}
}

res += fmt.Sprintln(strings.Repeat("-", rot+met+pat+han+mid+16))
res += fmt.Sprintf(pattern, rot, "#", met, "METHOD", pat, "PATH", han, "HANDLE", mid, "MIDDLEWARE")
res += fmt.Sprintln(strings.Repeat("-", rot+met+pat+han+mid+16))

for i, route := range r.routes {
i := i + 1

var mdw string
if val := len(route.middlewares); val != 0 {
mdw = r.name(route.middlewares[0])
route.middlewares = route.middlewares[1:]
}

parts := strings.Split(route.path, " ")

res += fmt.Sprintf(pattern, rot, strconv.Itoa(i), met, parts[0], pat, parts[1], han, r.name(route.handler), mid, mdw)

for _, middleware := range route.middlewares {
res += fmt.Sprintf(pattern, rot, "", met, "", pat, "", han, "", mid, r.name(middleware))
}

res += fmt.Sprintln(strings.Repeat("-", rot+met+pat+han+mid+16))
}

return res
}

// name returns real name of a given handler or a middleware.
func (r *Router) name(val any) string {
parts := strings.Split(runtime.FuncForPC(reflect.ValueOf(val).Pointer()).Name(), "/")

return strings.TrimSuffix(parts[len(parts)-1], "-fm")
}

router_test.go


package xhttp

import (
"bytes"
"context"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func Test_Router_NewRouter(t *testing.T) {
router := NewRouter()

assert.IsType(t, &http.ServeMux{}, router.handler)
assert.Empty(t, router.prefix)
assert.Empty(t, router.routes)
assert.Empty(t, router.middlewares)
}

func Test_Router_Group(t *testing.T) {
router := NewRouter()

group := router.Group("/api")

assert.IsType(t, &http.ServeMux{}, group.handler)
assert.Equal(t, "/api", group.prefix)
assert.Empty(t, group.routes)
assert.Empty(t, group.middlewares)
}

func Test_Router_Bind(t *testing.T) {
handler := func(http.ResponseWriter, *http.Request) {}

router := NewRouter()
router.Add("GET", "/", handler)

group := router.Group("/api")
group.Add("POST", "/", handler)
router.Bind(group)

assert.Len(t, router.routes, 2)
assert.Equal(t, "", router.prefix)
assert.Equal(t, "GET /{$}", router.routes[0].path)
assert.Equal(t, "/api", group.prefix)
assert.Equal(t, "POST /api", router.routes[1].path)
}

func Test_Router_Use(t *testing.T) {
middleware := func(http.Handler) http.Handler { return nil }

router := NewRouter()
router.Use(middleware)
router.Use(middleware)

assert.Len(t, router.middlewares, 2)
}

func Test_Router_Add(t *testing.T) {
handler := func(http.ResponseWriter, *http.Request) {}
middleware := func(http.Handler) http.Handler { return nil }

router := NewRouter()
router.Use(middleware)
router.Use(middleware)
router.Add("GET", "/", handler)
router.Add("PUT", "/v1", handler, middleware)
router.Add("POST", "/v2", handler, middleware, middleware)

assert.Len(t, router.middlewares, 2)
assert.Len(t, router.routes, 3)
assert.Equal(t, "GET /{$}", router.routes[0].path)
assert.NotNil(t, router.routes[0].handler)
assert.Len(t, router.routes[0].middlewares, 2)
assert.Equal(t, "PUT /v1", router.routes[1].path)
assert.NotNil(t, router.routes[1].handler)
assert.Len(t, router.routes[1].middlewares, 3)
assert.Equal(t, "POST /v2", router.routes[2].path)
assert.NotNil(t, router.routes[2].handler)
assert.Len(t, router.routes[2].middlewares, 4)
}

func Test_Router_extensive_example(t *testing.T) {
// Middlewares -------------------------------------------------------------

globalMiddleware1 := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("GLOBAL middleware 1")
next.ServeHTTP(w, r)
})
}

globalMiddleware2 := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("GLOBAL middleware 2")
next.ServeHTTP(w, r)
})
}

apiGlobalMiddleware := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("API GLOBAL middleware")
next.ServeHTTP(w, r)
})
}

apiV1GlobalMiddleware := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("V1 GLOBAL middleware")
next.ServeHTTP(w, r)
})
}

apiV2GlobalMiddleware := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("V2 GLOBAL middleware")
next.ServeHTTP(w, r)
})
}

auditMiddleware := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("AUDIT middleware")
next.ServeHTTP(w, r)
})
}

logMiddleware := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("LOG middleware")
next.ServeHTTP(w, r)
})
}

// Handlers ----------------------------------------------------------------

homeDefaultHandler := func(w http.ResponseWriter, r *http.Request) {
log.Println("GET /")
w.Write([]byte(`GET /`))
}

apiDefaultHandler := func(w http.ResponseWriter, r *http.Request) {
log.Println("GET /api")
w.Write([]byte(`GET /api`))
}

apiV1DefaultHandler := func(w http.ResponseWriter, r *http.Request) {
log.Println("GET /api/v1")
w.Write([]byte(`GET /api/v1`))
}

apiV1CreateAdminHandler := func(w http.ResponseWriter, r *http.Request) {
log.Println("POST /api/v1/admins")
w.Write([]byte(`POST /api/v1/admins`))
}

apiV1DeleteAdminHandler := func(w http.ResponseWriter, r *http.Request) {
log.Println("DELETE /api/v1/admins/{id}")
w.Write([]byte(`DELETE /api/v1/admins/{id}`))
}

apiV1ListLogsHandler := func(w http.ResponseWriter, r *http.Request) {
log.Println("GET /api/v1/logs")
w.Write([]byte(`GET /api/v1/logs`))
}

apiV2DefaultHandler := func(w http.ResponseWriter, r *http.Request) {
log.Println("GET /api/v2")
w.Write([]byte(`GET /api/v2`))
}

apiV2CreateAdminHandler := func(w http.ResponseWriter, r *http.Request) {
log.Println("POST /api/v2/admins")
w.Write([]byte(`POST /api/v2/admins`))
}

apiV2DeleteAdminHandler := func(w http.ResponseWriter, r *http.Request) {
log.Println("DELETE /api/v2/admins/{id}")
w.Write([]byte(`DELETE /api/v2/admins/{id}`))
}

// Base --------------------------------------------------------------------
baseRouter := NewRouter()
baseRouter.Use(globalMiddleware1)
baseRouter.Use(globalMiddleware2)
baseRouter.Add("GET", "/", homeDefaultHandler) // GET /

// API ---------------------------------------------------------------------
apiRouter := baseRouter.Group("/api")
apiRouter.Use(apiGlobalMiddleware)
apiRouter.Add("GET", "/", apiDefaultHandler) // GET /api
baseRouter.Bind(apiRouter)

// V1 ----------------------------------------------------------------------
v1Router := apiRouter.Group("/v1")
v1Router.Use(apiV1GlobalMiddleware)
v1Router.Add("GET", "/", apiV1DefaultHandler) // GET /api/v1
v1Router.Add("POST", "/admins", apiV1CreateAdminHandler) // POST /api/v1/admins
v1Router.Add("DELETE", "/admins/{id}", apiV1DeleteAdminHandler, auditMiddleware) // DELET /api/v1/admins/1
baseRouter.Bind(v1Router)

v1RouterLogs := v1Router.Group("/logs")
v1RouterLogs.Use(logMiddleware)
v1RouterLogs.Add("GET", "/", apiV1ListLogsHandler) // GET /api/v1/logs
baseRouter.Bind(v1RouterLogs)

// V2 ----------------------------------------------------------------------
v2Router := apiRouter.Group("/v2")
v2Router.Use(apiV2GlobalMiddleware)
v2Router.Add("GET", "/", apiV2DefaultHandler) // GET /api/v2
v2Router.Add("POST", "/admins", apiV2CreateAdminHandler) // POST /api/v2/admins
v2Router.Add("DELETE", "/admins/{id}", apiV2DeleteAdminHandler, auditMiddleware) // DELET /api/v2/admins/1
baseRouter.Bind(v2Router)

// -------------------------------------------------------------------------

var buf bytes.Buffer
log.SetFlags(0)
log.SetOutput(&buf)
defer func() {
log.SetOutput(os.Stderr)
}()

srv := httptest.NewServer(baseRouter.Handler())
defer srv.Close()

tests := []struct {
name string
haveMethod string
havePath string
wantStatus int
wantBody string
wantOutput string
}{
{
name: "home default",
haveMethod: "GET",
havePath: "",
wantStatus: 200,
wantBody: "GET /",
wantOutput: `
GLOBAL middleware 1
GLOBAL middleware 2
GET /`,
},
{
name: "home default with slash",
haveMethod: "GET",
havePath: "/",
wantStatus: 200,
wantBody: "GET /",
wantOutput: `
GLOBAL middleware 1
GLOBAL middleware 2
GET /`,
},
{
name: "home default invalid method",
haveMethod: "PATCH",
havePath: "/",
wantStatus: 405,
wantBody: "Method Not Allowed\n",
wantOutput: "",
},
{
name: "api default",
haveMethod: "GET",
havePath: "/api",
wantStatus: 200,
wantBody: "GET /api",
wantOutput: `
GLOBAL middleware 1
GLOBAL middleware 2
API GLOBAL middleware
GET /api`,
},
{
name: "api default invalid route",
haveMethod: "GET",
havePath: "/api/",
wantStatus: 404,
wantBody: "404 page not found\n",
wantOutput: "",
},
{
name: "api default invalid method",
haveMethod: "PATCH",
havePath: "/api",
wantStatus: 405,
wantBody: "Method Not Allowed\n",
wantOutput: "",
},
{
name: "api v1 default",
haveMethod: "GET",
havePath: "/api/v1",
wantStatus: 200,
wantBody: "GET /api/v1",
wantOutput: `
GLOBAL middleware 1
GLOBAL middleware 2
API GLOBAL middleware
V1 GLOBAL middleware
GET /api/v1`,
},
{
name: "api v1 default invalid route",
haveMethod: "GET",
havePath: "/api/v1/",
wantStatus: 404,
wantBody: "404 page not found\n",
wantOutput: "",
},
{
name: "api v1 default invalid method",
haveMethod: "PATCH",
havePath: "/api/v1",
wantStatus: 405,
wantBody: "Method Not Allowed\n",
wantOutput: "",
},
{
name: "api v1 create admin",
haveMethod: "POST",
havePath: "/api/v1/admins",
wantStatus: 200,
wantBody: "POST /api/v1/admins",
wantOutput: `
GLOBAL middleware 1
GLOBAL middleware 2
API GLOBAL middleware
V1 GLOBAL middleware
POST /api/v1/admins`,
},
{
name: "api v1 create admin invalid path",
haveMethod: "POST",
havePath: "/api/v1/admins/",
wantStatus: 404,
wantBody: "404 page not found\n",
wantOutput: "",
},
{
name: "api v1 create admin invalid method",
haveMethod: "PATCH",
havePath: "/api/v1/admins",
wantStatus: 405,
wantBody: "Method Not Allowed\n",
wantOutput: "",
},
{
name: "api v1 delete admin",
haveMethod: "DELETE",
havePath: "/api/v1/admins/{id}",
wantStatus: 200,
wantBody: "DELETE /api/v1/admins/{id}",
wantOutput: `
GLOBAL middleware 1
GLOBAL middleware 2
API GLOBAL middleware
V1 GLOBAL middleware
AUDIT middleware
DELETE /api/v1/admins/{id}`,
},
{
name: "api v1 delete admin invalid path",
haveMethod: "DELETE",
havePath: "/api/v1/admins/{id}/",
wantStatus: 404,
wantBody: "404 page not found\n",
wantOutput: "",
},
{
name: "api v1 delete admin invalid method",
haveMethod: "PATCH",
havePath: "/api/v1/admins/{id}",
wantStatus: 405,
wantBody: "Method Not Allowed\n",
wantOutput: "",
},
{
name: "api v1 list logs",
haveMethod: "GET",
havePath: "/api/v1/logs",
wantStatus: 200,
wantBody: "GET /api/v1/logs",
wantOutput: `
GLOBAL middleware 1
GLOBAL middleware 2
API GLOBAL middleware
V1 GLOBAL middleware
LOG middleware
GET /api/v1/logs`,
},
{
name: "api v1 list logs invalid path",
haveMethod: "GET",
havePath: "/api/v1/logs/",
wantStatus: 404,
wantBody: "404 page not found\n",
wantOutput: "",
},
{
name: "api v1 list logs invalid method",
haveMethod: "PATCH",
havePath: "/api/v1/logs",
wantStatus: 405,
wantBody: "Method Not Allowed\n",
wantOutput: "",
},
{
name: "api v2 default",
haveMethod: "GET",
havePath: "/api/v2",
wantStatus: 200,
wantBody: "GET /api/v2",
wantOutput: `
GLOBAL middleware 1
GLOBAL middleware 2
API GLOBAL middleware
V2 GLOBAL middleware
GET /api/v2`,
},
{
name: "api v2 default invalid path",
haveMethod: "GET",
havePath: "/api/v2/",
wantStatus: 404,
wantBody: "404 page not found\n",
wantOutput: "",
},
{
name: "api v2 default invalid method",
haveMethod: "PATCH",
havePath: "/api/v2",
wantStatus: 405,
wantBody: "Method Not Allowed\n",
wantOutput: "",
},
{
name: "api v2 create admin",
haveMethod: "POST",
havePath: "/api/v2/admins",
wantStatus: 200,
wantBody: "POST /api/v2/admins",
wantOutput: `
GLOBAL middleware 1
GLOBAL middleware 2
API GLOBAL middleware
V2 GLOBAL middleware
POST /api/v2/admins`,
},
{
name: "api v2 create admin invalid path",
haveMethod: "POST",
havePath: "/api/v2/admins/",
wantStatus: 404,
wantBody: "404 page not found\n",
wantOutput: "",
},
{
name: "api v2 create admin invalid method",
haveMethod: "PATCH",
havePath: "/api/v2/admins",
wantStatus: 405,
wantBody: "Method Not Allowed\n",
wantOutput: "",
},
{
name: "api v2 delete admin",
haveMethod: "DELETE",
havePath: "/api/v2/admins/{id}",
wantStatus: 200,
wantBody: "DELETE /api/v2/admins/{id}",
wantOutput: `
GLOBAL middleware 1
GLOBAL middleware 2
API GLOBAL middleware
V2 GLOBAL middleware
AUDIT middleware
DELETE /api/v2/admins/{id}`,
},
{
name: "api v2 delete admin invalid path",
haveMethod: "DELETE",
havePath: "/api/v2/admins/{id}/",
wantStatus: 404,
wantBody: "404 page not found\n",
wantOutput: "",
},
{
name: "api v2 delete admin invalid method",
haveMethod: "PATCH",
havePath: "/api/v2/admins/{id}",
wantStatus: 405,
wantBody: "Method Not Allowed\n",
wantOutput: "",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
buf.Reset()

req, err := http.NewRequestWithContext(context.Background(), test.haveMethod, srv.URL+test.havePath, nil)
assert.NoError(t, err)

res, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.Equal(t, test.wantStatus, res.StatusCode)

bdy, err := io.ReadAll(res.Body)
res.Body.Close()
assert.NoError(t, err)
assert.Equal(t, test.wantBody, string(bdy))

wantOutput := strings.ReplaceAll(test.wantOutput, "\n", "")
wantOutput = strings.ReplaceAll(wantOutput, "\t", "")
haveOutput := strings.ReplaceAll(buf.String(), "\n", "")
assert.Equal(t, wantOutput, haveOutput)
})
}
}