Skip to content

Commit f33e331

Browse files
committed
fix: Add reflect and server endpoints
1 parent 2744eed commit f33e331

File tree

7 files changed

+275
-6
lines changed

7 files changed

+275
-6
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![Build](https://github.com/NdoleStudio/httpmock/actions/workflows/ci.yml/badge.svg)](https://github.com/NdoleStudio/httpmock/actions/workflows/ci.yml)
44
[![GitHub contributors](https://img.shields.io/github/contributors/NdoleStudio/httpmock)](https://github.com/NdoleStudio/httpmock/graphs/contributors)
55
[![GitHub license](https://img.shields.io/github/license/NdoleStudio/httpmock?color=brightgreen)](https://github.com/NdoleStudio/httpmock/blob/master/LICENSE)
6-
![Docker Pulls](https://img.shields.io/docker/pulls/ndolestudio/httpmock)
6+
[![Docker Pulls](https://img.shields.io/docker/pulls/ndolestudio/httpmock)](https://hub.docker.com/r/ndolestudio/httpmock)
77
[![Netlify Status](https://api.netlify.com/api/v1/badges/6a751c80-ac38-4fa0-a470-3d2a69f98dfc/deploy-status)](https://app.netlify.com/sites/httpmock/deploys)
88

99
This is a mock http server which can be used to test HTTP requests and responses when building an HTTP client.

api/pkg/di/container.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,14 @@ func (container *Container) App() (app *fiber.App) {
143143
},
144144
))
145145
app.Use(cors.New())
146-
app.Use(middlewares.RequestRouter(container.Tracer(), container.Logger(), os.Getenv("APP_HOSTNAME"), container.ProjectEndpointRequestService()))
146+
app.Use(middlewares.RequestRouter(
147+
container.Tracer(),
148+
container.Logger(),
149+
os.Getenv("APP_HOSTNAME"),
150+
container.ProjectEndpointRequestService(),
151+
container.ServerHandler().Handle,
152+
container.ReflectHandler().Handle,
153+
))
147154
app.Use(healthcheck.New())
148155

149156
container.app = app
@@ -152,6 +159,8 @@ func (container *Container) App() (app *fiber.App) {
152159
container.RegisterProjectRoutes()
153160
container.RegisterProjectEndpointRoutes()
154161
container.RegisterProjectEndpointRequestRoutes()
162+
container.RegisterReflectRoutes()
163+
container.RegisterServerRoutes()
155164

156165
// UnAuthenticated routes
157166
container.RegisterLemonsqueezyRoutes()
@@ -296,6 +305,18 @@ func (container *Container) RegisterProjectEndpointRequestRoutes() {
296305
container.ProjectEndpointRequestHandler().RegisterRoutes(container.App(), container.ClerkBearerAuthMiddlewares())
297306
}
298307

308+
// RegisterReflectRoutes registers routes for the /reflect
309+
func (container *Container) RegisterReflectRoutes() {
310+
container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.ReflectHandler{}))
311+
container.ReflectHandler().RegisterRoutes(container.App())
312+
}
313+
314+
// RegisterServerRoutes registers routes for the /server
315+
func (container *Container) RegisterServerRoutes() {
316+
container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.ServerHandler{}))
317+
container.ServerHandler().RegisterRoutes(container.App())
318+
}
319+
299320
// ProjectHandler creates a new instance of handlers.ProjectHandler
300321
func (container *Container) ProjectHandler() (handler *handlers.ProjectHandler) {
301322
container.logger.Debug(fmt.Sprintf("creating %T", handler))
@@ -319,6 +340,24 @@ func (container *Container) ProjectEndpointHandler() (handler *handlers.ProjectE
319340
)
320341
}
321342

343+
// ReflectHandler creates a new instance of handlers.ReflectHandler
344+
func (container *Container) ReflectHandler() (handler *handlers.ReflectHandler) {
345+
container.logger.Debug(fmt.Sprintf("creating %T", handler))
346+
return handlers.NewReflectHandler(
347+
container.Logger(),
348+
container.Tracer(),
349+
)
350+
}
351+
352+
// ServerHandler creates a new instance of handlers.ServerHandler
353+
func (container *Container) ServerHandler() (handler *handlers.ServerHandler) {
354+
container.logger.Debug(fmt.Sprintf("creating %T", handler))
355+
return handlers.NewServerHandler(
356+
container.Logger(),
357+
container.Tracer(),
358+
)
359+
}
360+
322361
// ProjectEndpointRequestHandler creates a new instance of handlers.ProjectEndpointRequestHandler
323362
func (container *Container) ProjectEndpointRequestHandler() (handler *handlers.ProjectEndpointRequestHandler) {
324363
container.logger.Debug(fmt.Sprintf("creating %T", handler))
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package handlers
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/NdoleStudio/httpmock/pkg/telemetry"
10+
"github.com/gofiber/fiber/v2"
11+
"github.com/palantir/stacktrace"
12+
)
13+
14+
// ReflectHandler handles requests to the /reflect* endpoints
15+
type ReflectHandler struct {
16+
handler
17+
logger telemetry.Logger
18+
tracer telemetry.Tracer
19+
}
20+
21+
// NewReflectHandler creates a new ServerHandler
22+
func NewReflectHandler(
23+
logger telemetry.Logger,
24+
tracer telemetry.Tracer,
25+
) (h *ReflectHandler) {
26+
return &ReflectHandler{
27+
logger: logger.WithCodeNamespace(fmt.Sprintf("%T", h)),
28+
tracer: tracer,
29+
}
30+
}
31+
32+
// RegisterRoutes registers the routes for the MessageHandler
33+
func (h *ReflectHandler) RegisterRoutes(app *fiber.App) {
34+
router := app.Group("/reflect")
35+
router.All("/*", h.Handle)
36+
}
37+
38+
// Handle handles the request
39+
func (h *ReflectHandler) Handle(c *fiber.Ctx) error {
40+
_, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
41+
defer span.End()
42+
43+
for _, header := range h.getResponseHeaders(c) {
44+
for key, value := range header {
45+
c.Response().Header.Set(key, value)
46+
}
47+
}
48+
49+
c.Response().SetStatusCode(h.getResponseStatus(ctxLogger, c))
50+
51+
if len(c.Body()) > 0 {
52+
if _, err := c.Response().BodyWriter().Write(c.Body()); err != nil {
53+
msg := fmt.Sprintf("error while writing response body [%s] for request [%s] with method [%s]", c.Body(), c.BaseURL()+c.OriginalURL(), c.Method())
54+
ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))
55+
}
56+
}
57+
58+
return nil
59+
}
60+
61+
func (h *ReflectHandler) getResponseStatus(ctxLogger telemetry.Logger, c *fiber.Ctx) int {
62+
status := strings.TrimSpace(c.Get("response-status"))
63+
if status == "" {
64+
return fiber.StatusOK
65+
}
66+
67+
statusCode, err := strconv.Atoi(status)
68+
if err != nil || statusCode < 100 || statusCode > 599 {
69+
msg := fmt.Sprintf("The request has an invalid 'response-status' header [%s], defaulting to [%d]", c.Get("response-status"), fiber.StatusOK)
70+
ctxLogger.Warn(errors.New(msg))
71+
return fiber.StatusOK
72+
}
73+
return statusCode
74+
}
75+
76+
func (h *ReflectHandler) getResponseHeaders(c *fiber.Ctx) []map[string]string {
77+
var headers []map[string]string
78+
for key, value := range c.GetReqHeaders() {
79+
for _, header := range value {
80+
headers = append(headers, map[string]string{key: header})
81+
}
82+
}
83+
return headers
84+
}

api/pkg/handlers/server_handler.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"strconv"
8+
"strings"
9+
"time"
10+
11+
"github.com/NdoleStudio/httpmock/pkg/telemetry"
12+
"github.com/gofiber/fiber/v2"
13+
"github.com/palantir/stacktrace"
14+
)
15+
16+
// ServerHandler handles requests to the /server* endpoints
17+
type ServerHandler struct {
18+
handler
19+
logger telemetry.Logger
20+
tracer telemetry.Tracer
21+
}
22+
23+
// NewServerHandler creates a new ServerHandler
24+
func NewServerHandler(
25+
logger telemetry.Logger,
26+
tracer telemetry.Tracer,
27+
) (h *ServerHandler) {
28+
return &ServerHandler{
29+
logger: logger.WithCodeNamespace(fmt.Sprintf("%T", h)),
30+
tracer: tracer,
31+
}
32+
}
33+
34+
// RegisterRoutes registers the routes for the MessageHandler
35+
func (h *ServerHandler) RegisterRoutes(app *fiber.App) {
36+
router := app.Group("/server")
37+
router.All("/*", h.Handle)
38+
}
39+
40+
// Handle handles the request
41+
func (h *ServerHandler) Handle(c *fiber.Ctx) error {
42+
_, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger)
43+
defer span.End()
44+
45+
stopwatch := time.Now()
46+
headers := h.getResponseHeaders(ctxLogger, c)
47+
48+
delay := h.getResponseDelay(ctxLogger, c) - time.Since(stopwatch)
49+
if delay > 0 {
50+
time.Sleep(delay)
51+
}
52+
53+
ctxLogger.Debug(fmt.Sprintf("finished handling request with URL [%s] in [%s]", c.BaseURL()+c.OriginalURL(), time.Since(stopwatch).String()))
54+
55+
for _, header := range headers {
56+
for key, value := range header {
57+
c.Response().Header.Set(key, value)
58+
}
59+
}
60+
61+
c.Response().SetStatusCode(h.getResponseStatus(ctxLogger, c))
62+
63+
responseBody := strings.TrimSpace(c.Get("response-body"))
64+
if responseBody != "" {
65+
if _, err := c.Response().BodyWriter().Write([]byte(responseBody)); err != nil {
66+
msg := fmt.Sprintf("error while writing response body for request [%s] with method [%s]", c.BaseURL()+c.OriginalURL(), c.Method())
67+
ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))
68+
}
69+
}
70+
71+
return nil
72+
}
73+
74+
func (h *ServerHandler) getResponseHeaders(ctxLogger telemetry.Logger, c *fiber.Ctx) []map[string]string {
75+
var headers []map[string]string
76+
77+
headersString := strings.TrimSpace(c.Get("response-headers"))
78+
if headersString == "" {
79+
return headers
80+
}
81+
82+
if err := json.Unmarshal([]byte(headersString), &headers); err != nil {
83+
msg := fmt.Sprintf("Failed to parse headers [%s] into type [%T]", headersString, headers)
84+
ctxLogger.Warn(errors.New(msg))
85+
return headers
86+
}
87+
88+
return headers
89+
}
90+
91+
func (h *ServerHandler) getResponseDelay(ctxLogger telemetry.Logger, c *fiber.Ctx) time.Duration {
92+
delay, err := strconv.Atoi(strings.TrimSpace(c.Get("response-delay")))
93+
if err != nil || delay < 0 || delay > 100000 {
94+
msg := fmt.Sprintf("The request has an invalid 'response-delay' header [%s], defaulting to 0", c.Get("response-delay"))
95+
ctxLogger.Warn(errors.New(msg))
96+
return 0
97+
}
98+
return time.Duration(delay) * time.Millisecond
99+
}
100+
101+
func (h *ServerHandler) getResponseStatus(ctxLogger telemetry.Logger, c *fiber.Ctx) int {
102+
status := strings.TrimSpace(c.Get("response-status"))
103+
if status == "" {
104+
return fiber.StatusOK
105+
}
106+
107+
statusCode, err := strconv.Atoi(status)
108+
if err != nil || statusCode < 100 || statusCode > 599 {
109+
msg := fmt.Sprintf("The request has an invalid 'response-status' header [%s], defaulting to [%d]", c.Get("response-status"), fiber.StatusOK)
110+
ctxLogger.Warn(errors.New(msg))
111+
return fiber.StatusOK
112+
}
113+
return statusCode
114+
}

