github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/tenantfetchersvc/handler_test.go (about) 1 package tenantfetchersvc_test 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "net/http/httptest" 10 "testing" 11 12 "github.com/gorilla/mux" 13 "github.com/kyma-incubator/compass/components/director/internal/tenantfetchersvc" 14 "github.com/kyma-incubator/compass/components/director/internal/tenantfetchersvc/automock" 15 "github.com/kyma-incubator/compass/components/director/pkg/persistence/txtest" 16 "github.com/pkg/errors" 17 "github.com/stretchr/testify/assert" 18 "github.com/stretchr/testify/mock" 19 ) 20 21 type regionalTenantCreationRequest struct { 22 SubaccountID string `json:"subaccountTenantId"` 23 TenantID string `json:"tenantId"` 24 Subdomain string `json:"subdomain"` 25 SubscriptionProviderID string `json:"subscriptionProviderId"` 26 ProviderSubaccountID string `json:"providerSubaccountId"` 27 ConsumerTenantID string `json:"consumerTenantID"` 28 SubscriptionProviderAppName string `json:"subscriptionProviderAppName"` 29 } 30 31 type errReader int 32 33 func (errReader) Read(p []byte) (n int, err error) { 34 return 0, errors.New("test error") 35 } 36 37 func TestService_SubscriptionFlows(t *testing.T) { 38 // GIVEN 39 region := "eu-1" 40 41 target := "http://example.com/foo/:region" 42 txtest.CtxWithDBMatcher() 43 44 validRequestBody, err := json.Marshal(regionalTenantCreationRequest{ 45 SubaccountID: subaccountTenantExtID, 46 TenantID: tenantExtID, 47 Subdomain: regionalTenantSubdomain, 48 SubscriptionProviderID: subscriptionProviderID, 49 ProviderSubaccountID: providerSubaccountID, 50 ConsumerTenantID: consumerTenantID, 51 SubscriptionProviderAppName: subscriptionProviderAppName, 52 }) 53 assert.NoError(t, err) 54 55 bodyWithMissingParent, err := json.Marshal(regionalTenantCreationRequest{ 56 SubaccountID: subaccountTenantExtID, 57 Subdomain: regionalTenantSubdomain, 58 SubscriptionProviderID: subscriptionProviderID, 59 ProviderSubaccountID: providerSubaccountID, 60 ConsumerTenantID: consumerTenantID, 61 SubscriptionProviderAppName: subscriptionProviderAppName, 62 }) 63 assert.NoError(t, err) 64 65 bodyWithMissingTenantSubdomain, err := json.Marshal(regionalTenantCreationRequest{ 66 SubaccountID: subaccountTenantExtID, 67 TenantID: tenantExtID, 68 SubscriptionProviderID: subscriptionProviderID, 69 ProviderSubaccountID: providerSubaccountID, 70 ConsumerTenantID: consumerTenantID, 71 SubscriptionProviderAppName: subscriptionProviderAppName, 72 }) 73 assert.NoError(t, err) 74 75 bodyWithMissingSubscriptionConsumerID, err := json.Marshal(regionalTenantCreationRequest{ 76 SubaccountID: subaccountTenantExtID, 77 TenantID: tenantExtID, 78 Subdomain: regionalTenantSubdomain, 79 ProviderSubaccountID: providerSubaccountID, 80 ConsumerTenantID: consumerTenantID, 81 SubscriptionProviderAppName: subscriptionProviderAppName, 82 }) 83 assert.NoError(t, err) 84 85 bodyWithMatchingAccountAndSubaccountIDs, err := json.Marshal(regionalTenantCreationRequest{ 86 TenantID: tenantExtID, 87 SubaccountID: tenantExtID, 88 Subdomain: regionalTenantSubdomain, 89 SubscriptionProviderID: subscriptionProviderID, 90 ProviderSubaccountID: providerSubaccountID, 91 ConsumerTenantID: consumerTenantID, 92 SubscriptionProviderAppName: subscriptionProviderAppName, 93 }) 94 assert.NoError(t, err) 95 96 bodyWithMissingProviderSubaccountID, err := json.Marshal(regionalTenantCreationRequest{ 97 SubaccountID: subaccountTenantExtID, 98 TenantID: tenantExtID, 99 Subdomain: regionalTenantSubdomain, 100 SubscriptionProviderID: subscriptionProviderID, 101 ConsumerTenantID: consumerTenantID, 102 SubscriptionProviderAppName: subscriptionProviderAppName, 103 }) 104 assert.NoError(t, err) 105 106 bodyWithMissingConsumerTenantID, err := json.Marshal(regionalTenantCreationRequest{ 107 SubaccountID: subaccountTenantExtID, 108 TenantID: tenantExtID, 109 Subdomain: regionalTenantSubdomain, 110 SubscriptionProviderID: subscriptionProviderID, 111 ProviderSubaccountID: providerSubaccountID, 112 SubscriptionProviderAppName: subscriptionProviderAppName, 113 }) 114 assert.NoError(t, err) 115 116 bodyWithMissingSubscriptionProviderAppName, err := json.Marshal(regionalTenantCreationRequest{ 117 SubaccountID: subaccountTenantExtID, 118 TenantID: tenantExtID, 119 Subdomain: regionalTenantSubdomain, 120 SubscriptionProviderID: subscriptionProviderID, 121 ProviderSubaccountID: providerSubaccountID, 122 ConsumerTenantID: consumerTenantID, 123 }) 124 assert.NoError(t, err) 125 126 validHandlerConfig := tenantfetchersvc.HandlerConfig{ 127 RegionPathParam: "region", 128 TenantProviderConfig: tenantfetchersvc.TenantProviderConfig{ 129 TenantProvider: testProviderName, 130 TenantIDProperty: tenantProviderTenantIDProperty, 131 SubaccountTenantIDProperty: tenantProviderSubaccountTenantIDProperty, 132 CustomerIDProperty: tenantProviderCustomerIDProperty, 133 SubdomainProperty: tenantProviderSubdomainProperty, 134 SubscriptionProviderIDProperty: subscriptionProviderIDProperty, 135 ProviderSubaccountIDProperty: providerSubaccountIDProperty, 136 ConsumerTenantIDProperty: consumerTenantIDProperty, 137 SubscriptionProviderAppNameProperty: subscriptionProviderAppNameProperty, 138 }, 139 } 140 regionalTenant := tenantfetchersvc.TenantSubscriptionRequest{ 141 SubaccountTenantID: subaccountTenantExtID, 142 AccountTenantID: tenantExtID, 143 Subdomain: regionalTenantSubdomain, 144 Region: region, 145 SubscriptionProviderID: subscriptionProviderID, 146 ProviderSubaccountID: providerSubaccountID, 147 ConsumerTenantID: consumerTenantID, 148 SubscriptionProviderAppName: subscriptionProviderAppName, 149 SubscriptionPayload: string(validRequestBody), 150 } 151 152 regionalTenantWithMatchingParentID := tenantfetchersvc.TenantSubscriptionRequest{ 153 SubaccountTenantID: "", 154 AccountTenantID: tenantExtID, 155 Subdomain: regionalTenantSubdomain, 156 Region: region, 157 SubscriptionProviderID: subscriptionProviderID, 158 ProviderSubaccountID: providerSubaccountID, 159 ConsumerTenantID: consumerTenantID, 160 SubscriptionProviderAppName: subscriptionProviderAppName, 161 SubscriptionPayload: string(bodyWithMatchingAccountAndSubaccountIDs), 162 } 163 164 // Subscribe flow 165 testCases := []struct { 166 Name string 167 TenantSubscriberFn func() *automock.TenantSubscriber 168 Request *http.Request 169 Region string 170 ExpectedErrorOutput string 171 ExpectedSuccessOutput string 172 ExpectedStatusCode int 173 }{ 174 { 175 Name: "Succeeds", 176 TenantSubscriberFn: func() *automock.TenantSubscriber { 177 subscriber := &automock.TenantSubscriber{} 178 subscriber.On("Subscribe", mock.Anything, ®ionalTenant).Return(nil).Once() 179 return subscriber 180 }, 181 Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(validRequestBody)), 182 Region: region, 183 ExpectedSuccessOutput: compassURL, 184 ExpectedStatusCode: http.StatusOK, 185 }, 186 { 187 Name: "Succeeds to create account tenant when account ID and subaccount IDs are matching", 188 TenantSubscriberFn: func() *automock.TenantSubscriber { 189 subscriber := &automock.TenantSubscriber{} 190 subscriber.On("Subscribe", mock.Anything, ®ionalTenantWithMatchingParentID).Return(nil).Once() 191 return subscriber 192 }, 193 Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(bodyWithMatchingAccountAndSubaccountIDs)), 194 Region: region, 195 ExpectedSuccessOutput: compassURL, 196 ExpectedStatusCode: http.StatusOK, 197 }, 198 { 199 Name: "Returns error when region path parameter is missing", 200 TenantSubscriberFn: func() *automock.TenantSubscriber { return &automock.TenantSubscriber{} }, 201 Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(validRequestBody)), 202 ExpectedStatusCode: http.StatusBadRequest, 203 ExpectedErrorOutput: "Region path parameter is missing from request", 204 }, 205 { 206 Name: "Returns error when parent tenant is not found in body", 207 TenantSubscriberFn: func() *automock.TenantSubscriber { return &automock.TenantSubscriber{} }, 208 Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(bodyWithMissingParent)), 209 Region: region, 210 ExpectedStatusCode: http.StatusBadRequest, 211 ExpectedErrorOutput: fmt.Sprintf("mandatory property %q is missing from request body", tenantProviderTenantIDProperty), 212 }, 213 { 214 Name: "Returns error when SubscriptionProviderID is not found in body", 215 TenantSubscriberFn: func() *automock.TenantSubscriber { return &automock.TenantSubscriber{} }, 216 Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(bodyWithMissingSubscriptionConsumerID)), 217 Region: region, 218 ExpectedStatusCode: http.StatusBadRequest, 219 ExpectedErrorOutput: fmt.Sprintf("mandatory property %q is missing from request body", subscriptionProviderIDProperty), 220 }, 221 { 222 Name: "Returns error when providerSubaccountIDProperty is not found in body", 223 TenantSubscriberFn: func() *automock.TenantSubscriber { return &automock.TenantSubscriber{} }, 224 Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(bodyWithMissingProviderSubaccountID)), 225 Region: region, 226 ExpectedStatusCode: http.StatusBadRequest, 227 ExpectedErrorOutput: fmt.Sprintf("mandatory property %q is missing from request body", providerSubaccountIDProperty), 228 }, 229 { 230 Name: "Returns error when consumerTenantIDProperty is not found in body", 231 TenantSubscriberFn: func() *automock.TenantSubscriber { return &automock.TenantSubscriber{} }, 232 Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(bodyWithMissingConsumerTenantID)), 233 Region: region, 234 ExpectedStatusCode: http.StatusBadRequest, 235 ExpectedErrorOutput: fmt.Sprintf("mandatory property %q is missing from request body", consumerTenantIDProperty), 236 }, 237 { 238 Name: "Returns error when subscriptionProviderAppNameProperty is not found in body", 239 TenantSubscriberFn: func() *automock.TenantSubscriber { return &automock.TenantSubscriber{} }, 240 Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(bodyWithMissingSubscriptionProviderAppName)), 241 Region: region, 242 ExpectedStatusCode: http.StatusBadRequest, 243 ExpectedErrorOutput: fmt.Sprintf("mandatory property %q is missing from request body", subscriptionProviderAppNameProperty), 244 }, 245 { 246 Name: "Returns error when request body doesn't contain tenant subdomain", 247 TenantSubscriberFn: func() *automock.TenantSubscriber { return &automock.TenantSubscriber{} }, 248 Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(bodyWithMissingTenantSubdomain)), 249 Region: region, 250 ExpectedErrorOutput: fmt.Sprintf("mandatory property %q is missing from request body", tenantProviderSubdomainProperty), 251 ExpectedStatusCode: http.StatusBadRequest, 252 }, 253 { 254 Name: "Returns error when reading request body fails", 255 TenantSubscriberFn: func() *automock.TenantSubscriber { return &automock.TenantSubscriber{} }, 256 Request: httptest.NewRequest(http.MethodPut, target, errReader(0)), 257 Region: region, 258 ExpectedErrorOutput: tenantfetchersvc.InternalServerError, 259 ExpectedStatusCode: http.StatusInternalServerError, 260 }, 261 { 262 Name: "Returns error when tenant subscription fails", 263 TenantSubscriberFn: func() *automock.TenantSubscriber { 264 subscriber := &automock.TenantSubscriber{} 265 subscriber.On("Subscribe", mock.Anything, ®ionalTenant).Return(testError).Once() 266 return subscriber 267 }, 268 Request: httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(validRequestBody)), 269 Region: region, 270 ExpectedStatusCode: http.StatusInternalServerError, 271 ExpectedErrorOutput: tenantfetchersvc.InternalServerError, 272 }, 273 } 274 275 for _, testCase := range testCases { 276 t.Run(testCase.Name, func(t *testing.T) { 277 subscriber := testCase.TenantSubscriberFn() 278 defer mock.AssertExpectationsForObjects(t, subscriber) 279 280 handler := tenantfetchersvc.NewTenantsHTTPHandler(subscriber, validHandlerConfig) 281 req := testCase.Request 282 283 if len(testCase.Region) > 0 { 284 vars := map[string]string{ 285 "region": testCase.Region, 286 } 287 req = mux.SetURLVars(req, vars) 288 } 289 290 w := httptest.NewRecorder() 291 292 // WHEN 293 handler.SubscribeTenant(w, req) 294 295 // THEN 296 resp := w.Result() 297 body, err := io.ReadAll(resp.Body) 298 assert.NoError(t, err) 299 300 if len(testCase.ExpectedErrorOutput) > 0 { 301 assert.Contains(t, string(body), testCase.ExpectedErrorOutput) 302 } else { 303 assert.NoError(t, err) 304 } 305 306 if testCase.ExpectedSuccessOutput != "" { 307 assert.Equal(t, testCase.ExpectedSuccessOutput, string(body)) 308 } 309 310 assert.Equal(t, testCase.ExpectedStatusCode, resp.StatusCode) 311 }) 312 } 313 314 // Unsubscribe flow 315 t.Run("Unsubscribe", func(t *testing.T) { 316 subscriber := &automock.TenantSubscriber{} 317 subscriber.On("Unsubscribe", mock.Anything, ®ionalTenant).Return(testError).Once() 318 defer mock.AssertExpectationsForObjects(t, subscriber) 319 320 handler := tenantfetchersvc.NewTenantsHTTPHandler(subscriber, validHandlerConfig) 321 req := httptest.NewRequest(http.MethodPut, target, bytes.NewBuffer(validRequestBody)) 322 323 vars := map[string]string{ 324 validHandlerConfig.RegionPathParam: region, 325 } 326 req = mux.SetURLVars(req, vars) 327 328 w := httptest.NewRecorder() 329 330 // WHEN 331 handler.UnSubscribeTenant(w, req) 332 333 // THEN 334 resp := w.Result() 335 body, err := io.ReadAll(resp.Body) 336 assert.NoError(t, err) 337 338 assert.Contains(t, string(body), tenantfetchersvc.InternalServerError) 339 assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) 340 }) 341 } 342 343 func TestService_Dependencies(t *testing.T) { 344 const ( 345 regionPathVar = "region" 346 missingRegion = "eu-2" 347 existingRegion = "eu-1" 348 xsappname = "xsappname" 349 ) 350 target := fmt.Sprintf("/v1/regional/:%s/dependencies", regionPathVar) 351 352 subscriberSvc := &automock.TenantSubscriber{} 353 354 validHandlerConfig := tenantfetchersvc.HandlerConfig{ 355 RegionPathParam: "region", 356 RegionToDependenciesConfig: map[string][]tenantfetchersvc.Dependency{ 357 existingRegion: []tenantfetchersvc.Dependency{ 358 tenantfetchersvc.Dependency{Xsappname: xsappname}, 359 }, 360 }, 361 OmitDependenciesCallbackParam: "omit", 362 OmitDependenciesCallbackParamValue: "true", 363 } 364 365 validResponse := fmt.Sprintf("[{\"xsappname\":\"%s\"}]", xsappname) 366 validOmitParam := fmt.Sprintf("?%s=%s", validHandlerConfig.OmitDependenciesCallbackParam, validHandlerConfig.OmitDependenciesCallbackParamValue) 367 invalidOmitParam := fmt.Sprintf("?%s=%s-invalid", validHandlerConfig.OmitDependenciesCallbackParam, validHandlerConfig.OmitDependenciesCallbackParamValue) 368 369 testCases := []struct { 370 Name string 371 Request *http.Request 372 PathParams map[string]string 373 ExpectedErrorOutput string 374 ExpectedStatusCode int 375 ExpectedSuccessOutput string 376 }{ 377 { 378 Name: "Failure when region path param is missing", 379 Request: httptest.NewRequest(http.MethodGet, target, nil), 380 PathParams: map[string]string{}, 381 ExpectedStatusCode: http.StatusBadRequest, 382 ExpectedErrorOutput: "Region path parameter is missing from request", 383 }, 384 { 385 Name: "Failure when region path param is missing and omit is enabled", 386 Request: httptest.NewRequest(http.MethodGet, target+validOmitParam, nil), 387 PathParams: map[string]string{}, 388 ExpectedStatusCode: http.StatusBadRequest, 389 ExpectedErrorOutput: "Region path parameter is missing from request", 390 }, 391 { 392 Name: "Failure when region is invalid", 393 Request: httptest.NewRequest(http.MethodGet, target, nil), 394 PathParams: map[string]string{ 395 regionPathVar: missingRegion, 396 }, 397 ExpectedStatusCode: http.StatusBadRequest, 398 ExpectedErrorOutput: fmt.Sprintf("Invalid region provided: %s", missingRegion), 399 }, 400 { 401 Name: "Success when existing region is provided", 402 Request: httptest.NewRequest(http.MethodGet, target, nil), 403 PathParams: map[string]string{ 404 regionPathVar: existingRegion, 405 }, 406 ExpectedStatusCode: http.StatusOK, 407 ExpectedSuccessOutput: validResponse, 408 }, 409 { 410 Name: "Empty dependencies list when existing region is provided and omit param matches", 411 Request: httptest.NewRequest(http.MethodGet, target+validOmitParam, nil), 412 PathParams: map[string]string{ 413 regionPathVar: existingRegion, 414 }, 415 ExpectedStatusCode: http.StatusOK, 416 ExpectedSuccessOutput: "[]", 417 }, 418 { 419 Name: "Dependencies list when existing region is provided and omit param does not match", 420 Request: httptest.NewRequest(http.MethodGet, target+invalidOmitParam, nil), 421 PathParams: map[string]string{ 422 regionPathVar: existingRegion, 423 }, 424 ExpectedStatusCode: http.StatusOK, 425 ExpectedSuccessOutput: validResponse, 426 }, 427 } 428 429 for _, testCase := range testCases { 430 t.Run(testCase.Name, func(t *testing.T) { 431 defer mock.AssertExpectationsForObjects(t, subscriberSvc) 432 433 handler := tenantfetchersvc.NewTenantsHTTPHandler(subscriberSvc, validHandlerConfig) 434 req := testCase.Request 435 req = mux.SetURLVars(req, testCase.PathParams) 436 437 w := httptest.NewRecorder() 438 439 // WHEN 440 handler.Dependencies(w, req) 441 442 // THEN 443 resp := w.Result() 444 body, err := io.ReadAll(resp.Body) 445 assert.NoError(t, err) 446 447 if len(testCase.ExpectedErrorOutput) > 0 { 448 assert.Contains(t, string(body), testCase.ExpectedErrorOutput) 449 } else { 450 assert.NoError(t, err) 451 } 452 453 if testCase.ExpectedSuccessOutput != "" { 454 assert.Equal(t, testCase.ExpectedSuccessOutput, string(body)) 455 } 456 457 assert.Equal(t, testCase.ExpectedStatusCode, resp.StatusCode) 458 }) 459 } 460 } 461 func TestService_FetchTenantOnDemand(t *testing.T) { 462 const ( 463 parentIDPathVar = "tenantId" 464 tenantIDPathVar = "parentTenantId" 465 parentID = "fd116270-b71d-4c49-a4d7-4a03785a5e6a" 466 tenantID = "f09ba084-0e82-49ab-ab2e-b7ecc988312d" 467 ) 468 469 target := fmt.Sprintf("/v1/fetch/:%s/:%s", parentIDPathVar, tenantIDPathVar) 470 471 validHandlerConfig := tenantfetchersvc.HandlerConfig{ 472 TenantPathParam: "tenantId", 473 ParentTenantPathParam: "parentTenantId", 474 } 475 476 testCases := []struct { 477 Name string 478 Request *http.Request 479 PathParams map[string]string 480 TenantFetcherSvc func() *automock.TenantFetcher 481 ExpectedErrorOutput string 482 ExpectedStatusCode int 483 }{ 484 { 485 Name: "Successful fetch on-demand", 486 Request: httptest.NewRequest(http.MethodGet, target, nil), 487 PathParams: map[string]string{ 488 validHandlerConfig.ParentTenantPathParam: parentID, 489 validHandlerConfig.TenantPathParam: tenantID, 490 }, 491 TenantFetcherSvc: func() *automock.TenantFetcher { 492 svc := &automock.TenantFetcher{} 493 svc.On("SynchronizeTenant", mock.Anything, parentID, tenantID).Return(nil) 494 return svc 495 }, 496 ExpectedStatusCode: http.StatusOK, 497 }, 498 { 499 Name: "Failure when parent ID is missing", 500 Request: httptest.NewRequest(http.MethodGet, target, nil), 501 PathParams: map[string]string{ 502 validHandlerConfig.ParentTenantPathParam: "", 503 validHandlerConfig.TenantPathParam: tenantID, 504 }, 505 TenantFetcherSvc: func() *automock.TenantFetcher { return &automock.TenantFetcher{} }, 506 ExpectedStatusCode: http.StatusBadRequest, 507 ExpectedErrorOutput: "Parent tenant ID path parameter is missing from request", 508 }, 509 { 510 Name: "Failure when tenant ID is missing", 511 Request: httptest.NewRequest(http.MethodGet, target, nil), 512 PathParams: map[string]string{ 513 validHandlerConfig.ParentTenantPathParam: parentIDPathVar, 514 validHandlerConfig.TenantPathParam: "", 515 }, 516 TenantFetcherSvc: func() *automock.TenantFetcher { return &automock.TenantFetcher{} }, 517 ExpectedStatusCode: http.StatusBadRequest, 518 ExpectedErrorOutput: "Tenant path parameter is missing from request", 519 }, 520 { 521 Name: "Failure when fetch on-demand returns an error", 522 Request: httptest.NewRequest(http.MethodGet, target, nil), 523 PathParams: map[string]string{ 524 validHandlerConfig.ParentTenantPathParam: parentID, 525 validHandlerConfig.TenantPathParam: tenantID, 526 }, 527 TenantFetcherSvc: func() *automock.TenantFetcher { 528 svc := &automock.TenantFetcher{} 529 svc.On("SynchronizeTenant", mock.Anything, parentID, tenantID).Return(errors.New("error")) 530 return svc 531 }, 532 ExpectedStatusCode: http.StatusInternalServerError, 533 }, 534 } 535 for _, testCase := range testCases { 536 t.Run(testCase.Name, func(t *testing.T) { 537 tf := testCase.TenantFetcherSvc() 538 defer mock.AssertExpectationsForObjects(t, tf) 539 540 handler := tenantfetchersvc.NewTenantFetcherHTTPHandler(tf, validHandlerConfig) 541 req := testCase.Request 542 req = mux.SetURLVars(req, testCase.PathParams) 543 544 w := httptest.NewRecorder() 545 546 // WHEN 547 handler.FetchTenantOnDemand(w, req) 548 549 // THEN 550 resp := w.Result() 551 body, err := io.ReadAll(resp.Body) 552 assert.NoError(t, err) 553 554 if len(testCase.ExpectedErrorOutput) > 0 { 555 assert.Contains(t, string(body), testCase.ExpectedErrorOutput) 556 } else { 557 assert.NoError(t, err) 558 } 559 560 assert.Equal(t, testCase.ExpectedStatusCode, resp.StatusCode) 561 }) 562 } 563 }