github.com/avenga/couper@v1.12.2/handler/middleware/cors_test.go (about) 1 package middleware 2 3 import ( 4 "net/http" 5 "net/http/httptest" 6 "testing" 7 ) 8 9 func TestCORSOptions_AllowsOrigin(t *testing.T) { 10 tests := []struct { 11 name string 12 corsOptions *CORSOptions 13 origin string 14 exp bool 15 }{ 16 { 17 "any origin allowed, specific origin", 18 &CORSOptions{AllowedOrigins: []string{"*"}}, 19 "https://www.example.com", 20 true, 21 }, 22 { 23 "any origin allowed, *", 24 &CORSOptions{AllowedOrigins: []string{"*"}}, 25 "*", 26 true, 27 }, 28 { 29 "one specific origin allowed, specific allowed origin", 30 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}}, 31 "https://www.example.com", 32 true, 33 }, 34 { 35 "one specific origin allowed, specific disallowed origin", 36 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}}, 37 "http://www.another.host.com", 38 false, 39 }, 40 { 41 "one specific origin allowed, *", 42 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}}, 43 "*", 44 false, 45 }, 46 { 47 "several specific origins allowed, specific origin", 48 &CORSOptions{AllowedOrigins: []string{"https://www.example.com", "http://www.another.host.com"}}, 49 "https://www.example.com", 50 true, 51 }, 52 { 53 "several specific origins allowed, specific disallowed origin", 54 &CORSOptions{AllowedOrigins: []string{"https://www.example.com", "http://www.another.host.com"}}, 55 "https://www.disallowed.host.org", 56 false, 57 }, 58 { 59 "several specific origins allowed, *", 60 &CORSOptions{AllowedOrigins: []string{"https://www.example.com", "http://www.another.host.com"}}, 61 "*", 62 false, 63 }, 64 } 65 for _, tt := range tests { 66 t.Run(tt.name, func(subT *testing.T) { 67 allowed := tt.corsOptions.AllowsOrigin(tt.origin) 68 if allowed != tt.exp { 69 subT.Errorf("Expected %t, got: %t", tt.exp, allowed) 70 } 71 }) 72 } 73 } 74 75 func TestCORSOptions_isCorsRequest(t *testing.T) { 76 tests := []struct { 77 name string 78 requestHeaders map[string]string 79 exp bool 80 }{ 81 { 82 "without Origin", 83 map[string]string{}, 84 false, 85 }, 86 { 87 "with Origin", 88 map[string]string{"Origin": "https://www.example.com"}, 89 true, 90 }, 91 } 92 93 cors := &CORS{} 94 for _, tt := range tests { 95 t.Run(tt.name, func(subT *testing.T) { 96 req := httptest.NewRequest(http.MethodPost, "http://1.2.3.4/", nil) 97 for name, value := range tt.requestHeaders { 98 req.Header.Set(name, value) 99 } 100 101 corsRequest := cors.isCorsRequest(req) 102 if corsRequest != tt.exp { 103 subT.Errorf("Expected %t, got: %t", tt.exp, corsRequest) 104 } 105 }) 106 } 107 } 108 109 func TestCORSOptions_isCorsPreflightRequest(t *testing.T) { 110 tests := []struct { 111 name string 112 method string 113 requestHeaders map[string]string 114 exp bool 115 }{ 116 { 117 "OPTIONS, without Origin", 118 http.MethodOptions, 119 map[string]string{}, 120 false, 121 }, 122 { 123 "OPTIONS, with Origin", 124 http.MethodOptions, 125 map[string]string{"Origin": "https://www.example.com"}, 126 false, 127 }, 128 { 129 "POST, with Origin, with ACRM", 130 http.MethodPost, 131 map[string]string{"Origin": "https://www.example.com", "Access-Control-Request-Method": "POST"}, 132 false, 133 }, 134 { 135 "POST, with Origin, with ACRH", 136 http.MethodPost, 137 map[string]string{"Origin": "https://www.example.com", "Access-Control-Request-Headers": "Content-Type"}, 138 false, 139 }, 140 { 141 "OPTIONS, with Origin, with ACRM", 142 http.MethodOptions, 143 map[string]string{"Origin": "https://www.example.com", "Access-Control-Request-Method": "POST"}, 144 true, 145 }, 146 { 147 "OPTIONS, with Origin, with ACRH", 148 http.MethodOptions, 149 map[string]string{"Origin": "https://www.example.com", "Access-Control-Request-Headers": "Content-Type"}, 150 true, 151 }, 152 } 153 154 cors := &CORS{} 155 for _, tt := range tests { 156 t.Run(tt.name, func(subT *testing.T) { 157 req := httptest.NewRequest(tt.method, "http://1.2.3.4/", nil) 158 for name, value := range tt.requestHeaders { 159 req.Header.Set(name, value) 160 } 161 162 corsPfRequest := cors.isCorsPreflightRequest(req) 163 if corsPfRequest != tt.exp { 164 subT.Errorf("Expected %t, got: %t", tt.exp, corsPfRequest) 165 } 166 }) 167 } 168 } 169 170 func TestCORS_ServeHTTP(t *testing.T) { 171 upstreamHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 172 rw.Header().Set("Content-Type", "text/plain") 173 rw.WriteHeader(http.StatusOK) 174 _, err := rw.Write([]byte("from upstream")) 175 if err != nil { 176 t.Error(err) 177 } 178 }) 179 180 tests := []struct { 181 name string 182 corsOptions *CORSOptions 183 requestHeaders map[string]string 184 expectedResponseHeaders map[string]string 185 }{ 186 { 187 "non-CORS, specific origin", 188 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}}, 189 map[string]string{}, 190 map[string]string{ 191 "Access-Control-Allow-Origin": "", 192 "Access-Control-Allow-Credentials": "", 193 "Vary": "Origin", 194 }, 195 }, 196 { 197 "non-CORS, specific origin, allow credentials", 198 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}, AllowCredentials: true}, 199 map[string]string{}, 200 map[string]string{ 201 "Access-Control-Allow-Origin": "", 202 "Access-Control-Allow-Credentials": "", 203 "Vary": "Origin", 204 }, 205 }, 206 { 207 "non-CORS, any origin", 208 &CORSOptions{AllowedOrigins: []string{"*"}}, 209 map[string]string{}, 210 map[string]string{ 211 "Access-Control-Allow-Origin": "*", 212 "Access-Control-Allow-Credentials": "", 213 "Vary": "", 214 }, 215 }, 216 { 217 "non-CORS, any origin, allow credentials", 218 &CORSOptions{AllowedOrigins: []string{"*"}, AllowCredentials: true}, 219 map[string]string{}, 220 map[string]string{ 221 "Access-Control-Allow-Origin": "", 222 "Access-Control-Allow-Credentials": "", 223 "Vary": "Origin", 224 }, 225 }, 226 { 227 "CORS, specific origin", 228 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}}, 229 map[string]string{ 230 "Origin": "https://www.example.com", 231 }, 232 map[string]string{ 233 "Access-Control-Allow-Origin": "https://www.example.com", 234 "Access-Control-Allow-Credentials": "", 235 "Vary": "Origin", 236 }, 237 }, 238 { 239 "CORS, specific origins", 240 &CORSOptions{AllowedOrigins: []string{"https://www.example.com", "https://example.com"}}, 241 map[string]string{ 242 "Origin": "https://example.com", 243 }, 244 map[string]string{ 245 "Access-Control-Allow-Origin": "https://example.com", 246 "Access-Control-Allow-Credentials": "", 247 "Vary": "Origin", 248 }, 249 }, 250 { 251 "CORS, any origin", 252 &CORSOptions{AllowedOrigins: []string{"*"}}, 253 map[string]string{ 254 "Origin": "https://www.example.com", 255 }, 256 map[string]string{ 257 "Access-Control-Allow-Origin": "*", 258 "Access-Control-Allow-Credentials": "", 259 "Vary": "", 260 }, 261 }, 262 { 263 "CORS, any and specific origin", 264 &CORSOptions{AllowedOrigins: []string{"https://example.com", "https://www.example.com", "*"}}, 265 map[string]string{ 266 "Origin": "https://www.example.com", 267 }, 268 map[string]string{ 269 "Access-Control-Allow-Origin": "*", 270 "Access-Control-Allow-Credentials": "", 271 "Vary": "", 272 }, 273 }, 274 { 275 "CORS, specific origin, allow credentials", 276 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}, AllowCredentials: true}, 277 map[string]string{ 278 "Origin": "https://www.example.com", 279 }, 280 map[string]string{ 281 "Access-Control-Allow-Origin": "https://www.example.com", 282 "Access-Control-Allow-Credentials": "true", 283 "Vary": "Origin", 284 }, 285 }, 286 { 287 "CORS, any origin, allow credentials", 288 &CORSOptions{AllowedOrigins: []string{"*"}, AllowCredentials: true}, 289 map[string]string{ 290 "Origin": "https://www.example.com", 291 }, 292 map[string]string{ 293 "Access-Control-Allow-Origin": "https://www.example.com", 294 "Access-Control-Allow-Credentials": "true", 295 "Vary": "Origin", 296 }, 297 }, 298 { 299 "CORS, origin mismatch", 300 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}}, 301 map[string]string{ 302 "Origin": "https://www.example.org", 303 }, 304 map[string]string{ 305 "Access-Control-Allow-Origin": "", 306 "Access-Control-Allow-Credentials": "", 307 "Vary": "Origin", 308 }, 309 }, 310 { 311 "CORS, origin mismatch, allow credentials", 312 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}, AllowCredentials: true}, 313 map[string]string{ 314 "Origin": "https://www.example.org", 315 }, 316 map[string]string{ 317 "Access-Control-Allow-Origin": "", 318 "Access-Control-Allow-Credentials": "", 319 "Vary": "Origin", 320 }, 321 }, 322 } 323 for _, tt := range tests { 324 t.Run(tt.name, func(subT *testing.T) { 325 corsHandler := NewCORSHandler(tt.corsOptions, upstreamHandler) 326 327 req := httptest.NewRequest(http.MethodPost, "http://1.2.3.4/", nil) 328 for name, value := range tt.requestHeaders { 329 req.Header.Set(name, value) 330 } 331 332 rec := httptest.NewRecorder() 333 corsHandler.ServeHTTP(rec, req) 334 335 if !rec.Flushed { 336 rec.Flush() 337 } 338 339 res := rec.Result() 340 341 for name, expValue := range tt.expectedResponseHeaders { 342 value := res.Header.Get(name) 343 if value != expValue { 344 subT.Errorf("%s:\n\tExpected: %s %q, got: %s", tt.name, name, expValue, value) 345 } 346 } 347 348 if rec.Code != http.StatusOK { 349 subT.Errorf("Expected status %d, got: %d", http.StatusOK, rec.Code) 350 } else { 351 return // no error log for expected codes 352 } 353 }) 354 } 355 } 356 357 func TestProxy_ServeHTTP_CORS_PFC(t *testing.T) { 358 upstreamHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 359 rw.Header().Set("Content-Type", "text/plain") 360 rw.WriteHeader(http.StatusOK) 361 _, err := rw.Write([]byte("from upstream")) 362 if err != nil { 363 t.Error(err) 364 } 365 }) 366 367 methodAllowed := func(method string) bool { 368 return method == http.MethodPost 369 } 370 371 tests := []struct { 372 name string 373 corsOptions *CORSOptions 374 requestHeaders map[string]string 375 expectedResponseHeaders map[string]string 376 expectedVary []string 377 }{ 378 { 379 "specific origin, with ACRM", 380 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}, methodAllowed: methodAllowed}, 381 map[string]string{ 382 "Origin": "https://www.example.com", 383 "Access-Control-Request-Method": "POST", 384 }, 385 map[string]string{ 386 "Access-Control-Allow-Origin": "https://www.example.com", 387 "Access-Control-Allow-Methods": "POST", 388 "Access-Control-Allow-Headers": "", 389 "Access-Control-Allow-Credentials": "", 390 "Access-Control-Max-Age": "", 391 }, 392 []string{"Origin", "Access-Control-Request-Method"}, 393 }, 394 { 395 "specific origin, with ACRM, method not allowed", 396 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}, methodAllowed: methodAllowed}, 397 map[string]string{ 398 "Origin": "https://www.example.com", 399 "Access-Control-Request-Method": "PUT", 400 }, 401 map[string]string{ 402 "Access-Control-Allow-Origin": "https://www.example.com", 403 "Access-Control-Allow-Methods": "", 404 "Access-Control-Allow-Headers": "", 405 "Access-Control-Allow-Credentials": "", 406 "Access-Control-Max-Age": "", 407 }, 408 []string{"Origin", "Access-Control-Request-Method"}, 409 }, 410 { 411 "specific origin, with ACRH", 412 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}, methodAllowed: methodAllowed}, 413 map[string]string{ 414 "Origin": "https://www.example.com", 415 "Access-Control-Request-Headers": "X-Foo, X-Bar", 416 }, 417 map[string]string{ 418 "Access-Control-Allow-Origin": "https://www.example.com", 419 "Access-Control-Allow-Methods": "", 420 "Access-Control-Allow-Headers": "X-Foo, X-Bar", 421 "Access-Control-Allow-Credentials": "", 422 "Access-Control-Max-Age": "", 423 }, 424 []string{"Origin", "Access-Control-Request-Headers"}, 425 }, 426 { 427 "specific origin, with ACRM, ACRH", 428 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}, methodAllowed: methodAllowed}, 429 map[string]string{ 430 "Origin": "https://www.example.com", 431 "Access-Control-Request-Method": "POST", 432 "Access-Control-Request-Headers": "X-Foo, X-Bar", 433 }, 434 map[string]string{ 435 "Access-Control-Allow-Origin": "https://www.example.com", 436 "Access-Control-Allow-Methods": "POST", 437 "Access-Control-Allow-Headers": "X-Foo, X-Bar", 438 "Access-Control-Allow-Credentials": "", 439 "Access-Control-Max-Age": "", 440 }, 441 []string{"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"}, 442 }, 443 { 444 "specific origin, with ACRM, credentials", 445 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}, AllowCredentials: true, methodAllowed: methodAllowed}, 446 map[string]string{ 447 "Origin": "https://www.example.com", 448 "Access-Control-Request-Method": "POST", 449 }, 450 map[string]string{ 451 "Access-Control-Allow-Origin": "https://www.example.com", 452 "Access-Control-Allow-Methods": "POST", 453 "Access-Control-Allow-Headers": "", 454 "Access-Control-Allow-Credentials": "true", 455 "Access-Control-Max-Age": "", 456 }, 457 []string{"Origin", "Access-Control-Request-Method"}, 458 }, 459 { 460 "specific origin, with ACRM, max-age", 461 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}, MaxAge: "3600", methodAllowed: methodAllowed}, 462 map[string]string{ 463 "Origin": "https://www.example.com", 464 "Access-Control-Request-Method": "POST", 465 }, 466 map[string]string{ 467 "Access-Control-Allow-Origin": "https://www.example.com", 468 "Access-Control-Allow-Methods": "POST", 469 "Access-Control-Allow-Headers": "", 470 "Access-Control-Allow-Credentials": "", 471 "Access-Control-Max-Age": "3600", 472 }, 473 []string{"Origin", "Access-Control-Request-Method"}, 474 }, 475 { 476 "any origin, with ACRM", 477 &CORSOptions{AllowedOrigins: []string{"*"}, methodAllowed: methodAllowed}, 478 map[string]string{ 479 "Origin": "https://www.example.com", 480 "Access-Control-Request-Method": "POST", 481 }, 482 map[string]string{ 483 "Access-Control-Allow-Origin": "*", 484 "Access-Control-Allow-Methods": "POST", 485 "Access-Control-Allow-Headers": "", 486 "Access-Control-Allow-Credentials": "", 487 "Access-Control-Max-Age": "", 488 }, 489 []string{"Access-Control-Request-Method"}, 490 }, 491 { 492 "any origin, with ACRM, credentials", 493 &CORSOptions{AllowedOrigins: []string{"*"}, AllowCredentials: true, methodAllowed: methodAllowed}, 494 map[string]string{ 495 "Origin": "https://www.example.com", 496 "Access-Control-Request-Method": "POST", 497 }, 498 map[string]string{ 499 "Access-Control-Allow-Origin": "https://www.example.com", 500 "Access-Control-Allow-Methods": "POST", 501 "Access-Control-Allow-Headers": "", 502 "Access-Control-Allow-Credentials": "true", 503 "Access-Control-Max-Age": "", 504 }, 505 []string{"Origin", "Access-Control-Request-Method"}, 506 }, 507 { 508 "origin mismatch", 509 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}, methodAllowed: methodAllowed}, 510 map[string]string{ 511 "Origin": "https://www.example.org", 512 "Access-Control-Request-Method": "POST", 513 }, 514 map[string]string{ 515 "Access-Control-Allow-Origin": "", 516 "Access-Control-Allow-Methods": "", 517 "Access-Control-Allow-Headers": "", 518 "Access-Control-Allow-Credentials": "", 519 "Access-Control-Max-Age": "", 520 }, 521 []string{"Origin"}, 522 }, 523 { 524 "origin mismatch, credentials", 525 &CORSOptions{AllowedOrigins: []string{"https://www.example.com"}, AllowCredentials: true, methodAllowed: methodAllowed}, 526 map[string]string{ 527 "Origin": "https://www.example.org", 528 "Access-Control-Request-Method": "POST", 529 }, 530 map[string]string{ 531 "Access-Control-Allow-Origin": "", 532 "Access-Control-Allow-Methods": "", 533 "Access-Control-Allow-Headers": "", 534 "Access-Control-Allow-Credentials": "", 535 "Access-Control-Max-Age": "", 536 }, 537 []string{"Origin"}, 538 }, 539 } 540 for _, tt := range tests { 541 t.Run(tt.name, func(subT *testing.T) { 542 corsHandler := NewCORSHandler(tt.corsOptions, upstreamHandler) 543 544 req := httptest.NewRequest(http.MethodOptions, "http://1.2.3.4/", nil) 545 for name, value := range tt.requestHeaders { 546 req.Header.Set(name, value) 547 } 548 549 rec := httptest.NewRecorder() 550 551 corsHandler.ServeHTTP(rec, req) 552 553 if !rec.Flushed { 554 rec.Flush() 555 } 556 557 res := rec.Result() 558 559 tt.expectedResponseHeaders["Content-Type"] = "" 560 561 for name, expValue := range tt.expectedResponseHeaders { 562 value := res.Header.Get(name) 563 if value != expValue { 564 subT.Errorf("Expected %s %s, got: %s", name, expValue, value) 565 } 566 } 567 varyVals := res.Header.Values("Vary") 568 ve := false 569 if len(varyVals) != len(tt.expectedVary) { 570 ve = true 571 } else { 572 for i, ev := range tt.expectedVary { 573 if ev != varyVals[i] { 574 ve = true 575 break 576 } 577 } 578 } 579 if ve { 580 subT.Errorf("Vary mismatch, expected %s, got: %s", tt.expectedVary, varyVals) 581 } 582 583 if rec.Code != http.StatusNoContent { 584 subT.Errorf("Expected status %d, got: %d", http.StatusNoContent, rec.Code) 585 } else { 586 return // no error log for expected codes 587 } 588 }) 589 } 590 }