api/pkg/middlewares/request_router_middleware.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package middlewares
22

33
import (
44
"fmt"
5+
"strconv"
6+
"strings"
57
"time"
68

79
"github.com/NdoleStudio/httpmock/pkg/repositories"
@@ -12,7 +14,14 @@ import (
1214
)
1315

1416
// RequestRouter handles requests to the server
15-
func RequestRouter(tracer telemetry.Tracer, logger telemetry.Logger, hostname string, requestService *services.ProjectEndpointRequestService) fiber.Handler {
17+
func RequestRouter(
18+
tracer telemetry.Tracer,
19+
logger telemetry.Logger,
20+
hostname string,
21+
requestService *services.ProjectEndpointRequestService,
22+
serverHandler fiber.Handler,
23+
reflectHandler fiber.Handler,
24+
) fiber.Handler {
1625
return func(c *fiber.Ctx) error {
1726
stopwatch := time.Now()
1827

@@ -29,6 +38,10 @@ func RequestRouter(tracer telemetry.Tracer, logger telemetry.Logger, hostname st
2938
return c.Next()
3039
}
3140

41+
if len(c.Subdomains()[0]) < 8 {
42+
return handleNamedSubdomains(c, strings.TrimSpace(c.Subdomains()[0]), serverHandler, reflectHandler)
43+
}
44+
3245
endpoint, err := requestService.LoadByRequest(ctx, c.Subdomains()[0], c.Method(), c.Path())
3346
if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
3447
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
@@ -51,3 +64,22 @@ func RequestRouter(tracer telemetry.Tracer, logger telemetry.Logger, hostname st
5164
return nil
5265
}
5366
}
67+
68+
func handleNamedSubdomains(c *fiber.Ctx, subdomain string, serverHandler fiber.Handler, reflectHandler fiber.Handler) error {
69+
switch subdomain {
70+
case "reflect":
71+
return reflectHandler(c)
72+
case "server":
73+
return serverHandler(c)
74+
}
75+
76+
if status, err := strconv.Atoi(subdomain); err == nil && status >= 100 && status <= 599 {
77+
c.Request().Header.Set("response-status", subdomain)
78+
return serverHandler(c)
79+
}
80+
81+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
82+
"status": "error",
83+
"message": fmt.Sprintf("We cannot find a registered mock for URL [%s] and HTTP method [%s]", c.BaseURL()+c.OriginalURL(), c.Method()),
84+
})
85+
}

api/pkg/validators/project_handler_validator.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func (validator *ProjectHandlerValidator) ValidateUpdate(ctx context.Context, re
5555
"subdomain": []string{
5656
"required",
5757
"alpha_dash",
58-
"min:7",
58+
"min:8",
5959
"max:30",
6060
},
6161
"description": []string{
@@ -102,7 +102,7 @@ func (validator *ProjectHandlerValidator) ValidateCreate(ctx context.Context, re
102102
"subdomain": []string{
103103
"required",
104104
"alpha_dash",
105-
"min:7",
105+
"min:8",
106106
"max:30",
107107
},
108108
"description": []string{

web/src/app/projects/[project-id]/endpoints/[endpoint-id]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export default function EndpointShow() {
226226
}
227227
try {
228228
return JSON.stringify(JSON.parse(body), null, 4);
229-
} catch (e) {
229+
} catch (_) {
230230
return body;
231231
}
232232
};

0 commit comments

Comments
 (0)