auth

Paddy 2015-01-10 Parent:da2a0954e8d3 Child:385ac6294cdc

113:5bd46746b809 Browse Files

Let's test our verifyClient function. C'mon, it'll be fun! Add a function that tests the verifyClient function to our unit test suite. Basically, make sure that all the conceivable types of input have the right logic flow for what a "valid client" is. Also leave a note in client.go that makes it clear that public clients _should not be issued secrets in the first place_, because a public client that is issued a secret and specifies its client ID using the `client_id` POST body format will be told that it is not a valid client. While there are ways around this, the spec clearly states that non-confidential clients are not supposed to be issued secrets, so this seems like a nice way to conform to the spec or break trying.

client.go client_test.go

     1.1 --- a/client.go	Sat Jan 10 01:52:01 2015 -0500
     1.2 +++ b/client.go	Sat Jan 10 04:09:46 2015 -0500
     1.3 @@ -147,7 +147,7 @@
     1.4  		renderJSONError(enc, "server_error")
     1.5  		return nil, false
     1.6  	}
     1.7 -	if client.Secret != clientSecret {
     1.8 +	if client.Secret != clientSecret { // it's important that any client deemed "public" is not issued a client secret.
     1.9  		w.WriteHeader(http.StatusUnauthorized)
    1.10  		if fromAuthHeader {
    1.11  			w.Header().Set("WWW-Authenticate", "Basic")
     2.1 --- a/client_test.go	Sat Jan 10 01:52:01 2015 -0500
     2.2 +++ b/client_test.go	Sat Jan 10 04:09:46 2015 -0500
     2.3 @@ -1,9 +1,14 @@
     2.4  package auth
     2.5  
     2.6  import (
     2.7 +	"bytes"
     2.8  	"fmt"
     2.9 +	"io/ioutil"
    2.10 +	"net/http"
    2.11 +	"net/http/httptest"
    2.12  	"net/url"
    2.13  	"sort"
    2.14 +	"strings"
    2.15  	"testing"
    2.16  	"time"
    2.17  
    2.18 @@ -463,3 +468,398 @@
    2.19  		}
    2.20  	}
    2.21  }
    2.22 +
    2.23 +func TestVerifyClient(t *testing.T) {
    2.24 +	t.Parallel()
    2.25 +	memstore := NewMemstore()
    2.26 +	context := Context{
    2.27 +		clients: memstore,
    2.28 +	}
    2.29 +	client := Client{
    2.30 +		ID:      uuid.NewID(),
    2.31 +		Secret:  "super secret!",
    2.32 +		OwnerID: uuid.NewID(),
    2.33 +		Name:    "My test client",
    2.34 +		Logo:    "https://secondbit.org/logo.png",
    2.35 +		Website: "https://secondbit.org/",
    2.36 +		Type:    "confidential",
    2.37 +	}
    2.38 +	err := context.SaveClient(client)
    2.39 +	if err != nil {
    2.40 +		t.Fatal("Could not save client:", err)
    2.41 +	}
    2.42 +	publicClient := Client{
    2.43 +		ID:      uuid.NewID(),
    2.44 +		Secret:  "",
    2.45 +		OwnerID: uuid.NewID(),
    2.46 +		Name:    "A public client",
    2.47 +		Logo:    "https://secondbit.org/logo.png",
    2.48 +		Website: "https://secondbit.org/",
    2.49 +		Type:    "public",
    2.50 +	}
    2.51 +	err = context.SaveClient(publicClient)
    2.52 +	if err != nil {
    2.53 +		t.Fatal("Could not save client:", err)
    2.54 +	}
    2.55 +
    2.56 +	// verifyClient with no auth header, no public clients
    2.57 +	w := httptest.NewRecorder()
    2.58 +	r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
    2.59 +	if err != nil {
    2.60 +		t.Fatal("Can't build request:", err)
    2.61 +	}
    2.62 +	resp, success := verifyClient(w, r, false, context)
    2.63 +	if success {
    2.64 +		t.Error("Expected verification to fail, but succeeded with client ID:", resp)
    2.65 +	}
    2.66 +	if resp != nil {
    2.67 +		t.Error("Expected nil client ID, got", resp)
    2.68 +	}
    2.69 +	if w.Code != http.StatusBadRequest {
    2.70 +		t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
    2.71 +	}
    2.72 +	expectedBody := `{"error":"unauthorized_client"}`
    2.73 +	if expectedBody != strings.TrimSpace(w.Body.String()) {
    2.74 +		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
    2.75 +	}
    2.76 +
    2.77 +	// verifyClient with no auth header, public clients, empty client_id
    2.78 +	w = httptest.NewRecorder()
    2.79 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
    2.80 +	if err != nil {
    2.81 +		t.Fatal("Can't build request:", err)
    2.82 +	}
    2.83 +	resp, success = verifyClient(w, r, true, context)
    2.84 +	if success {
    2.85 +		t.Error("Expected verification to fail, but succeeded with client ID:", resp)
    2.86 +	}
    2.87 +	if resp != nil {
    2.88 +		t.Error("Expected nil client ID, got", resp)
    2.89 +	}
    2.90 +	if w.Code != http.StatusUnauthorized {
    2.91 +		t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
    2.92 +	}
    2.93 +	expectedBody = `{"error":"invalid_client"}`
    2.94 +	if expectedBody != strings.TrimSpace(w.Body.String()) {
    2.95 +		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
    2.96 +	}
    2.97 +
    2.98 +	// verifyClient with auth header, no public clients, empty client id
    2.99 +	w = httptest.NewRecorder()
   2.100 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.101 +	if err != nil {
   2.102 +		t.Fatal("Can't build request:", err)
   2.103 +	}
   2.104 +	r.SetBasicAuth("", "no client ID set")
   2.105 +	resp, success = verifyClient(w, r, false, context)
   2.106 +	if success {
   2.107 +		t.Error("Expected verification to fail, but succeeded with client ID:", resp)
   2.108 +	}
   2.109 +	if resp != nil {
   2.110 +		t.Error("Expected nil client ID, got", resp)
   2.111 +	}
   2.112 +	if w.Code != http.StatusUnauthorized {
   2.113 +		t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
   2.114 +	}
   2.115 +	expectedBody = `{"error":"invalid_client"}`
   2.116 +	if expectedBody != strings.TrimSpace(w.Body.String()) {
   2.117 +		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
   2.118 +	}
   2.119 +	if w.Header().Get("WWW-Authenticate") != "Basic" {
   2.120 +		t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
   2.121 +	}
   2.122 +
   2.123 +	// verifyClient with auth header, public clients, empty client id
   2.124 +	w = httptest.NewRecorder()
   2.125 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.126 +	if err != nil {
   2.127 +		t.Fatal("Can't build request:", err)
   2.128 +	}
   2.129 +	r.SetBasicAuth("", "no client ID set")
   2.130 +	resp, success = verifyClient(w, r, true, context)
   2.131 +	if success {
   2.132 +		t.Error("Expected verification to fail, but succeeded with client ID:", resp)
   2.133 +	}
   2.134 +	if resp != nil {
   2.135 +		t.Error("Expected nil client ID, got", resp)
   2.136 +	}
   2.137 +	if w.Code != http.StatusUnauthorized {
   2.138 +		t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
   2.139 +	}
   2.140 +	expectedBody = `{"error":"invalid_client"}`
   2.141 +	if expectedBody != strings.TrimSpace(w.Body.String()) {
   2.142 +		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
   2.143 +	}
   2.144 +	if w.Header().Get("WWW-Authenticate") != "Basic" {
   2.145 +		t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
   2.146 +	}
   2.147 +
   2.148 +	// verifyClient with auth header, no public clients, invalid client ID
   2.149 +	w = httptest.NewRecorder()
   2.150 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.151 +	if err != nil {
   2.152 +		t.Fatal("Can't build request:", err)
   2.153 +	}
   2.154 +	r.SetBasicAuth("not an actual id", "invalid client ID set")
   2.155 +	resp, success = verifyClient(w, r, false, context)
   2.156 +	if success {
   2.157 +		t.Error("Expected verification to fail, but succeeded with client ID:", resp)
   2.158 +	}
   2.159 +	if resp != nil {
   2.160 +		t.Error("Expected nil client ID, got", resp)
   2.161 +	}
   2.162 +	if w.Code != http.StatusUnauthorized {
   2.163 +		t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
   2.164 +	}
   2.165 +	expectedBody = `{"error":"invalid_client"}`
   2.166 +	if expectedBody != strings.TrimSpace(w.Body.String()) {
   2.167 +		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
   2.168 +	}
   2.169 +	if w.Header().Get("WWW-Authenticate") != "Basic" {
   2.170 +		t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
   2.171 +	}
   2.172 +
   2.173 +	// verifyClient with auth header, public clients, invalid client ID
   2.174 +	w = httptest.NewRecorder()
   2.175 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.176 +	if err != nil {
   2.177 +		t.Fatal("Can't build request:", err)
   2.178 +	}
   2.179 +	r.SetBasicAuth("not an actual id", "invalid client ID set")
   2.180 +	resp, success = verifyClient(w, r, true, context)
   2.181 +	if success {
   2.182 +		t.Error("Expected verification to fail, but succeeded with client ID:", resp)
   2.183 +	}
   2.184 +	if resp != nil {
   2.185 +		t.Error("Expected nil client ID, got", resp)
   2.186 +	}
   2.187 +	if w.Code != http.StatusUnauthorized {
   2.188 +		t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
   2.189 +	}
   2.190 +	expectedBody = `{"error":"invalid_client"}`
   2.191 +	if expectedBody != strings.TrimSpace(w.Body.String()) {
   2.192 +		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
   2.193 +	}
   2.194 +	if w.Header().Get("WWW-Authenticate") != "Basic" {
   2.195 +		t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
   2.196 +	}
   2.197 +
   2.198 +	// verifyClient with auth header, no public clients, client ID valid but not in clientStore
   2.199 +	w = httptest.NewRecorder()
   2.200 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.201 +	if err != nil {
   2.202 +		t.Fatal("Can't build request:", err)
   2.203 +	}
   2.204 +	r.SetBasicAuth(uuid.NewID().String(), "non existent client ID set")
   2.205 +	resp, success = verifyClient(w, r, false, context)
   2.206 +	if success {
   2.207 +		t.Error("Expected verification to fail, but succeeded with client ID:", resp)
   2.208 +	}
   2.209 +	if resp != nil {
   2.210 +		t.Error("Expected nil client ID, got", resp)
   2.211 +	}
   2.212 +	if w.Code != http.StatusUnauthorized {
   2.213 +		t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
   2.214 +	}
   2.215 +	expectedBody = `{"error":"invalid_client"}`
   2.216 +	if expectedBody != strings.TrimSpace(w.Body.String()) {
   2.217 +		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
   2.218 +	}
   2.219 +	if w.Header().Get("WWW-Authenticate") != "Basic" {
   2.220 +		t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
   2.221 +	}
   2.222 +
   2.223 +	// verifyClient with auth header, public clients, client ID valid but not in clientStore
   2.224 +	w = httptest.NewRecorder()
   2.225 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.226 +	if err != nil {
   2.227 +		t.Fatal("Can't build request:", err)
   2.228 +	}
   2.229 +	r.SetBasicAuth(uuid.NewID().String(), "non existent client ID set")
   2.230 +	resp, success = verifyClient(w, r, true, context)
   2.231 +	if success {
   2.232 +		t.Error("Expected verification to fail, but succeeded with client ID:", resp)
   2.233 +	}
   2.234 +	if resp != nil {
   2.235 +		t.Error("Expected nil client ID, got", resp)
   2.236 +	}
   2.237 +	if w.Code != http.StatusUnauthorized {
   2.238 +		t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
   2.239 +	}
   2.240 +	expectedBody = `{"error":"invalid_client"}`
   2.241 +	if expectedBody != strings.TrimSpace(w.Body.String()) {
   2.242 +		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
   2.243 +	}
   2.244 +	if w.Header().Get("WWW-Authenticate") != "Basic" {
   2.245 +		t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
   2.246 +	}
   2.247 +
   2.248 +	// verifyClient with auth header, no public clients, client secret wrong
   2.249 +	w = httptest.NewRecorder()
   2.250 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.251 +	if err != nil {
   2.252 +		t.Fatal("Can't build request:", err)
   2.253 +	}
   2.254 +	r.SetBasicAuth(client.ID.String(), "not actually the secret")
   2.255 +	resp, success = verifyClient(w, r, false, context)
   2.256 +	if success {
   2.257 +		t.Error("Expected verification to fail, but succeeded with client ID:", resp)
   2.258 +	}
   2.259 +	if resp != nil {
   2.260 +		t.Error("Expected nil client ID, got", resp)
   2.261 +	}
   2.262 +	if w.Code != http.StatusUnauthorized {
   2.263 +		t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
   2.264 +	}
   2.265 +	expectedBody = `{"error":"invalid_client"}`
   2.266 +	if expectedBody != strings.TrimSpace(w.Body.String()) {
   2.267 +		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
   2.268 +	}
   2.269 +	if w.Header().Get("WWW-Authenticate") != "Basic" {
   2.270 +		t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
   2.271 +	}
   2.272 +
   2.273 +	// verifyClient with auth header, public clients, client secret wrong
   2.274 +	w = httptest.NewRecorder()
   2.275 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.276 +	if err != nil {
   2.277 +		t.Fatal("Can't build request:", err)
   2.278 +	}
   2.279 +	r.SetBasicAuth(client.ID.String(), "not actually the secret")
   2.280 +	resp, success = verifyClient(w, r, true, context)
   2.281 +	if success {
   2.282 +		t.Error("Expected verification to fail, but succeeded with client ID:", resp)
   2.283 +	}
   2.284 +	if resp != nil {
   2.285 +		t.Error("Expected nil client ID, got", resp)
   2.286 +	}
   2.287 +	if w.Code != http.StatusUnauthorized {
   2.288 +		t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
   2.289 +	}
   2.290 +	expectedBody = `{"error":"invalid_client"}`
   2.291 +	if expectedBody != strings.TrimSpace(w.Body.String()) {
   2.292 +		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
   2.293 +	}
   2.294 +	if w.Header().Get("WWW-Authenticate") != "Basic" {
   2.295 +		t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
   2.296 +	}
   2.297 +
   2.298 +	// verifyClient with no auth header, public clients, invalid client_id post form value
   2.299 +	w = httptest.NewRecorder()
   2.300 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.301 +	if err != nil {
   2.302 +		t.Fatal("Can't build request:", err)
   2.303 +	}
   2.304 +	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   2.305 +	params := url.Values{}
   2.306 +	params.Set("client_id", "not an actual id")
   2.307 +	body := bytes.NewBufferString(params.Encode())
   2.308 +	r.Body = ioutil.NopCloser(body)
   2.309 +	resp, success = verifyClient(w, r, true, context)
   2.310 +	if success {
   2.311 +		t.Error("Expected verification to fail, but succeeded with client ID:", resp)
   2.312 +	}
   2.313 +	if resp != nil {
   2.314 +		t.Error("Expected nil client ID, got", resp)
   2.315 +	}
   2.316 +	if w.Code != http.StatusUnauthorized {
   2.317 +		t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
   2.318 +	}
   2.319 +	expectedBody = `{"error":"invalid_client"}`
   2.320 +	if expectedBody != strings.TrimSpace(w.Body.String()) {
   2.321 +		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
   2.322 +	}
   2.323 +
   2.324 +	// verifyClient with no auth header, public clients, client_id valid but not in clientStore
   2.325 +	w = httptest.NewRecorder()
   2.326 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.327 +	if err != nil {
   2.328 +		t.Fatal("Can't build request:", err)
   2.329 +	}
   2.330 +	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   2.331 +	params = url.Values{}
   2.332 +	params.Set("client_id", uuid.NewID().String())
   2.333 +	body = bytes.NewBufferString(params.Encode())
   2.334 +	r.Body = ioutil.NopCloser(body)
   2.335 +	resp, success = verifyClient(w, r, true, context)
   2.336 +	if success {
   2.337 +		t.Error("Expected verification to fail, but succeeded with client ID:", resp)
   2.338 +	}
   2.339 +	if resp != nil {
   2.340 +		t.Error("Expected nil client ID, got", resp)
   2.341 +	}
   2.342 +	if w.Code != http.StatusUnauthorized {
   2.343 +		t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
   2.344 +	}
   2.345 +	expectedBody = `{"error":"invalid_client"}`
   2.346 +	if expectedBody != strings.TrimSpace(w.Body.String()) {
   2.347 +		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
   2.348 +	}
   2.349 +
   2.350 +	// verifyClient with auth header, public clients, valid client_id and secret
   2.351 +	w = httptest.NewRecorder()
   2.352 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.353 +	if err != nil {
   2.354 +		t.Fatal("Can't build request:", err)
   2.355 +	}
   2.356 +	r.SetBasicAuth(client.ID.String(), client.Secret)
   2.357 +	resp, success = verifyClient(w, r, true, context)
   2.358 +	if !success {
   2.359 +		t.Error("Expected verification to succeed, but it failed")
   2.360 +	}
   2.361 +	if !client.ID.Equal(resp) {
   2.362 +		t.Errorf("Expected client ID to be %s, got %s", client.ID, resp)
   2.363 +	}
   2.364 +	if w.Code != http.StatusOK {
   2.365 +		t.Errorf("Expected status code of %d, got %d", http.StatusOK, w.Code)
   2.366 +	}
   2.367 +	if w.Body.String() != "" {
   2.368 +		t.Errorf(`Expected empty body, got "%s"`, w.Body.String())
   2.369 +	}
   2.370 +
   2.371 +	// verifyClient with auth header, no public clients, valid client_id and secret
   2.372 +	w = httptest.NewRecorder()
   2.373 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.374 +	if err != nil {
   2.375 +		t.Fatal("Can't build request:", err)
   2.376 +	}
   2.377 +	r.SetBasicAuth(client.ID.String(), client.Secret)
   2.378 +	resp, success = verifyClient(w, r, true, context)
   2.379 +	if !success {
   2.380 +		t.Error("Expected verification to succeed, but it failed")
   2.381 +	}
   2.382 +	if !client.ID.Equal(resp) {
   2.383 +		t.Errorf("Expected client ID to be %s, got %s", client.ID, resp)
   2.384 +	}
   2.385 +	if w.Code != http.StatusOK {
   2.386 +		t.Errorf("Expected status code of %d, got %d", http.StatusOK, w.Code)
   2.387 +	}
   2.388 +	if w.Body.String() != "" {
   2.389 +		t.Errorf(`Expected empty body, got "%s"`, w.Body.String())
   2.390 +	}
   2.391 +
   2.392 +	// verifyClient with no auth header, public clients, valid client_id and secret
   2.393 +	w = httptest.NewRecorder()
   2.394 +	r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
   2.395 +	if err != nil {
   2.396 +		t.Fatal("Can't build request:", err)
   2.397 +	}
   2.398 +	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   2.399 +	params = url.Values{}
   2.400 +	params.Set("client_id", publicClient.ID.String())
   2.401 +	body = bytes.NewBufferString(params.Encode())
   2.402 +	r.Body = ioutil.NopCloser(body)
   2.403 +	resp, success = verifyClient(w, r, true, context)
   2.404 +	if !success {
   2.405 +		t.Error("Expected verification to succeed, but it failed")
   2.406 +	}
   2.407 +	if !publicClient.ID.Equal(resp) {
   2.408 +		t.Errorf("Expected client ID to be %s, got %s", publicClient.ID, resp)
   2.409 +	}
   2.410 +	if w.Code != http.StatusOK {
   2.411 +		t.Errorf("Expected status code of %d, got %d", http.StatusOK, w.Code)
   2.412 +	}
   2.413 +	if w.Body.String() != "" {
   2.414 +		t.Errorf(`Expected empty body, got "%s"`, w.Body.String())
   2.415 +	}
   2.416 +}