github.com/anthropics/anthropic-sdk-go@v1.70.1/aws/aws_test.go (about)

     1  package aws
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"io"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"reflect"
    11  	"strings"
    12  	"testing"
    13  
    14  	awssdk "github.com/aws/aws-sdk-go-v2/aws"
    15  	v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
    16  	"github.com/aws/aws-sdk-go-v2/credentials"
    17  
    18  	"github.com/anthropics/anthropic-sdk-go"
    19  	"github.com/anthropics/anthropic-sdk-go/internal/awsauth"
    20  )
    21  
    22  func makeStaticConfig(region string) awssdk.Config {
    23  	return awssdk.Config{
    24  		Region: region,
    25  		Credentials: credentials.StaticCredentialsProvider{
    26  			Value: awssdk.Credentials{
    27  				AccessKeyID:     "test-access-key",
    28  				SecretAccessKey: "test-secret-key",
    29  			},
    30  		},
    31  	}
    32  }
    33  
    34  type capturedRequest struct {
    35  	Headers http.Header
    36  	URL     string
    37  }
    38  
    39  func messagesHandler(captured *capturedRequest) http.HandlerFunc {
    40  	return func(w http.ResponseWriter, r *http.Request) {
    41  		captured.Headers = r.Header.Clone()
    42  		captured.URL = r.URL.String()
    43  		w.Header().Set("Content-Type", "application/json")
    44  		json.NewEncoder(w).Encode(map[string]any{
    45  			"id":            "msg_test",
    46  			"type":          "message",
    47  			"role":          "assistant",
    48  			"content":       []map[string]any{{"type": "text", "text": "hi"}},
    49  			"model":         "claude-sonnet-4-6-20250514",
    50  			"stop_reason":   "end_turn",
    51  			"stop_sequence": nil,
    52  			"usage":         map[string]any{"input_tokens": 1, "output_tokens": 1},
    53  		})
    54  	}
    55  }
    56  
    57  func newTestClient(t *testing.T, cfg ClientConfig) (*Client, *capturedRequest) {
    58  	t.Helper()
    59  	var captured capturedRequest
    60  	server := httptest.NewServer(messagesHandler(&captured))
    61  	t.Cleanup(server.Close)
    62  
    63  	cfg.BaseURL = server.URL
    64  	client, err := NewClient(context.Background(), cfg)
    65  	if err != nil {
    66  		t.Fatalf("NewClient failed: %v", err)
    67  	}
    68  	return client, &captured
    69  }
    70  
    71  func sendTestRequest(t *testing.T, client *Client) {
    72  	t.Helper()
    73  	_, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
    74  		Model:     "claude-sonnet-4-6-20250514",
    75  		MaxTokens: 1,
    76  		Messages: []anthropic.MessageParam{
    77  			anthropic.NewUserMessage(anthropic.NewTextBlock("hi")),
    78  		},
    79  	})
    80  	if err != nil {
    81  		t.Fatalf("request failed: %v", err)
    82  	}
    83  }
    84  
    85  // --- Service sync test ---
    86  
    87  func TestClientServicesMatchAnthropicClient(t *testing.T) {
    88  	awsType := reflect.TypeOf(Client{})
    89  	anthropicType := reflect.TypeOf(anthropic.Client{})
    90  
    91  	for i := 0; i < anthropicType.NumField(); i++ {
    92  		field := anthropicType.Field(i)
    93  		awsField, ok := awsType.FieldByName(field.Name)
    94  		if !ok {
    95  			t.Errorf("aws.Client is missing field %q (type %s) from anthropic.Client", field.Name, field.Type)
    96  			continue
    97  		}
    98  		if awsField.Type != field.Type {
    99  			t.Errorf("aws.Client.%s has type %s, expected %s", field.Name, awsField.Type, field.Type)
   100  		}
   101  	}
   102  }
   103  
   104  // --- Validation tests ---
   105  
   106  func TestNewClientRequiresWorkspaceID(t *testing.T) {
   107  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "")
   108  	t.Setenv("ANTHROPIC_AWS_API_KEY", "")
   109  
   110  	_, err := NewClient(context.Background(), ClientConfig{
   111  		AWSRegion:          "us-east-1",
   112  		AWSAccessKey:       "key",
   113  		AWSSecretAccessKey: "secret",
   114  	})
   115  	if err == nil {
   116  		t.Fatal("expected error when WorkspaceID is missing")
   117  	}
   118  }
   119  
   120  func TestNewClientRequiresBaseURLOrRegion(t *testing.T) {
   121  	t.Setenv("AWS_REGION", "")
   122  	t.Setenv("ANTHROPIC_AWS_BASE_URL", "")
   123  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "default")
   124  
   125  	_, err := NewClient(context.Background(), ClientConfig{
   126  		APIKey: "my-key",
   127  	})
   128  	if err == nil {
   129  		t.Fatal("expected error when neither base URL nor region is available")
   130  	}
   131  }
   132  
   133  func TestNewClientRegionRequiredForSigV4(t *testing.T) {
   134  	t.Setenv("AWS_REGION", "")
   135  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "default")
   136  	t.Setenv("ANTHROPIC_AWS_API_KEY", "")
   137  
   138  	_, err := NewClient(context.Background(), ClientConfig{
   139  		AWSAccessKey:       "key",
   140  		AWSSecretAccessKey: "secret",
   141  	})
   142  	if err == nil {
   143  		t.Fatal("expected error when region is missing and SigV4 is used")
   144  	}
   145  }
   146  
   147  // --- API key mode tests ---
   148  
   149  func TestAPIKeyModeHeaders(t *testing.T) {
   150  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "")
   151  	t.Setenv("ANTHROPIC_AWS_API_KEY", "")
   152  
   153  	client, captured := newTestClient(t, ClientConfig{
   154  		APIKey:      "my-api-key",
   155  		AWSRegion:   "us-east-1",
   156  		WorkspaceID: "ws-123",
   157  	})
   158  	sendTestRequest(t, client)
   159  
   160  	if got := captured.Headers.Get("X-Api-Key"); got != "my-api-key" {
   161  		t.Errorf("expected x-api-key %q, got %q", "my-api-key", got)
   162  	}
   163  	if got := captured.Headers.Get("Anthropic-Workspace-Id"); got != "ws-123" {
   164  		t.Errorf("expected anthropic-workspace-id %q, got %q", "ws-123", got)
   165  	}
   166  	if captured.Headers.Get("Authorization") != "" {
   167  		t.Error("expected no Authorization header in API key mode")
   168  	}
   169  }
   170  
   171  func TestAPIKeyFromEnvHeaders(t *testing.T) {
   172  	t.Setenv("ANTHROPIC_AWS_API_KEY", "env-api-key")
   173  	t.Setenv("AWS_REGION", "us-east-1")
   174  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "")
   175  
   176  	client, captured := newTestClient(t, ClientConfig{
   177  		WorkspaceID: "ws-456",
   178  	})
   179  	sendTestRequest(t, client)
   180  
   181  	if got := captured.Headers.Get("X-Api-Key"); got != "env-api-key" {
   182  		t.Errorf("expected x-api-key %q, got %q", "env-api-key", got)
   183  	}
   184  }
   185  
   186  func TestExplicitAPIKeyOverridesEnv(t *testing.T) {
   187  	t.Setenv("ANTHROPIC_AWS_API_KEY", "env-api-key")
   188  	t.Setenv("AWS_REGION", "us-east-1")
   189  
   190  	client, captured := newTestClient(t, ClientConfig{
   191  		APIKey:      "explicit-key",
   192  		WorkspaceID: "ws-789",
   193  	})
   194  	sendTestRequest(t, client)
   195  
   196  	if got := captured.Headers.Get("X-Api-Key"); got != "explicit-key" {
   197  		t.Errorf("expected x-api-key %q, got %q", "explicit-key", got)
   198  	}
   199  }
   200  
   201  // --- SigV4 mode tests ---
   202  
   203  func TestSigV4ModeHeaders(t *testing.T) {
   204  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "")
   205  	t.Setenv("ANTHROPIC_AWS_API_KEY", "")
   206  	t.Setenv("ANTHROPIC_API_KEY", "")
   207  
   208  	client, captured := newTestClient(t, ClientConfig{
   209  		AWSRegion:          "us-east-1",
   210  		AWSAccessKey:       "test-access-key",
   211  		AWSSecretAccessKey: "test-secret-key",
   212  		WorkspaceID:        "ws-sigv4",
   213  	})
   214  	sendTestRequest(t, client)
   215  
   216  	auth := captured.Headers.Get("Authorization")
   217  	if auth == "" {
   218  		t.Fatal("expected Authorization header in SigV4 mode")
   219  	}
   220  	if !strings.HasPrefix(auth, "AWS4-HMAC-SHA256") {
   221  		t.Errorf("expected AWS4 signature, got: %s", auth)
   222  	}
   223  	if !strings.Contains(auth, defaultServiceName) {
   224  		t.Errorf("expected service name %q in Authorization, got: %s", defaultServiceName, auth)
   225  	}
   226  	if captured.Headers.Get("X-Amz-Date") == "" {
   227  		t.Error("expected X-Amz-Date header in SigV4 mode")
   228  	}
   229  	if got := captured.Headers.Get("Anthropic-Workspace-Id"); got != "ws-sigv4" {
   230  		t.Errorf("expected anthropic-workspace-id %q, got %q", "ws-sigv4", got)
   231  	}
   232  }
   233  
   234  func TestSigV4WithSessionToken(t *testing.T) {
   235  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "")
   236  	t.Setenv("ANTHROPIC_AWS_API_KEY", "")
   237  	t.Setenv("ANTHROPIC_API_KEY", "")
   238  
   239  	client, captured := newTestClient(t, ClientConfig{
   240  		AWSRegion:          "us-east-1",
   241  		AWSAccessKey:       "test-access-key",
   242  		AWSSecretAccessKey: "test-secret-key",
   243  		AWSSessionToken:    "test-session-token",
   244  		WorkspaceID:        "ws-session",
   245  	})
   246  	sendTestRequest(t, client)
   247  
   248  	if captured.Headers.Get("X-Amz-Security-Token") != "test-session-token" {
   249  		t.Errorf("expected X-Amz-Security-Token %q, got %q", "test-session-token", captured.Headers.Get("X-Amz-Security-Token"))
   250  	}
   251  }
   252  
   253  // --- Workspace ID tests ---
   254  
   255  func TestWorkspaceIDFromEnv(t *testing.T) {
   256  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "env-workspace")
   257  	t.Setenv("ANTHROPIC_AWS_API_KEY", "test-key")
   258  	t.Setenv("AWS_REGION", "us-east-1")
   259  
   260  	client, captured := newTestClient(t, ClientConfig{})
   261  	sendTestRequest(t, client)
   262  
   263  	if got := captured.Headers.Get("Anthropic-Workspace-Id"); got != "env-workspace" {
   264  		t.Errorf("expected anthropic-workspace-id %q, got %q", "env-workspace", got)
   265  	}
   266  }
   267  
   268  func TestPerRequestWorkspaceIDOverridesClientConfig(t *testing.T) {
   269  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "")
   270  	t.Setenv("ANTHROPIC_AWS_API_KEY", "")
   271  
   272  	client, captured := newTestClient(t, ClientConfig{
   273  		APIKey:      "my-api-key",
   274  		AWSRegion:   "us-east-1",
   275  		WorkspaceID: "ws-client",
   276  	})
   277  	_, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
   278  		Model:       "claude-sonnet-4-6-20250514",
   279  		MaxTokens:   1,
   280  		Messages:    []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock("hi"))},
   281  		WorkspaceID: anthropic.String("ws-request"),
   282  	})
   283  	if err != nil {
   284  		t.Fatalf("request failed: %v", err)
   285  	}
   286  
   287  	if got := captured.Headers.Values("Anthropic-Workspace-Id"); len(got) != 1 || got[0] != "ws-request" {
   288  		t.Errorf("expected single anthropic-workspace-id %q, got %q", "ws-request", got)
   289  	}
   290  }
   291  
   292  // --- Base URL tests ---
   293  
   294  func TestBaseURLDerivedFromRegion(t *testing.T) {
   295  	t.Setenv("ANTHROPIC_AWS_BASE_URL", "")
   296  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "")
   297  	t.Setenv("ANTHROPIC_AWS_API_KEY", "")
   298  
   299  	resolved, err := awsauth.ResolveConfig(toInternalConfig(ClientConfig{
   300  		AWSRegion:   "us-west-2",
   301  		WorkspaceID: "default",
   302  		APIKey:      "my-key",
   303  	}), awsResolveParams())
   304  	if err != nil {
   305  		t.Fatalf("unexpected error: %v", err)
   306  	}
   307  	expected := "https://aws-external-anthropic.us-west-2.api.aws"
   308  	if resolved.BaseURL != expected {
   309  		t.Errorf("expected base URL %q, got %q", expected, resolved.BaseURL)
   310  	}
   311  }
   312  
   313  func TestBaseURLFromEnv(t *testing.T) {
   314  	t.Setenv("ANTHROPIC_AWS_BASE_URL", "https://custom.gateway.example.com")
   315  	t.Setenv("ANTHROPIC_AWS_API_KEY", "my-key")
   316  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "")
   317  
   318  	resolved, err := awsauth.ResolveConfig(toInternalConfig(ClientConfig{
   319  		WorkspaceID: "default",
   320  	}), awsResolveParams())
   321  	if err != nil {
   322  		t.Fatalf("unexpected error: %v", err)
   323  	}
   324  	if resolved.BaseURL != "https://custom.gateway.example.com" {
   325  		t.Errorf("expected base URL from env, got %q", resolved.BaseURL)
   326  	}
   327  }
   328  
   329  func TestBaseURLExplicitOverridesRegion(t *testing.T) {
   330  	t.Setenv("ANTHROPIC_AWS_BASE_URL", "")
   331  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "")
   332  	t.Setenv("ANTHROPIC_AWS_API_KEY", "")
   333  
   334  	resolved, err := awsauth.ResolveConfig(toInternalConfig(ClientConfig{
   335  		BaseURL:     "https://explicit.example.com",
   336  		AWSRegion:   "us-east-1",
   337  		WorkspaceID: "default",
   338  		APIKey:      "my-key",
   339  	}), awsResolveParams())
   340  	if err != nil {
   341  		t.Fatalf("unexpected error: %v", err)
   342  	}
   343  	if resolved.BaseURL != "https://explicit.example.com" {
   344  		t.Errorf("expected explicit base URL, got %q", resolved.BaseURL)
   345  	}
   346  }
   347  
   348  // --- skipAuth tests ---
   349  
   350  func TestSkipAuthNoWorkspaceRequired(t *testing.T) {
   351  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "")
   352  	t.Setenv("ANTHROPIC_AWS_API_KEY", "")
   353  
   354  	_, err := NewClient(context.Background(), ClientConfig{
   355  		BaseURL:  "https://proxy.example.com",
   356  		SkipAuth: true,
   357  	})
   358  	if err != nil {
   359  		t.Fatalf("expected no error with skipAuth, got: %v", err)
   360  	}
   361  }
   362  
   363  func TestSkipAuthNoAuthHeaders(t *testing.T) {
   364  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "")
   365  	t.Setenv("ANTHROPIC_AWS_API_KEY", "")
   366  	t.Setenv("ANTHROPIC_API_KEY", "")
   367  
   368  	client, captured := newTestClient(t, ClientConfig{
   369  		SkipAuth: true,
   370  	})
   371  	sendTestRequest(t, client)
   372  
   373  	if captured.Headers.Get("Authorization") != "" {
   374  		t.Error("expected no Authorization header with skipAuth")
   375  	}
   376  	if captured.Headers.Get("X-Amz-Date") != "" {
   377  		t.Error("expected no X-Amz-Date header with skipAuth")
   378  	}
   379  	if captured.Headers.Get("Anthropic-Workspace-Id") != "" {
   380  		t.Error("expected no anthropic-workspace-id header with skipAuth")
   381  	}
   382  }
   383  
   384  func TestSkipAuthIgnoresProvidedCreds(t *testing.T) {
   385  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "")
   386  	t.Setenv("ANTHROPIC_AWS_API_KEY", "")
   387  	t.Setenv("ANTHROPIC_API_KEY", "")
   388  
   389  	client, captured := newTestClient(t, ClientConfig{
   390  		AWSAccessKey:       "key",
   391  		AWSSecretAccessKey: "secret",
   392  		AWSRegion:          "us-east-1",
   393  		WorkspaceID:        "ws-123",
   394  		SkipAuth:           true,
   395  	})
   396  	sendTestRequest(t, client)
   397  
   398  	if captured.Headers.Get("Authorization") != "" {
   399  		t.Error("expected no Authorization header with skipAuth even when credentials are provided")
   400  	}
   401  	if captured.Headers.Get("Anthropic-Workspace-Id") != "" {
   402  		t.Error("expected no anthropic-workspace-id header with skipAuth even when workspace ID is provided")
   403  	}
   404  }
   405  
   406  // --- ANTHROPIC_API_KEY isolation test ---
   407  
   408  func TestDoesNotLeakAnthropicAPIKey(t *testing.T) {
   409  	t.Setenv("ANTHROPIC_API_KEY", "should-not-appear")
   410  	t.Setenv("ANTHROPIC_AWS_WORKSPACE_ID", "")
   411  	t.Setenv("ANTHROPIC_AWS_API_KEY", "")
   412  
   413  	client, captured := newTestClient(t, ClientConfig{
   414  		AWSRegion:          "us-east-1",
   415  		AWSAccessKey:       "test-access-key",
   416  		AWSSecretAccessKey: "test-secret-key",
   417  		WorkspaceID:        "ws-test",
   418  	})
   419  	sendTestRequest(t, client)
   420  
   421  	if got := captured.Headers.Get("X-Api-Key"); got != "" {
   422  		t.Errorf("expected no x-api-key header when using SigV4, got %q", got)
   423  	}
   424  }
   425  
   426  // --- Middleware unit tests ---
   427  
   428  func TestAWSMiddlewareSigV4Signing(t *testing.T) {
   429  	cfg := makeStaticConfig("us-east-1")
   430  	signer := v4.NewSigner()
   431  	middleware := awsauth.SigV4Middleware(signer, cfg, defaultServiceName)
   432  
   433  	body := []byte(`{"messages":[{"role":"user","content":"hello"}]}`)
   434  	req, err := http.NewRequest("POST", "https://aws-external-anthropic.us-east-1.api.aws/v1/messages", bytes.NewReader(body))
   435  	if err != nil {
   436  		t.Fatalf("failed to create request: %v", err)
   437  	}
   438  	req.Header.Set("Content-Type", "application/json")
   439  
   440  	_, err = middleware(req, func(r *http.Request) (*http.Response, error) {
   441  		if r.Header.Get("Authorization") == "" {
   442  			t.Error("expected Authorization header to be set by SigV4 signing")
   443  		}
   444  		if r.Header.Get("X-Amz-Date") == "" {
   445  			t.Error("expected X-Amz-Date header to be set by SigV4 signing")
   446  		}
   447  		bodyBytes, err := io.ReadAll(r.Body)
   448  		if err != nil {
   449  			t.Fatalf("failed to read request body: %v", err)
   450  		}
   451  		if !bytes.Equal(bodyBytes, body) {
   452  			t.Errorf("expected body %q, got %q", body, bodyBytes)
   453  		}
   454  		return &http.Response{StatusCode: 200, Body: http.NoBody}, nil
   455  	})
   456  	if err != nil {
   457  		t.Fatalf("middleware failed: %v", err)
   458  	}
   459  }
   460  
   461  func TestAWSMiddlewareEmptyBody(t *testing.T) {
   462  	cfg := makeStaticConfig("us-east-1")
   463  	signer := v4.NewSigner()
   464  	middleware := awsauth.SigV4Middleware(signer, cfg, defaultServiceName)
   465  
   466  	req, err := http.NewRequest("GET", "https://aws-external-anthropic.us-east-1.api.aws/v1/models", nil)
   467  	if err != nil {
   468  		t.Fatalf("failed to create request: %v", err)
   469  	}
   470  
   471  	_, err = middleware(req, func(r *http.Request) (*http.Response, error) {
   472  		if r.Header.Get("Authorization") == "" {
   473  			t.Error("expected Authorization header from SigV4 signing even with empty body")
   474  		}
   475  		return &http.Response{StatusCode: 200, Body: http.NoBody}, nil
   476  	})
   477  	if err != nil {
   478  		t.Fatalf("middleware failed: %v", err)
   479  	}
   480  }
   481  
   482  func TestAWSMiddlewareServiceName(t *testing.T) {
   483  	cfg := makeStaticConfig("eu-west-1")
   484  	signer := v4.NewSigner()
   485  	middleware := awsauth.SigV4Middleware(signer, cfg, defaultServiceName)
   486  
   487  	req, err := http.NewRequest("POST", "https://aws-external-anthropic.eu-west-1.api.aws/v1/messages", bytes.NewReader([]byte(`{}`)))
   488  	if err != nil {
   489  		t.Fatalf("failed to create request: %v", err)
   490  	}
   491  
   492  	var gotAuth string
   493  	_, err = middleware(req, func(r *http.Request) (*http.Response, error) {
   494  		gotAuth = r.Header.Get("Authorization")
   495  		return &http.Response{StatusCode: 200, Body: http.NoBody}, nil
   496  	})
   497  	if err != nil {
   498  		t.Fatalf("middleware failed: %v", err)
   499  	}
   500  
   501  	if gotAuth == "" {
   502  		t.Fatal("expected non-empty Authorization header")
   503  	}
   504  	expectedServiceFragment := "/" + defaultServiceName + "/"
   505  	if !bytes.Contains([]byte(gotAuth), []byte(expectedServiceFragment)) {
   506  		t.Errorf("expected Authorization to contain service %q, got: %s", defaultServiceName, gotAuth)
   507  	}
   508  }
   509  
   510  func TestAWSMiddlewareQueryParamsIncludedInSignature(t *testing.T) {
   511  	cfg := makeStaticConfig("us-east-1")
   512  	signer := v4.NewSigner()
   513  	middleware := awsauth.SigV4Middleware(signer, cfg, defaultServiceName)
   514  
   515  	body := []byte(`{}`)
   516  	urlWithQuery := "https://aws-external-anthropic.us-east-1.api.aws/v1/messages?beta=true&stream=false"
   517  	req, err := http.NewRequest("POST", urlWithQuery, bytes.NewReader(body))
   518  	if err != nil {
   519  		t.Fatalf("failed to create request: %v", err)
   520  	}
   521  
   522  	var signedAuth string
   523  	_, err = middleware(req, func(r *http.Request) (*http.Response, error) {
   524  		signedAuth = r.Header.Get("Authorization")
   525  		if r.URL.RawQuery == "" {
   526  			t.Error("expected query params to be preserved after signing")
   527  		}
   528  		if r.URL.Query().Get("beta") != "true" || r.URL.Query().Get("stream") != "false" {
   529  			t.Errorf("query params were modified: got %s", r.URL.RawQuery)
   530  		}
   531  		return &http.Response{StatusCode: 200, Body: http.NoBody}, nil
   532  	})
   533  	if err != nil {
   534  		t.Fatalf("middleware failed: %v", err)
   535  	}
   536  
   537  	middlewareNoQuery := awsauth.SigV4Middleware(signer, cfg, defaultServiceName)
   538  	reqNoQuery, err := http.NewRequest("POST", "https://aws-external-anthropic.us-east-1.api.aws/v1/messages", bytes.NewReader(body))
   539  	if err != nil {
   540  		t.Fatalf("failed to create request: %v", err)
   541  	}
   542  
   543  	var signedAuthNoQuery string
   544  	_, err = middlewareNoQuery(reqNoQuery, func(r *http.Request) (*http.Response, error) {
   545  		signedAuthNoQuery = r.Header.Get("Authorization")
   546  		return &http.Response{StatusCode: 200, Body: http.NoBody}, nil
   547  	})
   548  	if err != nil {
   549  		t.Fatalf("middleware failed: %v", err)
   550  	}
   551  
   552  	if signedAuth == signedAuthNoQuery {
   553  		t.Error("expected different signatures for requests with and without query params")
   554  	}
   555  }
   556  
   557  func TestAWSMiddlewareBodyReplayed(t *testing.T) {
   558  	cfg := makeStaticConfig("us-east-1")
   559  	signer := v4.NewSigner()
   560  	middleware := awsauth.SigV4Middleware(signer, cfg, defaultServiceName)
   561  
   562  	originalBody := []byte(`{"test":"body content"}`)
   563  	req, err := http.NewRequest("POST", "https://aws-external-anthropic.us-east-1.api.aws/v1/messages", bytes.NewReader(originalBody))
   564  	if err != nil {
   565  		t.Fatalf("failed to create request: %v", err)
   566  	}
   567  
   568  	_, err = middleware(req, func(r *http.Request) (*http.Response, error) {
   569  		if r.GetBody == nil {
   570  			t.Error("expected GetBody to be set for retry support")
   571  		}
   572  		rc, err := r.GetBody()
   573  		if err != nil {
   574  			t.Fatalf("GetBody failed: %v", err)
   575  		}
   576  		replayed, err := io.ReadAll(rc)
   577  		if err != nil {
   578  			t.Fatalf("failed to read replayed body: %v", err)
   579  		}
   580  		if !bytes.Equal(replayed, originalBody) {
   581  			t.Errorf("replayed body %q does not match original %q", replayed, originalBody)
   582  		}
   583  		return &http.Response{StatusCode: 200, Body: http.NoBody}, nil
   584  	})
   585  	if err != nil {
   586  		t.Fatalf("middleware failed: %v", err)
   587  	}
   588  }