http_test.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: LicenseRef-MutuaL-1.2
  4
  5package mcp
  6
  7import (
  8	"context"
  9	"net/http"
 10	"net/http/httptest"
 11	"strings"
 12	"testing"
 13)
 14
 15// server.SECURITY.3 server.SECURITY.6
 16func TestHTTPBearerAuthACIDServerSecurity3And6AllowsMatchingBearerToken(t *testing.T) {
 17	calls := 0
 18	handler := requireBearerToken("secret-token", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
 19		calls++
 20		w.WriteHeader(http.StatusNoContent)
 21	}))
 22
 23	recorder := httptest.NewRecorder()
 24	request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/mcp", nil)
 25	request.Header.Set("Authorization", "Bearer secret-token")
 26	handler.ServeHTTP(recorder, request)
 27
 28	if recorder.Code != http.StatusNoContent {
 29		t.Fatalf("status = %d, want %d", recorder.Code, http.StatusNoContent)
 30	}
 31	if calls != 1 {
 32		t.Fatalf("next calls = %d, want 1", calls)
 33	}
 34}
 35
 36// server.SECURITY.7
 37func TestHTTPBearerAuthACIDServerSecurity7RejectsInvalidTokens(t *testing.T) {
 38	tests := []struct {
 39		name   string
 40		header string
 41	}{
 42		{name: "missing", header: ""},
 43		{name: "wrong", header: "Bearer wrong-token"},
 44		{name: "basic", header: "Basic secret-token"},
 45		{name: "malformed", header: "Bearer"},
 46	}
 47
 48	for _, tt := range tests {
 49		t.Run(tt.name, func(t *testing.T) {
 50			calls := 0
 51			handler := requireBearerToken("secret-token", http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
 52				calls++
 53			}))
 54
 55			recorder := httptest.NewRecorder()
 56			request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/mcp", nil)
 57			if tt.header != "" {
 58				request.Header.Set("Authorization", tt.header)
 59			}
 60			handler.ServeHTTP(recorder, request)
 61
 62			if recorder.Code != http.StatusUnauthorized {
 63				t.Fatalf("status = %d, want %d", recorder.Code, http.StatusUnauthorized)
 64			}
 65			if got := recorder.Header().Get("WWW-Authenticate"); got != "Bearer" {
 66				t.Fatalf("WWW-Authenticate = %q, want Bearer", got)
 67			}
 68			if calls != 0 {
 69				t.Fatalf("next calls = %d, want 0", calls)
 70			}
 71		})
 72	}
 73}
 74
 75func TestHTTPBearerAuthAllowsRequestsWhenNoTokenConfigured(t *testing.T) {
 76	calls := 0
 77	handler := requireBearerToken("", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
 78		calls++
 79		w.WriteHeader(http.StatusNoContent)
 80	}))
 81
 82	recorder := httptest.NewRecorder()
 83	request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/mcp", nil)
 84	handler.ServeHTTP(recorder, request)
 85
 86	if recorder.Code != http.StatusNoContent {
 87		t.Fatalf("status = %d, want %d", recorder.Code, http.StatusNoContent)
 88	}
 89	if calls != 1 {
 90		t.Fatalf("next calls = %d, want 1", calls)
 91	}
 92}
 93
 94// server.SECURITY.2
 95func TestHTTPBearerAuthACIDServerSecurity2DoesNotExposeTokensInUnauthorizedResponse(t *testing.T) {
 96	handler := requireBearerToken("secret-token", http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
 97		t.Fatal("next handler was called")
 98	}))
 99
100	recorder := httptest.NewRecorder()
101	request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/mcp", nil)
102	request.Header.Set("Authorization", "Bearer submitted-token")
103	handler.ServeHTTP(recorder, request)
104
105	body := recorder.Body.String()
106	if strings.Contains(body, "secret-token") {
107		t.Fatalf("response body exposed configured token: %q", body)
108	}
109	if strings.Contains(body, "submitted-token") {
110		t.Fatalf("response body exposed submitted token: %q", body)
111	}
112}