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  }