Skip to content

Commit

Permalink
feat: add support for delayed responses (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
juan131 authored Nov 16, 2023
1 parent b5d19fb commit 7cae706
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 10 deletions.
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ COPY dist/api-mock_linux_${TARGETARCH}*/api-mock /usr/local/bin/
USER 1001
EXPOSE 8080
ENV API_TOKEN="" \
LOG_LEVEL="info" \
FAILURE_RESP_BODY="{\"success\": false}" \
FAILURE_RESP_CODE=400 \
METHODS="GET,POST" \
RESP_DELAY=0 \
SUB_ROUTES="" \
SUCCESS_RESP_BODY="{\"success\": true}" \
SUCCESS_RESP_CODE=200 \
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ The API mock can be configured with the following environment variables:
| Variable | Description | Default |
| -------- | ----------- | ------- |
| `PORT` | The port to listen on | `8080` |
| `LOG_LEVEL` | The log level | `info` |
| `API_TOKEN` | Bearer token to authenticate requests | `` |
| `FAILURE_RESP_BODY` | The response body to return when mocking a failure | `{"success": "false"}` |
| `FAILURE_RESP_CODE` | The HTTP status code to return when mocking a failure | `400` |
| `SUCCESS_RESP_BODY` | The response body to return when mocking a success | `{"success": "true"}` |
| `SUCCESS_RESP_CODE` | The HTTP status code to return when mocking a success | `200` |
| `SUCCESS_RATIO` | The ratio of success to failure responses | `1.0` |
| `METHODS` | The HTTP methods to mock | `GET,POST` |
| `RESP_DELAY` | The response delay (in milliseconds) | `0` |
| `SUB_ROUTES` | The sub routes to mock | `` |
| `RATE_LIMIT` | The API rate limit (requests per second) | `1000` |
| `RATE_EXCEEDED_RESP_BODY` | The response body to return when mocking a rate exceeded | `{"success": "false", "error": "rate limit exceeded"}` |
Expand Down
3 changes: 2 additions & 1 deletion internal/service/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (
)

// newStructuredLogger creates a new structured logger compatible with GCP logging syntax
func newStructuredLogger() *slog.Logger {
func newStructuredLogger(level slog.Level) *slog.Logger {
return slog.New(
slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
Level: level,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
switch a.Key {
case slog.LevelKey:
Expand Down
6 changes: 6 additions & 0 deletions internal/service/r_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"net/http"
"net/url"
"time"

"github.com/go-chi/render"

Expand All @@ -28,6 +29,11 @@ func (svc *service) incReqCounter() func(next http.Handler) http.Handler {
// handleMock mocks request handling
// Route: /v1/mock/*
func (svc *service) handleMock(w http.ResponseWriter, r *http.Request) {
// Delay response
if svc.cfg.respDelay > 0 {
time.Sleep(svc.cfg.respDelay)
}

// Return failure based on success ratio and requests counter
if shouldFail(svc.cfg.successRatio, svc.reqCounter) {
render.Status(r, svc.cfg.failureCode)
Expand Down
9 changes: 5 additions & 4 deletions internal/service/r_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package service
import (
"bytes"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
Expand All @@ -23,7 +24,7 @@ func Test_service_handleMock(t *testing.T) {
methods: []string{http.MethodGet},
subRoutes: []string{"/foo"},
},
logger: newStructuredLogger(),
logger: newStructuredLogger(slog.LevelDebug),
}

tests := []struct {
Expand Down Expand Up @@ -129,7 +130,7 @@ func Test_service_handleBatchMock(t *testing.T) {
methods: []string{http.MethodGet},
subRoutes: []string{"/foo"},
},
logger: newStructuredLogger(),
logger: newStructuredLogger(slog.LevelDebug),
}

tests := []struct {
Expand Down Expand Up @@ -194,7 +195,7 @@ func Test_service_handleBatchMock(t *testing.T) {

func Test_handleNotFound(t *testing.T) {
svc := &service{
logger: newStructuredLogger(),
logger: newStructuredLogger(slog.LevelDebug),
}
tests := []struct {
name string
Expand Down Expand Up @@ -239,7 +240,7 @@ func Test_handleNotFound(t *testing.T) {

func Test_handleMethodNotAllowed(t *testing.T) {
svc := &service{
logger: newStructuredLogger(),
logger: newStructuredLogger(slog.LevelDebug),
}
tests := []struct {
name string
Expand Down
34 changes: 30 additions & 4 deletions internal/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type SvcConfig struct {
port int // server listening port
apiToken string // api token
methods, subRoutes []string // supported sub-routes
respDelay time.Duration // response delay in milliseconds
failureCode int // response code for failed requests
failureRespBody map[string]interface{} // response body for failed requests
successCode int // response code for successful requests
Expand All @@ -70,10 +71,22 @@ func (svc *service) ListenAndServe() {

// Make makes a Service struct which wraps all callable methods encompassing the mock service
func Make(cfg *SvcConfig) Service {
return &service{
cfg: cfg,
logger: newStructuredLogger(),
svc := service{
cfg: cfg,
}

switch os.Getenv("LOG_LEVEL") {
case "debug":
svc.logger = newStructuredLogger(slog.LevelDebug)
case "warn":
svc.logger = newStructuredLogger(slog.LevelWarn)
case "error":
svc.logger = newStructuredLogger(slog.LevelError)
default:
svc.logger = newStructuredLogger(slog.LevelInfo)
}

return &svc
}

// LoadConfigFromEnv loads the configuration from the environment.
Expand All @@ -89,12 +102,25 @@ func LoadConfigFromEnv() (*SvcConfig, error) {
if portENV != "" {
svc.port, err = strconv.Atoi(portENV)
if err != nil {
return nil, fmt.Errorf("invalid port format for PORT: %w", err)
return nil, fmt.Errorf("invalid int format for PORT: %w", err)
}
} else {
svc.port = defaultPort
}

respDelayENV := os.Getenv("RESP_DELAY")
if respDelayENV != "" {
respDelayINT, err := strconv.Atoi(respDelayENV)
if err != nil {
return nil, fmt.Errorf("invalid int format for RESP_DELAY: %w", err)
}

svc.respDelay = time.Duration(respDelayINT) * time.Millisecond
if svc.respDelay > 30*time.Second {
return nil, fmt.Errorf("RESP_DELAY cannot be greater than 30 seconds")
}
}

failureCodeEnv := os.Getenv("FAILURE_RESP_CODE")
if failureCodeEnv != "" {
svc.failureCode, err = strconv.Atoi(failureCodeEnv)
Expand Down
11 changes: 10 additions & 1 deletion internal/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

func Test_LoadConfigFromEnv(t *testing.T) {
type env struct {
port, apiToken, failureRespCode, failureRespBody, successRespCode, successRespBody, successRatio, rateLimit, rateExceededRespBody, methods, subRoutes string
port, apiToken, respDelay, failureRespCode, failureRespBody, successRespCode, successRespBody, successRatio, rateLimit, rateExceededRespBody, methods, subRoutes string
}
tests := []struct {
name string
Expand Down Expand Up @@ -68,6 +68,14 @@ func Test_LoadConfigFromEnv(t *testing.T) {
want: nil,
wantErr: true,
},
{
name: "Invalid response delay",
env: env{
respDelay: "not-a-number",
},
want: nil,
wantErr: true,
},
{
name: "Invalid failure response code",
env: env{
Expand Down Expand Up @@ -137,6 +145,7 @@ func Test_LoadConfigFromEnv(t *testing.T) {
t.Run(test.name, func(tt *testing.T) {
tt.Setenv("PORT", test.env.port)
tt.Setenv("API_TOKEN", test.env.apiToken)
tt.Setenv("RESP_DELAY", test.env.respDelay)
tt.Setenv("FAILURE_RESP_CODE", test.env.failureRespCode)
tt.Setenv("FAILURE_RESP_BODY", test.env.failureRespBody)
tt.Setenv("SUCCESS_RESP_CODE", test.env.successRespCode)
Expand Down

0 comments on commit 7cae706

Please sign in to comment.