From bd8b7839499061911be5e6b53f07abc55695f3eb Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:39:25 +0100 Subject: [PATCH 1/4] Add api key authentication --- .mockery.yaml | 3 +-- app/app.go | 5 +++-- app/lambda/handler.go | 3 ++- app/lambda/module.go | 3 ++- app/standalone/module.go | 3 ++- cmd/lambda.go | 3 ++- cmd/root.go | 5 +++-- cmd/run.go | 3 ++- cmd/serve.go | 3 ++- config/config.go | 8 ++++++++ handler/handler.go | 14 +++++++++++++- internal/execution/dispatcher.go | 3 ++- .../execution/dispatcher/dispatcher_dedicated.go | 3 ++- internal/execution/dispatcher/dispatcher_pooled.go | 3 ++- .../execution/dispatcher/dispatcher_pooled_test.go | 5 +++-- internal/execution/supervisor/adapter.go | 3 ++- internal/execution/supervisor/adapter_file.go | 3 ++- internal/execution/supervisor/adapter_file_test.go | 3 ++- internal/execution/supervisor/adapter_rpc.go | 3 ++- internal/execution/supervisor/adapter_rpc_test.go | 3 ++- internal/execution/supervisor/adapter_test.go | 3 ++- internal/execution/supervisor/supervisor.go | 3 ++- internal/execution/supervisor/supervisor_test.go | 3 ++- internal/execution/worker/worker_test.go | 5 +++-- main.go | 1 + runtime/handler.go | 3 ++- runtime/handler_validate.go | 3 ++- runtime/runtime.go | 3 ++- util/conf/parse.go | 3 ++- util/truthy_test.go | 3 ++- 30 files changed, 79 insertions(+), 33 deletions(-) diff --git a/.mockery.yaml b/.mockery.yaml index 05fb41c..deb6671 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -26,5 +26,4 @@ packages: inpackage: True dir: "{{.InterfaceDir}}" interfaces: - Dispatcher: -# testonly: True \ No newline at end of file + Dispatcher: \ No newline at end of file diff --git a/app/app.go b/app/app.go index b8afedb..0bd7762 100644 --- a/app/app.go +++ b/app/app.go @@ -1,13 +1,14 @@ package app import ( + "github.com/urfave/cli/v2" + "go.uber.org/fx" + "github.com/lambda-feedback/shimmy/config" "github.com/lambda-feedback/shimmy/internal/shell" "github.com/lambda-feedback/shimmy/runtime" "github.com/lambda-feedback/shimmy/util/conf" "github.com/lambda-feedback/shimmy/util/logging" - "github.com/urfave/cli/v2" - "go.uber.org/fx" ) func New(ctx *cli.Context) (*shell.Shell, error) { diff --git a/app/lambda/handler.go b/app/lambda/handler.go index b8a7b6b..9deedf4 100644 --- a/app/lambda/handler.go +++ b/app/lambda/handler.go @@ -7,9 +7,10 @@ import ( "github.com/aws/aws-lambda-go/lambda" "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" - "github.com/lambda-feedback/shimmy/internal/server" "go.uber.org/fx" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/server" ) // LambdaHandlerParams represents the parameters required for diff --git a/app/lambda/module.go b/app/lambda/module.go index 474528f..1ed820a 100644 --- a/app/lambda/module.go +++ b/app/lambda/module.go @@ -1,9 +1,10 @@ package lambda import ( + "go.uber.org/fx" + "github.com/lambda-feedback/shimmy/handler" "github.com/lambda-feedback/shimmy/util/logging" - "go.uber.org/fx" ) func Module(config Config) fx.Option { diff --git a/app/standalone/module.go b/app/standalone/module.go index 61c4365..e4be75c 100644 --- a/app/standalone/module.go +++ b/app/standalone/module.go @@ -1,10 +1,11 @@ package standalone import ( + "go.uber.org/fx" + "github.com/lambda-feedback/shimmy/handler" "github.com/lambda-feedback/shimmy/internal/server" "github.com/lambda-feedback/shimmy/util/logging" - "go.uber.org/fx" ) func Module(config Config) fx.Option { diff --git a/cmd/lambda.go b/cmd/lambda.go index d3d1b8f..7d78342 100644 --- a/cmd/lambda.go +++ b/cmd/lambda.go @@ -1,11 +1,12 @@ package cmd import ( + "github.com/urfave/cli/v2" + "github.com/lambda-feedback/shimmy/app" "github.com/lambda-feedback/shimmy/app/lambda" "github.com/lambda-feedback/shimmy/util/conf" "github.com/lambda-feedback/shimmy/util/logging" - "github.com/urfave/cli/v2" ) var ( diff --git a/cmd/root.go b/cmd/root.go index 8150606..396a482 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,11 +6,12 @@ import ( "os" "time" + "github.com/urfave/cli/v2" + "go.uber.org/zap" + "github.com/lambda-feedback/shimmy/config" "github.com/lambda-feedback/shimmy/util/conf" "github.com/lambda-feedback/shimmy/util/logging" - "github.com/urfave/cli/v2" - "go.uber.org/zap" ) var ( diff --git a/cmd/run.go b/cmd/run.go index 57e0449..0b01be8 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -3,8 +3,9 @@ package cmd import ( "os" - "github.com/lambda-feedback/shimmy/util/logging" "github.com/urfave/cli/v2" + + "github.com/lambda-feedback/shimmy/util/logging" ) var ( diff --git a/cmd/serve.go b/cmd/serve.go index eec2365..2d521c9 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -1,11 +1,12 @@ package cmd import ( + "github.com/urfave/cli/v2" + "github.com/lambda-feedback/shimmy/app" "github.com/lambda-feedback/shimmy/app/standalone" "github.com/lambda-feedback/shimmy/util/conf" "github.com/lambda-feedback/shimmy/util/logging" - "github.com/urfave/cli/v2" ) var ( diff --git a/config/config.go b/config/config.go index 8bcec26..020d38e 100644 --- a/config/config.go +++ b/config/config.go @@ -8,6 +8,11 @@ const ( JSON MessageEncoding = "json" ) +type AuthConfig struct { + // Key is the secret key for the application + Key string `conf:"key"` +} + type Config struct { // LogLevel is the log level for the application LogLevel string `conf:"log_level"` @@ -17,4 +22,7 @@ type Config struct { // Runtime is the runtime configuration Runtime runtime.Config `conf:"runtime"` + + // Auth is the authentication configuration + Auth AuthConfig `conf:"auth"` } diff --git a/handler/handler.go b/handler/handler.go index 36ebe5f..7381c60 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -4,27 +4,32 @@ import ( "io" "net/http" - "github.com/lambda-feedback/shimmy/runtime" "go.uber.org/fx" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/config" + "github.com/lambda-feedback/shimmy/runtime" ) type CommandHandlerParams struct { fx.In Handler runtime.Handler + Config config.Config Log *zap.Logger } func NewCommandHandler(params CommandHandlerParams) *CommandHandler { return &CommandHandler{ handler: params.Handler, + config: params.Config, log: params.Log, } } type CommandHandler struct { handler runtime.Handler + config config.Config log *zap.Logger } @@ -34,6 +39,13 @@ func (h *CommandHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { zap.String("method", r.Method), ) + // Check for authorization + if h.config.Auth.Key != "" && r.Header.Get("api-key") != h.config.Auth.Key { + log.Debug("unauthorized request") + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + body, err := io.ReadAll(r.Body) if err != nil { log.Debug("failed to read body", zap.Error(err)) diff --git a/internal/execution/dispatcher.go b/internal/execution/dispatcher.go index c1b03e7..300ca3f 100644 --- a/internal/execution/dispatcher.go +++ b/internal/execution/dispatcher.go @@ -3,9 +3,10 @@ package execution import ( "context" + "go.uber.org/zap" + "github.com/lambda-feedback/shimmy/internal/execution/dispatcher" "github.com/lambda-feedback/shimmy/internal/execution/supervisor" - "go.uber.org/zap" ) type Dispatcher dispatcher.Dispatcher diff --git a/internal/execution/dispatcher/dispatcher_dedicated.go b/internal/execution/dispatcher/dispatcher_dedicated.go index eb440a9..2cb5223 100644 --- a/internal/execution/dispatcher/dispatcher_dedicated.go +++ b/internal/execution/dispatcher/dispatcher_dedicated.go @@ -4,8 +4,9 @@ import ( "context" "fmt" - "github.com/lambda-feedback/shimmy/internal/execution/supervisor" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/execution/supervisor" ) type DedicatedDispatcher struct { diff --git a/internal/execution/dispatcher/dispatcher_pooled.go b/internal/execution/dispatcher/dispatcher_pooled.go index 93ce1e6..7a49429 100644 --- a/internal/execution/dispatcher/dispatcher_pooled.go +++ b/internal/execution/dispatcher/dispatcher_pooled.go @@ -6,8 +6,9 @@ import ( "runtime" "github.com/jackc/puddle/v2" - "github.com/lambda-feedback/shimmy/internal/execution/supervisor" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/execution/supervisor" ) type PooledDispatcher struct { diff --git a/internal/execution/dispatcher/dispatcher_pooled_test.go b/internal/execution/dispatcher/dispatcher_pooled_test.go index efae4eb..d8a90d4 100644 --- a/internal/execution/dispatcher/dispatcher_pooled_test.go +++ b/internal/execution/dispatcher/dispatcher_pooled_test.go @@ -5,11 +5,12 @@ import ( "testing" "time" - "github.com/lambda-feedback/shimmy/internal/execution/dispatcher" - "github.com/lambda-feedback/shimmy/internal/execution/supervisor" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/execution/dispatcher" + "github.com/lambda-feedback/shimmy/internal/execution/supervisor" ) func TestPooledDispatcher_New_UsesCPUCoreFallback(t *testing.T) { diff --git a/internal/execution/supervisor/adapter.go b/internal/execution/supervisor/adapter.go index 3a12a53..e31eb13 100644 --- a/internal/execution/supervisor/adapter.go +++ b/internal/execution/supervisor/adapter.go @@ -4,8 +4,9 @@ import ( "context" "time" - "github.com/lambda-feedback/shimmy/internal/execution/worker" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/execution/worker" ) // AdapterWorkerFactoryFn is a type alias for a function that creates a worker diff --git a/internal/execution/supervisor/adapter_file.go b/internal/execution/supervisor/adapter_file.go index b69ea9e..7810a4e 100644 --- a/internal/execution/supervisor/adapter_file.go +++ b/internal/execution/supervisor/adapter_file.go @@ -12,8 +12,9 @@ import ( "sync" "time" - "github.com/lambda-feedback/shimmy/internal/execution/worker" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/execution/worker" ) // fileAdapter is an adapter that allows supervisors to use files to diff --git a/internal/execution/supervisor/adapter_file_test.go b/internal/execution/supervisor/adapter_file_test.go index fb46f94..30f5cf8 100644 --- a/internal/execution/supervisor/adapter_file_test.go +++ b/internal/execution/supervisor/adapter_file_test.go @@ -7,10 +7,11 @@ import ( "strings" "testing" - "github.com/lambda-feedback/shimmy/internal/execution/worker" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/execution/worker" ) func TestFileAdapter_Start_DoesNotStartWorker(t *testing.T) { diff --git a/internal/execution/supervisor/adapter_rpc.go b/internal/execution/supervisor/adapter_rpc.go index 583f667..ec40d5b 100644 --- a/internal/execution/supervisor/adapter_rpc.go +++ b/internal/execution/supervisor/adapter_rpc.go @@ -11,8 +11,9 @@ import ( "time" "github.com/ethereum/go-ethereum/rpc" - "github.com/lambda-feedback/shimmy/internal/execution/worker" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/execution/worker" ) // RpcConfig describes the configuration for the rpc interface. diff --git a/internal/execution/supervisor/adapter_rpc_test.go b/internal/execution/supervisor/adapter_rpc_test.go index aee8f72..2ac8860 100644 --- a/internal/execution/supervisor/adapter_rpc_test.go +++ b/internal/execution/supervisor/adapter_rpc_test.go @@ -6,10 +6,11 @@ import ( "io" "testing" - "github.com/lambda-feedback/shimmy/internal/execution/worker" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/execution/worker" ) type rwc struct { diff --git a/internal/execution/supervisor/adapter_test.go b/internal/execution/supervisor/adapter_test.go index bc74e8a..c426f7a 100644 --- a/internal/execution/supervisor/adapter_test.go +++ b/internal/execution/supervisor/adapter_test.go @@ -3,9 +3,10 @@ package supervisor import ( "testing" - "github.com/lambda-feedback/shimmy/internal/execution/worker" "github.com/stretchr/testify/assert" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/execution/worker" ) func TestDefaultAdapterFactory(t *testing.T) { diff --git a/internal/execution/supervisor/supervisor.go b/internal/execution/supervisor/supervisor.go index 559b0a9..58b3248 100644 --- a/internal/execution/supervisor/supervisor.go +++ b/internal/execution/supervisor/supervisor.go @@ -6,8 +6,9 @@ import ( "sync" "time" - "github.com/lambda-feedback/shimmy/internal/execution/worker" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/execution/worker" ) type Supervisor interface { diff --git a/internal/execution/supervisor/supervisor_test.go b/internal/execution/supervisor/supervisor_test.go index 213272e..82bdb95 100644 --- a/internal/execution/supervisor/supervisor_test.go +++ b/internal/execution/supervisor/supervisor_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" - "github.com/lambda-feedback/shimmy/internal/execution/supervisor" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/execution/supervisor" ) func TestSupervisor_New_DefaultWorkerFactory(t *testing.T) { diff --git a/internal/execution/worker/worker_test.go b/internal/execution/worker/worker_test.go index e2707e8..f2edbc1 100644 --- a/internal/execution/worker/worker_test.go +++ b/internal/execution/worker/worker_test.go @@ -9,10 +9,11 @@ import ( "testing" "time" - "github.com/lambda-feedback/shimmy/internal/execution/worker" - "github.com/lambda-feedback/shimmy/util" "github.com/stretchr/testify/assert" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/execution/worker" + "github.com/lambda-feedback/shimmy/util" ) func TestWorker_Start_IsAlive(t *testing.T) { diff --git a/main.go b/main.go index 718d70f..cb0e18d 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "time" "github.com/getsentry/sentry-go" + "github.com/lambda-feedback/shimmy/cmd" "github.com/lambda-feedback/shimmy/util" ) diff --git a/runtime/handler.go b/runtime/handler.go index e94a483..f5d5a99 100644 --- a/runtime/handler.go +++ b/runtime/handler.go @@ -7,9 +7,10 @@ import ( "net/http" "strings" - "github.com/lambda-feedback/shimmy/runtime/schema" "go.uber.org/fx" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/runtime/schema" ) var ( diff --git a/runtime/handler_validate.go b/runtime/handler_validate.go index cfd6080..293a666 100644 --- a/runtime/handler_validate.go +++ b/runtime/handler_validate.go @@ -3,9 +3,10 @@ package runtime import ( "fmt" - "github.com/lambda-feedback/shimmy/runtime/schema" "github.com/xeipuuv/gojsonschema" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/runtime/schema" ) // validationType is the type of validation. diff --git a/runtime/runtime.go b/runtime/runtime.go index 1b3411c..a29ce92 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -3,9 +3,10 @@ package runtime import ( "context" - "github.com/lambda-feedback/shimmy/internal/execution" "go.uber.org/fx" "go.uber.org/zap" + + "github.com/lambda-feedback/shimmy/internal/execution" ) // Runtime is the interface for a runtime. diff --git a/util/conf/parse.go b/util/conf/parse.go index ec882b1..f14f937 100644 --- a/util/conf/parse.go +++ b/util/conf/parse.go @@ -9,8 +9,9 @@ import ( "github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/v2" - "github.com/lambda-feedback/shimmy/util/cliflags" "github.com/urfave/cli/v2" + + "github.com/lambda-feedback/shimmy/util/cliflags" ) type ParseOptions struct { diff --git a/util/truthy_test.go b/util/truthy_test.go index c65248b..089296d 100644 --- a/util/truthy_test.go +++ b/util/truthy_test.go @@ -3,8 +3,9 @@ package util_test import ( "testing" - "github.com/lambda-feedback/shimmy/util" "github.com/stretchr/testify/assert" + + "github.com/lambda-feedback/shimmy/util" ) func TestTruthy(t *testing.T) { From 0e0e973a03afa898a771485d9d51375c82b1e418 Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:46:26 +0100 Subject: [PATCH 2/4] Add auth-key flag --- README.md | 4 ++++ cmd/root.go | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/README.md b/README.md index 6ceb422..280b0a9 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ GLOBAL OPTIONS: --log-level value set the log level. Options: debug, info, warn, error, panic, fatal. [$LOG_LEVEL] --version print the version + auth + + --auth-key value, -k value the authentication key to use for incoming requests. [$AUTH_KEY] + function --arg value, -a value [ --arg value, -a value ] additional arguments for to the worker process. [$FUNCTION_ARGS] diff --git a/cmd/root.go b/cmd/root.go index 396a482..42fdb3a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -36,6 +36,13 @@ functions on arbitrary, serverless platforms.` Usage: "set the log format. Options: production, development.", EnvVars: []string{"LOG_FORMAT"}, }, + // auth flags + &cli.StringFlag{ + Name: "auth-key", + Usage: "the secret key for the application.", + Category: "auth", + EnvVars: []string{"AUTH_KEY"}, + }, // shim flags &cli.StringFlag{ Name: "interface", @@ -242,6 +249,7 @@ func parseRootConfig(ctx *cli.Context) (config.Config, error) { // map cli flags to config fields cliMap := map[string]string{ + "auth-key": "auth.key", "max-workers": "runtime.max_workers", "command": "runtime.cmd", "cwd": "runtime.cwd", From 48ddae0840c1817b48955f70edd2476319201257 Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:01:35 +0100 Subject: [PATCH 3/4] Fix Dockerfile casing --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f9e4ac7..bc161e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM golang:1.22 as builder +FROM --platform=$BUILDPLATFORM golang:1.22 AS builder WORKDIR /app From 8bafbcf6339324cee2318def0c42bb3a77e23f5e Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Mon, 4 Aug 2025 12:46:44 +0100 Subject: [PATCH 4/4] Added unit tests for checking if the provided API key in the HTTP request header is valid, and if the API key is invalid --- handler/handler_test.go | 102 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 handler/handler_test.go diff --git a/handler/handler_test.go b/handler/handler_test.go new file mode 100644 index 0000000..4765a5a --- /dev/null +++ b/handler/handler_test.go @@ -0,0 +1,102 @@ +package handler + +import ( + "bytes" + "context" + "github.com/lambda-feedback/shimmy/config" + "github.com/lambda-feedback/shimmy/runtime" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.uber.org/zap" + "io" + "net/http" + "net/http/httptest" + "testing" +) + +// --- Mock handler --- +type MockHandler struct { + mock.Mock +} + +func (m *MockHandler) Handle(ctx context.Context, req runtime.Request) runtime.Response { + args := m.Called(ctx, req) + return args.Get(0).(runtime.Response) +} + +// --- Test --- +func TestServeHTTP_Success(t *testing.T) { + mockHandler := new(MockHandler) + + reqBody := []byte(`{"example": "value"}`) + req := httptest.NewRequest(http.MethodPost, "/test", bytes.NewReader(reqBody)) + req.Header.Set("api-key", "secret") + + w := httptest.NewRecorder() + + expectedResponse := runtime.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: []byte(`{"ok":true}`), + } + + mockHandler.On("Handle", mock.Anything, mock.MatchedBy(func(r runtime.Request) bool { + return r.Path == "/test" && + r.Method == http.MethodPost && + bytes.Equal(r.Body, reqBody) + })).Return(expectedResponse) + + handler := &CommandHandler{ + handler: mockHandler, + log: zap.NewNop(), // or zaptest.NewLogger(t) + config: config.Config{ + LogLevel: "debug", + Runtime: runtime.Config{}, + Auth: config.AuthConfig{Key: "secret"}, + }, + } + + handler.ServeHTTP(w, req) + + res := w.Result() + defer res.Body.Close() + + body, _ := io.ReadAll(res.Body) + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "application/json", res.Header.Get("Content-Type")) + assert.Equal(t, `{"ok":true}`, string(body)) + mockHandler.AssertExpectations(t) +} + +func TestServeHTTP_Unauthorized(t *testing.T) { + mockHandler := new(MockHandler) + + req := httptest.NewRequest(http.MethodPost, "/test", bytes.NewReader([]byte(`{"example": "value"}`))) + req.Header.Set("api-key", "wrong-key") // wrong key + + w := httptest.NewRecorder() + + handler := &CommandHandler{ + handler: mockHandler, // won't be called + log: zap.NewNop(), + config: config.Config{ + LogLevel: "debug", + Runtime: runtime.Config{}, + Auth: config.AuthConfig{Key: "Secret"}, + }, + } + + handler.ServeHTTP(w, req) + + res := w.Result() + defer res.Body.Close() + + body, _ := io.ReadAll(res.Body) + + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + assert.Contains(t, string(body), "unauthorized") + + // Ensure handler was not called + mockHandler.AssertNotCalled(t, "Handle", mock.Anything, mock.Anything) +}