k8s.io/apiserver@v0.31.1/pkg/admission/plugin/webhook/request/admissionreview_test.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package request 18 19 import ( 20 "reflect" 21 "strings" 22 "testing" 23 24 "github.com/google/go-cmp/cmp" 25 26 admissionv1 "k8s.io/api/admission/v1" 27 admissionv1beta1 "k8s.io/api/admission/v1beta1" 28 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 29 appsv1 "k8s.io/api/apps/v1" 30 authenticationv1 "k8s.io/api/authentication/v1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/runtime" 33 "k8s.io/apimachinery/pkg/runtime/schema" 34 "k8s.io/apimachinery/pkg/types" 35 "k8s.io/apiserver/pkg/admission" 36 "k8s.io/apiserver/pkg/admission/plugin/webhook" 37 "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" 38 "k8s.io/apiserver/pkg/authentication/user" 39 utilpointer "k8s.io/utils/pointer" 40 ) 41 42 func TestVerifyAdmissionResponse(t *testing.T) { 43 v1beta1JSONPatch := admissionv1beta1.PatchTypeJSONPatch 44 v1JSONPatch := admissionv1.PatchTypeJSONPatch 45 46 emptyv1beta1Patch := admissionv1beta1.PatchType("") 47 emptyv1Patch := admissionv1.PatchType("") 48 49 invalidv1beta1Patch := admissionv1beta1.PatchType("Foo") 50 invalidv1Patch := admissionv1.PatchType("Foo") 51 52 testcases := []struct { 53 name string 54 uid types.UID 55 mutating bool 56 review runtime.Object 57 58 expectAuditAnnotations map[string]string 59 expectAllowed bool 60 expectPatch []byte 61 expectPatchType admissionv1.PatchType 62 expectResult *metav1.Status 63 expectErr string 64 }{ 65 // Allowed validating 66 { 67 name: "v1beta1 allowed validating", 68 uid: "123", 69 review: &admissionv1beta1.AdmissionReview{ 70 Response: &admissionv1beta1.AdmissionResponse{Allowed: true}, 71 }, 72 expectAllowed: true, 73 }, 74 { 75 name: "v1 allowed validating", 76 uid: "123", 77 review: &admissionv1.AdmissionReview{ 78 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, 79 Response: &admissionv1.AdmissionResponse{UID: "123", Allowed: true}, 80 }, 81 expectAllowed: true, 82 }, 83 // Allowed mutating 84 { 85 name: "v1beta1 allowed mutating", 86 uid: "123", 87 mutating: true, 88 review: &admissionv1beta1.AdmissionReview{ 89 Response: &admissionv1beta1.AdmissionResponse{Allowed: true}, 90 }, 91 expectAllowed: true, 92 }, 93 { 94 name: "v1 allowed mutating", 95 uid: "123", 96 mutating: true, 97 review: &admissionv1.AdmissionReview{ 98 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, 99 Response: &admissionv1.AdmissionResponse{UID: "123", Allowed: true}, 100 }, 101 expectAllowed: true, 102 }, 103 104 // Audit annotations 105 { 106 name: "v1beta1 auditAnnotations", 107 uid: "123", 108 review: &admissionv1beta1.AdmissionReview{ 109 Response: &admissionv1beta1.AdmissionResponse{ 110 Allowed: true, 111 AuditAnnotations: map[string]string{"foo": "bar"}, 112 }, 113 }, 114 expectAllowed: true, 115 expectAuditAnnotations: map[string]string{"foo": "bar"}, 116 }, 117 { 118 name: "v1 auditAnnotations", 119 uid: "123", 120 review: &admissionv1.AdmissionReview{ 121 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, 122 Response: &admissionv1.AdmissionResponse{ 123 UID: "123", 124 Allowed: true, 125 AuditAnnotations: map[string]string{"foo": "bar"}, 126 }, 127 }, 128 expectAllowed: true, 129 expectAuditAnnotations: map[string]string{"foo": "bar"}, 130 }, 131 132 // Patch 133 { 134 name: "v1beta1 patch", 135 uid: "123", 136 mutating: true, 137 review: &admissionv1beta1.AdmissionReview{ 138 Response: &admissionv1beta1.AdmissionResponse{ 139 Allowed: true, 140 Patch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 141 }, 142 }, 143 expectAllowed: true, 144 expectPatch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 145 expectPatchType: "JSONPatch", 146 }, 147 { 148 name: "v1 patch", 149 uid: "123", 150 mutating: true, 151 review: &admissionv1.AdmissionReview{ 152 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, 153 Response: &admissionv1.AdmissionResponse{ 154 UID: "123", 155 Allowed: true, 156 Patch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 157 PatchType: &v1JSONPatch, 158 }, 159 }, 160 expectAllowed: true, 161 expectPatch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 162 expectPatchType: "JSONPatch", 163 }, 164 165 // Result 166 { 167 name: "v1beta1 result", 168 uid: "123", 169 review: &admissionv1beta1.AdmissionReview{ 170 Response: &admissionv1beta1.AdmissionResponse{ 171 Allowed: false, 172 Result: &metav1.Status{Status: "Failure", Message: "Foo", Code: 401}, 173 }, 174 }, 175 expectAllowed: false, 176 expectResult: &metav1.Status{Status: "Failure", Message: "Foo", Code: 401}, 177 }, 178 { 179 name: "v1 result", 180 uid: "123", 181 review: &admissionv1.AdmissionReview{ 182 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, 183 Response: &admissionv1.AdmissionResponse{ 184 UID: "123", 185 Allowed: false, 186 Result: &metav1.Status{Status: "Failure", Message: "Foo", Code: 401}, 187 }, 188 }, 189 expectAllowed: false, 190 expectResult: &metav1.Status{Status: "Failure", Message: "Foo", Code: 401}, 191 }, 192 193 // Missing response 194 { 195 name: "v1beta1 no response", 196 uid: "123", 197 review: &admissionv1beta1.AdmissionReview{}, 198 expectErr: "response was absent", 199 }, 200 { 201 name: "v1 no response", 202 uid: "123", 203 review: &admissionv1.AdmissionReview{ 204 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, 205 }, 206 expectErr: "response was absent", 207 }, 208 209 // v1 invalid responses 210 { 211 name: "v1 wrong group", 212 uid: "123", 213 review: &admissionv1.AdmissionReview{ 214 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io2/v1", Kind: "AdmissionReview"}, 215 Response: &admissionv1.AdmissionResponse{ 216 UID: "123", 217 Allowed: true, 218 }, 219 }, 220 expectErr: "expected webhook response of admission.k8s.io/v1, Kind=AdmissionReview", 221 }, 222 { 223 name: "v1 wrong version", 224 uid: "123", 225 review: &admissionv1.AdmissionReview{ 226 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v2", Kind: "AdmissionReview"}, 227 Response: &admissionv1.AdmissionResponse{ 228 UID: "123", 229 Allowed: true, 230 }, 231 }, 232 expectErr: "expected webhook response of admission.k8s.io/v1, Kind=AdmissionReview", 233 }, 234 { 235 name: "v1 wrong kind", 236 uid: "123", 237 review: &admissionv1.AdmissionReview{ 238 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview2"}, 239 Response: &admissionv1.AdmissionResponse{ 240 UID: "123", 241 Allowed: true, 242 }, 243 }, 244 expectErr: "expected webhook response of admission.k8s.io/v1, Kind=AdmissionReview", 245 }, 246 { 247 name: "v1 wrong uid", 248 uid: "123", 249 review: &admissionv1.AdmissionReview{ 250 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, 251 Response: &admissionv1.AdmissionResponse{ 252 UID: "1234", 253 Allowed: true, 254 }, 255 }, 256 expectErr: `expected response.uid="123"`, 257 }, 258 { 259 name: "v1 patch without patch type", 260 uid: "123", 261 mutating: true, 262 review: &admissionv1.AdmissionReview{ 263 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, 264 Response: &admissionv1.AdmissionResponse{ 265 UID: "123", 266 Allowed: true, 267 Patch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 268 }, 269 }, 270 expectErr: `webhook returned response.patch but not response.patchType`, 271 }, 272 { 273 name: "v1 patch type without patch", 274 uid: "123", 275 mutating: true, 276 review: &admissionv1.AdmissionReview{ 277 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, 278 Response: &admissionv1.AdmissionResponse{ 279 UID: "123", 280 Allowed: true, 281 PatchType: &v1JSONPatch, 282 }, 283 }, 284 expectErr: `webhook returned response.patchType but not response.patch`, 285 }, 286 { 287 name: "v1 empty patch type", 288 uid: "123", 289 mutating: true, 290 review: &admissionv1.AdmissionReview{ 291 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, 292 Response: &admissionv1.AdmissionResponse{ 293 UID: "123", 294 Allowed: true, 295 Patch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 296 PatchType: &emptyv1Patch, 297 }, 298 }, 299 expectErr: `webhook returned invalid response.patchType of ""`, 300 }, 301 { 302 name: "v1 invalid patch type", 303 uid: "123", 304 mutating: true, 305 review: &admissionv1.AdmissionReview{ 306 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, 307 Response: &admissionv1.AdmissionResponse{ 308 UID: "123", 309 Allowed: true, 310 Patch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 311 PatchType: &invalidv1Patch, 312 }, 313 }, 314 expectAllowed: true, 315 expectPatch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 316 expectPatchType: invalidv1Patch, // invalid patch types are caught when the mutating dispatcher evaluates the patch 317 }, 318 { 319 name: "v1 patch for validating webhook", 320 uid: "123", 321 mutating: false, 322 review: &admissionv1.AdmissionReview{ 323 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, 324 Response: &admissionv1.AdmissionResponse{ 325 UID: "123", 326 Allowed: true, 327 Patch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 328 }, 329 }, 330 expectErr: `validating webhook may not return response.patch`, 331 }, 332 { 333 name: "v1 patch type for validating webhook", 334 uid: "123", 335 mutating: false, 336 review: &admissionv1.AdmissionReview{ 337 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io/v1", Kind: "AdmissionReview"}, 338 Response: &admissionv1.AdmissionResponse{ 339 UID: "123", 340 Allowed: true, 341 PatchType: &invalidv1Patch, 342 }, 343 }, 344 expectErr: `validating webhook may not return response.patchType`, 345 }, 346 347 // v1beta1 invalid responses that we have to allow/fixup for compatibility 348 { 349 name: "v1beta1 wrong group/version/kind", 350 uid: "123", 351 review: &admissionv1beta1.AdmissionReview{ 352 TypeMeta: metav1.TypeMeta{APIVersion: "admission.k8s.io2/v2", Kind: "AdmissionReview2"}, 353 Response: &admissionv1beta1.AdmissionResponse{ 354 Allowed: true, 355 }, 356 }, 357 expectAllowed: true, 358 }, 359 { 360 name: "v1beta1 wrong uid", 361 uid: "123", 362 review: &admissionv1beta1.AdmissionReview{ 363 Response: &admissionv1beta1.AdmissionResponse{ 364 UID: "1234", 365 Allowed: true, 366 }, 367 }, 368 expectAllowed: true, 369 }, 370 { 371 name: "v1beta1 validating returns patch/patchType", 372 uid: "123", 373 mutating: false, 374 review: &admissionv1beta1.AdmissionReview{ 375 Response: &admissionv1beta1.AdmissionResponse{ 376 UID: "1234", 377 Allowed: true, 378 Patch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 379 PatchType: &v1beta1JSONPatch, 380 }, 381 }, 382 expectAllowed: true, 383 }, 384 { 385 name: "v1beta1 empty patch type", 386 uid: "123", 387 mutating: true, 388 review: &admissionv1beta1.AdmissionReview{ 389 Response: &admissionv1beta1.AdmissionResponse{ 390 Allowed: true, 391 Patch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 392 PatchType: &emptyv1beta1Patch, 393 }, 394 }, 395 expectAllowed: true, 396 expectPatch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 397 expectPatchType: admissionv1.PatchTypeJSONPatch, 398 }, 399 { 400 name: "v1beta1 invalid patchType", 401 uid: "123", 402 mutating: true, 403 review: &admissionv1beta1.AdmissionReview{ 404 Response: &admissionv1beta1.AdmissionResponse{ 405 Allowed: true, 406 Patch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 407 PatchType: &invalidv1beta1Patch, 408 }, 409 }, 410 expectAllowed: true, 411 expectPatch: []byte(`[{"op":"add","path":"/foo","value":"bar"}]`), 412 expectPatchType: admissionv1.PatchTypeJSONPatch, 413 }, 414 } 415 416 for _, tc := range testcases { 417 t.Run(tc.name, func(t *testing.T) { 418 result, err := VerifyAdmissionResponse(tc.uid, tc.mutating, tc.review) 419 if err != nil { 420 if len(tc.expectErr) > 0 { 421 if !strings.Contains(err.Error(), tc.expectErr) { 422 t.Errorf("expected error '%s', got %v", tc.expectErr, err) 423 } 424 } else { 425 t.Errorf("unexpected error %v", err) 426 } 427 return 428 } else if len(tc.expectErr) > 0 { 429 t.Errorf("expected error '%s', got none", tc.expectErr) 430 return 431 } 432 433 if e, a := tc.expectAuditAnnotations, result.AuditAnnotations; !reflect.DeepEqual(e, a) { 434 t.Errorf("unexpected: %v", cmp.Diff(e, a)) 435 } 436 if e, a := tc.expectAllowed, result.Allowed; !reflect.DeepEqual(e, a) { 437 t.Errorf("unexpected: %v", cmp.Diff(e, a)) 438 } 439 if e, a := tc.expectPatch, result.Patch; !reflect.DeepEqual(e, a) { 440 t.Errorf("unexpected: %v", cmp.Diff(e, a)) 441 } 442 if e, a := tc.expectPatchType, result.PatchType; !reflect.DeepEqual(e, a) { 443 t.Errorf("unexpected: %v", cmp.Diff(e, a)) 444 } 445 if e, a := tc.expectResult, result.Result; !reflect.DeepEqual(e, a) { 446 t.Errorf("unexpected: %v", cmp.Diff(e, a)) 447 } 448 }) 449 } 450 } 451 452 func TestCreateAdmissionObjects(t *testing.T) { 453 internalObj := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{ResourceVersion: "2", Name: "myname", Namespace: "myns"}} 454 internalObjOld := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{ResourceVersion: "1", Name: "myname", Namespace: "myns"}} 455 versionedObj := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{ResourceVersion: "2", Name: "myname", Namespace: "myns"}} 456 versionedObjOld := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{ResourceVersion: "1", Name: "myname", Namespace: "myns"}} 457 userInfo := &user.DefaultInfo{ 458 Name: "myuser", 459 Groups: []string{"mygroup"}, 460 UID: "myuid", 461 Extra: map[string][]string{"extrakey": {"value1", "value2"}}, 462 } 463 attrs := admission.NewAttributesRecord( 464 internalObj.DeepCopyObject(), 465 internalObjOld.DeepCopyObject(), 466 schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, 467 "myns", 468 "myname", 469 schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, 470 "", 471 admission.Update, 472 &metav1.UpdateOptions{FieldManager: "foo"}, 473 false, 474 userInfo, 475 ) 476 477 testcases := []struct { 478 name string 479 attrs *admission.VersionedAttributes 480 invocation *generic.WebhookInvocation 481 482 expectRequest func(uid types.UID) runtime.Object 483 expectResponse runtime.Object 484 expectErr string 485 }{ 486 { 487 name: "no supported versions", 488 invocation: &generic.WebhookInvocation{ 489 Webhook: webhook.NewMutatingWebhookAccessor("mywebhook", "mycfg", &admissionregistrationv1.MutatingWebhook{}), 490 }, 491 expectErr: "webhook does not accept known AdmissionReview versions", 492 }, 493 { 494 name: "no known supported versions", 495 invocation: &generic.WebhookInvocation{ 496 Webhook: webhook.NewMutatingWebhookAccessor("mywebhook", "mycfg", &admissionregistrationv1.MutatingWebhook{ 497 AdmissionReviewVersions: []string{"vX"}, 498 }), 499 }, 500 expectErr: "webhook does not accept known AdmissionReview versions", 501 }, 502 { 503 name: "v1", 504 attrs: &admission.VersionedAttributes{ 505 VersionedObject: versionedObj.DeepCopyObject(), 506 VersionedOldObject: versionedObjOld.DeepCopyObject(), 507 Attributes: attrs, 508 }, 509 invocation: &generic.WebhookInvocation{ 510 Resource: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"}, 511 Subresource: "", 512 Kind: schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Deployment"}, 513 Webhook: webhook.NewMutatingWebhookAccessor("mywebhook", "mycfg", &admissionregistrationv1.MutatingWebhook{ 514 AdmissionReviewVersions: []string{"v1", "v1beta1"}, 515 }), 516 }, 517 expectRequest: func(uid types.UID) runtime.Object { 518 return &admissionv1.AdmissionReview{ 519 Request: &admissionv1.AdmissionRequest{ 520 UID: uid, 521 Kind: metav1.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Deployment"}, 522 Resource: metav1.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"}, 523 SubResource: "", 524 RequestKind: &metav1.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, 525 RequestResource: &metav1.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, 526 RequestSubResource: "", 527 Name: "myname", 528 Namespace: "myns", 529 Operation: "UPDATE", 530 UserInfo: authenticationv1.UserInfo{ 531 Username: "myuser", 532 UID: "myuid", 533 Groups: []string{"mygroup"}, 534 Extra: map[string]authenticationv1.ExtraValue{"extrakey": {"value1", "value2"}}, 535 }, 536 Object: runtime.RawExtension{Object: versionedObj}, 537 OldObject: runtime.RawExtension{Object: versionedObjOld}, 538 DryRun: utilpointer.BoolPtr(false), 539 Options: runtime.RawExtension{Object: &metav1.UpdateOptions{FieldManager: "foo"}}, 540 }, 541 } 542 }, 543 expectResponse: &admissionv1.AdmissionReview{}, 544 }, 545 { 546 name: "v1beta1", 547 attrs: &admission.VersionedAttributes{ 548 VersionedObject: versionedObj.DeepCopyObject(), 549 VersionedOldObject: versionedObjOld.DeepCopyObject(), 550 Attributes: attrs, 551 }, 552 invocation: &generic.WebhookInvocation{ 553 Resource: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"}, 554 Subresource: "", 555 Kind: schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Deployment"}, 556 Webhook: webhook.NewMutatingWebhookAccessor("mywebhook", "mycfg", &admissionregistrationv1.MutatingWebhook{ 557 AdmissionReviewVersions: []string{"v1beta1", "v1"}, 558 }), 559 }, 560 expectRequest: func(uid types.UID) runtime.Object { 561 return &admissionv1beta1.AdmissionReview{ 562 Request: &admissionv1beta1.AdmissionRequest{ 563 UID: uid, 564 Kind: metav1.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Deployment"}, 565 Resource: metav1.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"}, 566 SubResource: "", 567 RequestKind: &metav1.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, 568 RequestResource: &metav1.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, 569 RequestSubResource: "", 570 Name: "myname", 571 Namespace: "myns", 572 Operation: "UPDATE", 573 UserInfo: authenticationv1.UserInfo{ 574 Username: "myuser", 575 UID: "myuid", 576 Groups: []string{"mygroup"}, 577 Extra: map[string]authenticationv1.ExtraValue{"extrakey": {"value1", "value2"}}, 578 }, 579 Object: runtime.RawExtension{Object: versionedObj}, 580 OldObject: runtime.RawExtension{Object: versionedObjOld}, 581 DryRun: utilpointer.BoolPtr(false), 582 Options: runtime.RawExtension{Object: &metav1.UpdateOptions{FieldManager: "foo"}}, 583 }, 584 } 585 }, 586 expectResponse: &admissionv1beta1.AdmissionReview{}, 587 }, 588 } 589 590 for _, tc := range testcases { 591 t.Run(tc.name, func(t *testing.T) { 592 uid, request, response, err := CreateAdmissionObjects(tc.attrs, tc.invocation) 593 if err != nil { 594 if len(tc.expectErr) > 0 { 595 if !strings.Contains(err.Error(), tc.expectErr) { 596 t.Errorf("expected error '%s', got %v", tc.expectErr, err) 597 } 598 } else { 599 t.Errorf("unexpected error %v", err) 600 } 601 return 602 } else if len(tc.expectErr) > 0 { 603 t.Errorf("expected error '%s', got none", tc.expectErr) 604 return 605 } 606 607 if len(uid) == 0 { 608 t.Errorf("expected uid, got none") 609 } 610 if e, a := tc.expectRequest(uid), request; !reflect.DeepEqual(e, a) { 611 t.Errorf("unexpected: %v", cmp.Diff(e, a)) 612 } 613 if e, a := tc.expectResponse, response; !reflect.DeepEqual(e, a) { 614 t.Errorf("unexpected: %v", cmp.Diff(e, a)) 615 } 616 }) 617 } 618 }