diff --git a/test/e2e/testdata/cors.yaml b/test/e2e/testdata/cors.yaml new file mode 100644 index 00000000000..1967fc96288 --- /dev/null +++ b/test/e2e/testdata/cors.yaml @@ -0,0 +1,49 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: cors-example + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-with-cors + namespace: gateway-conformance-infra + cors: + allowOrigins: + - type: Exact + value: "https://www.foo.com" + - type: Exact + value: "https://www.bar.com" + - type: RegularExpression + value: "https://[a-zA-Z0-9]+.foobar.com" + allowMethods: + - GET + - POST + - PUT + - PATCH + - DELETE + - OPTIONS + allowHeaders: + - "x-header-1" + - "x-header-2" + exposeHeaders: + - "x-header-3" + - "x-header-4" +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-with-cors + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: PathPrefix + value: /cors + backendRefs: + - name: infra-backend-v1 + port: 8080 diff --git a/test/e2e/tests/cors.go b/test/e2e/tests/cors.go new file mode 100644 index 00000000000..4b4e54bcc8f --- /dev/null +++ b/test/e2e/tests/cors.go @@ -0,0 +1,128 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build e2e +// +build e2e + +package tests + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, CorsTest) +} + +var CorsTest = suite.ConformanceTest{ + ShortName: "Cors", + Description: "Resource with CORS enabled", + Manifests: []string{"testdata/cors.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("should enable cors with Allow Origin Exact", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-with-cors", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/cors", + Headers: map[string]string{ + "Origin": "https://www.foo.com", + }, + }, + Response: http.Response{ + StatusCode: 200, + Headers: map[string]string{ + "access-control-allow-origin": "https://www.foo.com", + "access-control-expose-headers": "x-header-3, x-header-4", + }, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + + t.Run("should enable cors with Allow Origin Regex", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-with-cors", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/cors", + Method: "OPTIONS", + Headers: map[string]string{ + "Origin": "https://anydomain.foobar.com", + }, + }, + Response: http.Response{ + StatusCode: 200, + Headers: map[string]string{ + "access-control-allow-origin": "https://anydomain.foobar.com", + "access-control-expose-headers": "x-header-3, x-header-4", + }, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + + t.Run("should not contain cors headers when Origin not registered", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-with-cors", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/cors", + Headers: map[string]string{ + "Origin": "https://unknown.foo.com", + }, + }, + Response: http.Response{ + AbsentHeaders: []string{"access-control-allow-origin", "access-control-expose-headers"}, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) + }, +}