auth
auth/client_test.go
Make all tests that deal with the store interfaces go through the Context. This is mainly important so that pre- and post- save/retrieval/deletion/whatever transforms can be done without doing them in every single implementation of the store. Change the Endpoint URI property to be a string, not a *url.URL. This makes testing easier, JSON responses cleaner, and is all around just a better strategy. Just because we turn it into a URL every now and then doesn't mean that's how we need to store it. Add JSON tags to the Client type and Endpoint type. Create normalizeURI and normalizeURIString methods to... well, normalize the Endpoint URIs. This makes it so that we can compare them, and forgive some arbitrary user behaviour (like slashes, etc.) Add a NormalizedURI property to the Endpoint type. This is where we store the NormalizedURI, which is what we'll be using when we want to check if an endpoint is valid or not. For the sake of tests and predictability, however, we always want to redirect to the URI, not the NormalizedURI. Add checks to the Client creation API endpoint to give better errors. Now leaving out the Type won't be considered an invalid type, it will be considered a missing parameter. An empty name will be reported as a missing parameter, a name with too few characters will be reported as an insufficient name, and a name with too many characters will be reported as an overflow name. We gather as many of these errors as apply before returning. Check if an Endpoint URI is absolute before adding it as an endpoint, or return an invalid value error if it is not. Always return the errors array when creating a client. We could succeed in creating one or more things and still have errors. We should return anything that's created _as well as_ any errors encountered. Add unit testing for our CreateClientHandler. Fix our oauth2 tests so that if there's an error in the body, it's in the test logs. This should help debugging significantly. Fix our oauth2 tests so that the Profile only requires 1 iteration for its password hashing. This means each time we want to validate a session, it doesn't add a full second to our test runs. This is a big speed improvement for our tests. Add test helper methods for comparing API errors, API responses, and filling in server-generated information in a response that it's impossible to have an expectation around (e.g., IDs) so that we can use our comparison helpers to check if a response is as we expect it. Fix a typo in our Context helpers that was reporting no sessionStore being set _only_ when a sessionStore was set. So yes, the opposite of what we wanted. Oops. This was discovered by passing all our tests through the context. methods instead of operating on the stores themselves.
1.1 --- a/client_test.go Wed Jan 14 00:23:30 2015 -0500 1.2 +++ b/client_test.go Sun Jan 18 01:02:14 2015 -0500 1.3 @@ -2,6 +2,7 @@ 1.4 1.5 import ( 1.6 "bytes" 1.7 + "encoding/json" 1.8 "fmt" 1.9 "io/ioutil" 1.10 "net/http" 1.11 @@ -60,7 +61,7 @@ 1.12 if !endpoint1.Added.Equal(endpoint2.Added) { 1.13 return false, "Added", endpoint1.Added, endpoint2.Added 1.14 } 1.15 - if endpoint1.URI.String() != endpoint2.URI.String() { 1.16 + if endpoint1.URI != endpoint2.URI { 1.17 return false, "URI", endpoint1.URI, endpoint2.URI 1.18 } 1.19 return true, "", nil, nil 1.20 @@ -77,15 +78,16 @@ 1.21 Website: "website", 1.22 } 1.23 for _, store := range clientStores { 1.24 - err := store.saveClient(client) 1.25 + context := Context{clients: store} 1.26 + err := context.SaveClient(client) 1.27 if err != nil { 1.28 t.Fatalf("Error saving client to %T: %s", store, err) 1.29 } 1.30 - err = store.saveClient(client) 1.31 + err = context.SaveClient(client) 1.32 if err != ErrClientAlreadyExists { 1.33 t.Fatalf("Expected ErrClientAlreadyExists, got %v from %T", err, store) 1.34 } 1.35 - retrieved, err := store.getClient(client.ID) 1.36 + retrieved, err := context.GetClient(client.ID) 1.37 if err != nil { 1.38 t.Fatalf("Error retrieving client from %T: %s", store, err) 1.39 } 1.40 @@ -93,7 +95,7 @@ 1.41 if !success { 1.42 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result) 1.43 } 1.44 - clients, err := store.listClientsByOwner(client.OwnerID, 25, 0) 1.45 + clients, err := context.ListClientsByOwner(client.OwnerID, 25, 0) 1.46 if err != nil { 1.47 t.Fatalf("Error retrieving clients by owner from %T: %s", store, err) 1.48 } 1.49 @@ -104,19 +106,19 @@ 1.50 if !success { 1.51 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result) 1.52 } 1.53 - err = store.deleteClient(client.ID) 1.54 + err = context.DeleteClient(client.ID) 1.55 if err != nil { 1.56 t.Fatalf("Error deleting client from %T: %s", store, err) 1.57 } 1.58 - err = store.deleteClient(client.ID) 1.59 + err = context.DeleteClient(client.ID) 1.60 if err != ErrClientNotFound { 1.61 t.Fatalf("Expected ErrClientNotFound, got %s from %T", err, store) 1.62 } 1.63 - retrieved, err = store.getClient(client.ID) 1.64 + retrieved, err = context.GetClient(client.ID) 1.65 if err != ErrClientNotFound { 1.66 t.Fatalf("Expected ErrClientNotFound from %T, got %+v and %s", store, retrieved, err) 1.67 } 1.68 - clients, err = store.listClientsByOwner(client.OwnerID, 25, 0) 1.69 + clients, err = context.ListClientsByOwner(client.OwnerID, 25, 0) 1.70 if err != nil { 1.71 t.Fatalf("Error listing clients by owner from %T: %s", store, err) 1.72 } 1.73 @@ -136,30 +138,29 @@ 1.74 Logo: "logo", 1.75 Website: "website", 1.76 } 1.77 - uri1, _ := url.Parse("https://www.example.com/") 1.78 - uri2, _ := url.Parse("https://www.example.com/my/full/path") 1.79 endpoint1 := Endpoint{ 1.80 ID: uuid.NewID(), 1.81 ClientID: client.ID, 1.82 Added: time.Now(), 1.83 - URI: *uri1, 1.84 + URI: "https://www.example.com/", 1.85 } 1.86 endpoint2 := Endpoint{ 1.87 ID: uuid.NewID(), 1.88 ClientID: client.ID, 1.89 Added: time.Now(), 1.90 - URI: *uri2, 1.91 + URI: "https://www.example.com/my/full/path", 1.92 } 1.93 for _, store := range clientStores { 1.94 - err := store.saveClient(client) 1.95 + context := Context{clients: store} 1.96 + err := context.SaveClient(client) 1.97 if err != nil { 1.98 t.Fatalf("Error saving client to %T: %s", store, err) 1.99 } 1.100 - err = store.addEndpoints(client.ID, []Endpoint{endpoint1}) 1.101 + err = context.AddEndpoints(client.ID, []Endpoint{endpoint1}) 1.102 if err != nil { 1.103 t.Fatalf("Error adding endpoint to client in %T: %s", store, err) 1.104 } 1.105 - endpoints, err := store.listEndpoints(client.ID, 10, 0) 1.106 + endpoints, err := context.ListEndpoints(client.ID, 10, 0) 1.107 if err != nil { 1.108 t.Fatalf("Error retrieving endpoints from %T: %s", store, err) 1.109 } 1.110 @@ -170,11 +171,11 @@ 1.111 if !success { 1.112 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result) 1.113 } 1.114 - err = store.addEndpoints(client.ID, []Endpoint{endpoint2}) 1.115 + err = context.AddEndpoints(client.ID, []Endpoint{endpoint2}) 1.116 if err != nil { 1.117 t.Fatalf("Error adding endpoint to client in %T: %s", store, err) 1.118 } 1.119 - endpoints, err = store.listEndpoints(client.ID, 10, 0) 1.120 + endpoints, err = context.ListEndpoints(client.ID, 10, 0) 1.121 if err != nil { 1.122 t.Fatalf("Error retrieving endpoints from %T: %s", store, err) 1.123 } 1.124 @@ -192,11 +193,11 @@ 1.125 if !success { 1.126 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result) 1.127 } 1.128 - err = store.removeEndpoint(client.ID, endpoint1.ID) 1.129 + err = context.RemoveEndpoint(client.ID, endpoint1.ID) 1.130 if err != nil { 1.131 t.Fatalf("Error removing endpoint from client in %T: %s", store, err) 1.132 } 1.133 - endpoints, err = store.listEndpoints(client.ID, 10, 0) 1.134 + endpoints, err = context.ListEndpoints(client.ID, 10, 0) 1.135 if err != nil { 1.136 t.Fatalf("Error listing endpoints in %T: %s", store, err) 1.137 } 1.138 @@ -207,11 +208,11 @@ 1.139 if !success { 1.140 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result) 1.141 } 1.142 - err = store.removeEndpoint(client.ID, endpoint2.ID) 1.143 + err = context.RemoveEndpoint(client.ID, endpoint2.ID) 1.144 if err != nil { 1.145 t.Fatalf("Error removing endpoint from client in %T: %s", store, err) 1.146 } 1.147 - endpoints, err = store.listEndpoints(client.ID, 10, 0) 1.148 + endpoints, err = context.ListEndpoints(client.ID, 10, 0) 1.149 if err != nil { 1.150 t.Fatalf("Error listing endpoints in %T: %s", store, err) 1.151 } 1.152 @@ -267,27 +268,28 @@ 1.153 t.Fatalf("Expected field `%s` to be `%v`, got `%v`", field, expected, got) 1.154 } 1.155 for _, store := range clientStores { 1.156 - err := store.saveClient(client) 1.157 + context := Context{clients: store} 1.158 + err := context.SaveClient(client) 1.159 if err != nil { 1.160 t.Fatalf("Error saving client in %T: %s", store, err) 1.161 } 1.162 - err = store.updateClient(client.ID, change) 1.163 + err = context.UpdateClient(client.ID, change) 1.164 if err != nil { 1.165 t.Fatalf("Error updating client in %T: %s", store, err) 1.166 } 1.167 - retrieved, err := store.getClient(client.ID) 1.168 + retrieved, err := context.GetClient(client.ID) 1.169 if err != nil { 1.170 - t.Fatalf("Error getting profile from %T: %s", store, err) 1.171 + t.Fatalf("Error getting client from %T: %s", store, err) 1.172 } 1.173 match, field, expected, got = compareClients(expectation, retrieved) 1.174 if !match { 1.175 t.Fatalf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store) 1.176 } 1.177 - err = store.deleteClient(client.ID) 1.178 + err = context.DeleteClient(client.ID) 1.179 if err != nil { 1.180 t.Fatalf("Error deleting client from %T: %s", store, err) 1.181 } 1.182 - err = store.updateClient(client.ID, change) 1.183 + err = context.UpdateClient(client.ID, change) 1.184 if err != ErrClientNotFound { 1.185 t.Fatalf("Expected ErrClientNotFound, got %v from %T", err, store) 1.186 } 1.187 @@ -305,19 +307,17 @@ 1.188 Logo: "logo", 1.189 Website: "website", 1.190 } 1.191 - uri1, _ := url.Parse("https://www.example.com/first") 1.192 - uri2, _ := url.Parse("https://www.example.com/my/full/path") 1.193 endpoint1 := Endpoint{ 1.194 ID: uuid.NewID(), 1.195 ClientID: client.ID, 1.196 Added: time.Now(), 1.197 - URI: *uri1, 1.198 + URI: "https://www.example.com/first", 1.199 } 1.200 endpoint2 := Endpoint{ 1.201 ID: uuid.NewID(), 1.202 ClientID: client.ID, 1.203 Added: time.Now(), 1.204 - URI: *uri2, 1.205 + URI: "https://www.example.com/my/full/path", 1.206 } 1.207 candidates := map[string]bool{ 1.208 "https://www.example.com/": false, 1.209 @@ -327,20 +327,21 @@ 1.210 "https://www.example.com/my/full/path": true, 1.211 } 1.212 for _, store := range clientStores { 1.213 - err := store.saveClient(client) 1.214 + context := Context{clients: store} 1.215 + err := context.SaveClient(client) 1.216 if err != nil { 1.217 t.Fatalf("Error saving client in %T: %s", store, err) 1.218 } 1.219 - err = store.addEndpoints(client.ID, []Endpoint{endpoint1}) 1.220 + err = context.AddEndpoints(client.ID, []Endpoint{endpoint1}) 1.221 if err != nil { 1.222 t.Fatalf("Error saving endpoint in %T: %s", store, err) 1.223 } 1.224 - err = store.addEndpoints(client.ID, []Endpoint{endpoint2}) 1.225 + err = context.AddEndpoints(client.ID, []Endpoint{endpoint2}) 1.226 if err != nil { 1.227 t.Fatalf("Error saving endpoint in %T: %s", store, err) 1.228 } 1.229 for candidate, expectation := range candidates { 1.230 - result, err := store.checkEndpoint(client.ID, candidate) 1.231 + result, err := context.CheckEndpoint(client.ID, candidate) 1.232 if err != nil { 1.233 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err) 1.234 } 1.235 @@ -367,19 +368,17 @@ 1.236 Logo: "logo", 1.237 Website: "website", 1.238 } 1.239 - uri1, _ := url.Parse("https://www.example.com/first") 1.240 - uri2, _ := url.Parse("https://www.example.com/my/full/path") 1.241 endpoint1 := Endpoint{ 1.242 ID: uuid.NewID(), 1.243 ClientID: client.ID, 1.244 Added: time.Now(), 1.245 - URI: *uri1, 1.246 + URI: "https://www.example.com/first", 1.247 } 1.248 endpoint2 := Endpoint{ 1.249 ID: uuid.NewID(), 1.250 ClientID: client.ID, 1.251 Added: time.Now(), 1.252 - URI: *uri2, 1.253 + URI: "https://www.example.com/my/full/path", 1.254 } 1.255 candidates := map[string]bool{ 1.256 "https://www.example.com/": false, 1.257 @@ -389,20 +388,21 @@ 1.258 "https://www.example.com/my/full/path": true, 1.259 } 1.260 for _, store := range clientStores { 1.261 - err := store.saveClient(client) 1.262 + context := Context{clients: store} 1.263 + err := context.SaveClient(client) 1.264 if err != nil { 1.265 t.Fatalf("Error saving client in %T: %s", store, err) 1.266 } 1.267 - err = store.addEndpoints(client.ID, []Endpoint{endpoint1}) 1.268 + err = context.AddEndpoints(client.ID, []Endpoint{endpoint1}) 1.269 if err != nil { 1.270 t.Fatalf("Error saving endpoint in %T: %s", store, err) 1.271 } 1.272 - err = store.addEndpoints(client.ID, []Endpoint{endpoint2}) 1.273 + err = context.AddEndpoints(client.ID, []Endpoint{endpoint2}) 1.274 if err != nil { 1.275 t.Fatalf("Error saving endpoint in %T: %s", store, err) 1.276 } 1.277 for candidate, expectation := range candidates { 1.278 - result, err := store.checkEndpoint(client.ID, candidate) 1.279 + result, err := context.CheckEndpoint(client.ID, candidate) 1.280 if err != nil { 1.281 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err) 1.282 } 1.283 @@ -863,3 +863,143 @@ 1.284 t.Errorf(`Expected empty body, got "%s"`, w.Body.String()) 1.285 } 1.286 } 1.287 + 1.288 +func TestCreateClientHandler(t *testing.T) { 1.289 + t.Parallel() 1.290 + memstore := NewMemstore() 1.291 + c := Context{ 1.292 + clients: memstore, 1.293 + profiles: memstore, 1.294 + } 1.295 + w := httptest.NewRecorder() 1.296 + r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/clients", nil) 1.297 + if err != nil { 1.298 + t.Fatal("Can't build request:", err) 1.299 + } 1.300 + r.Header.Set("Content-Type", "application/json") 1.301 + CreateClientHandler(w, r, c) 1.302 + if w.Code != http.StatusUnauthorized { 1.303 + t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code) 1.304 + } 1.305 + expected := `{"errors":[{"error":"access_denied"}]}` 1.306 + result := strings.TrimSpace(w.Body.String()) 1.307 + if result != expected { 1.308 + t.Errorf("Expected response to be `%s`, got `%s`", expected, result) 1.309 + } 1.310 + w = httptest.NewRecorder() 1.311 + r.Header.Set("Authorization", "Not basic at all...") 1.312 + CreateClientHandler(w, r, c) 1.313 + if w.Code != http.StatusUnauthorized { 1.314 + t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code) 1.315 + } 1.316 + expected = `{"errors":[{"error":"access_denied"}]}` 1.317 + result = strings.TrimSpace(w.Body.String()) 1.318 + if result != expected { 1.319 + t.Errorf("Expected response to be `%s`, got `%s`", expected, result) 1.320 + } 1.321 + w = httptest.NewRecorder() 1.322 + r.Header.Set("Authorization", "Basic TotallyNotBase64Encoded") 1.323 + CreateClientHandler(w, r, c) 1.324 + if w.Code != http.StatusUnauthorized { 1.325 + t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code) 1.326 + } 1.327 + expected = `{"errors":[{"error":"access_denied"}]}` 1.328 + result = strings.TrimSpace(w.Body.String()) 1.329 + if result != expected { 1.330 + t.Errorf("Expected response to be `%s`, got `%s`", expected, result) 1.331 + } 1.332 + w = httptest.NewRecorder() 1.333 + r.Header.Set("Authorization", "Basic dGhpc2hhc25vY29sb24=") 1.334 + CreateClientHandler(w, r, c) 1.335 + if w.Code != http.StatusUnauthorized { 1.336 + t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code) 1.337 + } 1.338 + expected = `{"errors":[{"error":"access_denied"}]}` 1.339 + result = strings.TrimSpace(w.Body.String()) 1.340 + if result != expected { 1.341 + t.Errorf("Expected response to be `%s`, got `%s`", expected, result) 1.342 + } 1.343 + profile := Profile{ 1.344 + ID: uuid.NewID(), 1.345 + Name: "Test User", 1.346 + Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0", 1.347 + Iterations: 1, 1.348 + Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b", 1.349 + PassphraseScheme: 1, 1.350 + Compromised: false, 1.351 + LockedUntil: time.Time{}, 1.352 + PassphraseReset: "", 1.353 + PassphraseResetCreated: time.Time{}, 1.354 + Created: time.Now(), 1.355 + LastSeen: time.Time{}, 1.356 + } 1.357 + login := Login{ 1.358 + Type: "email", 1.359 + Value: "test@example.com", 1.360 + ProfileID: profile.ID, 1.361 + Created: time.Now(), 1.362 + LastUsed: time.Time{}, 1.363 + } 1.364 + w = httptest.NewRecorder() 1.365 + r.SetBasicAuth("test@example.com", "mysecurepassphrase") 1.366 + CreateClientHandler(w, r, c) 1.367 + if w.Code != http.StatusUnauthorized { 1.368 + t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code) 1.369 + } 1.370 + expected = `{"errors":[{"error":"access_denied"}]}` 1.371 + result = strings.TrimSpace(w.Body.String()) 1.372 + if result != expected { 1.373 + t.Errorf("Expected response to be `%s`, got `%s`", expected, result) 1.374 + } 1.375 + err = c.SaveProfile(profile) 1.376 + if err != nil { 1.377 + t.Error("Error saving profile:", err) 1.378 + } 1.379 + err = c.AddLogin(login) 1.380 + if err != nil { 1.381 + t.Error("Error adding login:", err) 1.382 + } 1.383 + r.SetBasicAuth("test@example.com", "mysecurepassphrase") 1.384 + type testStruct struct { 1.385 + request string 1.386 + code int 1.387 + resp response 1.388 + } 1.389 + tests := []testStruct{ 1.390 + {``, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/"}}}}, 1.391 + {`{}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}}, 1.392 + {`{"type":"notarealtype"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}}, 1.393 + {`{"type":"notarealtype","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrOverflow, Field: "/name"}}}}, 1.394 + {`{"type":"notarealtype","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrInsufficient, Field: "/name"}}}}, 1.395 + {`{"type":"public"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/name"}}}}, 1.396 + {`{"type":"public","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrOverflow, Field: "/name"}}}}, 1.397 + {`{"type":"public","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInsufficient, Field: "/name"}}}}, 1.398 + {`{"name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}}}}, 1.399 + {`{"type":"notarealtype","name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}}}}, 1.400 + {`{"type":"public","name":"My Client"}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}}}, 1.401 + {`{"type":"public","name":"My Client", "endpoints": ["https://test.secondbit.org/", "https://paddy.io"]}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://test.secondbit.org/"}, {URI: "https://paddy.io"}}}}, 1.402 + {`{"type":"public","name":"My Client", "endpoints": [":/not a url", "https://paddy.io"]}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://paddy.io"}}, Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/endpoints/0"}}}}, 1.403 + {`{"type":"public","name":"My Client", "endpoints": [":/not a url", "/relative/uri", "https://paddy.io"]}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://paddy.io"}}, Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/endpoints/0"}, {Slug: requestErrInvalidValue, Field: "/endpoints/1"}}}}, 1.404 + } 1.405 + for pos, test := range tests { 1.406 + t.Logf("Test #%d: `%s`", pos, test.request) 1.407 + w = httptest.NewRecorder() 1.408 + body := bytes.NewBufferString(test.request) 1.409 + r.Body = ioutil.NopCloser(body) 1.410 + CreateClientHandler(w, r, c) 1.411 + if w.Code != test.code { 1.412 + t.Errorf("Expected response code to be %d, got %d", test.code, w.Code) 1.413 + } 1.414 + t.Logf("Response: %s", w.Body.String()) 1.415 + var res response 1.416 + err = json.Unmarshal(w.Body.Bytes(), &res) 1.417 + if err != nil { 1.418 + t.Error("Unexpected error unmarshalling response:", err) 1.419 + } 1.420 + fillInServerGenerated(test.resp, res) 1.421 + success, field, expectation, result := compareResponses(test.resp, res) 1.422 + if !success { 1.423 + t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result) 1.424 + } 1.425 + } 1.426 +}