diff --git a/tests/e2e/routing_test.go b/tests/e2e/routing_test.go new file mode 100644 index 0000000..7c641f8 --- /dev/null +++ b/tests/e2e/routing_test.go @@ -0,0 +1,81 @@ +package e2e + +import ( + "context" + "io" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" +) + +// RoutingSuite verifies node-level routing matchers. A forward proxy's only +// chain node relays to an upstream proxy, but the node carries a matcher +// Host(`tcp-echo`) so it is only eligible for requests whose Host matches. A +// request to tcp-echo:5678 is matched, relayed upstream, and reaches the echo +// server ("hello-gost"); a request to a non-matching host is excluded (no +// eligible node) and never reaches the echo server. +type RoutingSuite struct { + suite.Suite + ctx context.Context + echoC testcontainers.Container + proxyC testcontainers.Container + upstreamC testcontainers.Container +} + +func (s *RoutingSuite) SetupSuite() { + s.ctx = context.Background() + + echoC, err := RunEchoContainer(s.ctx, SharedNetworkName) + s.Require().NoError(err) + s.echoC = echoC + + proxyC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName, + "testdata/routing/server.yaml", []string{"gost-proxy"}, []string{"8080/tcp"}) + s.Require().NoError(err) + s.proxyC = proxyC + + upstreamC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName, + "testdata/routing/upstream.yaml", []string{"gost-upstream"}, []string{"8081/tcp"}) + s.Require().NoError(err) + s.upstreamC = upstreamC +} + +func (s *RoutingSuite) TearDownSuite() { + for _, c := range []testcontainers.Container{s.proxyC, s.upstreamC, s.echoC} { + if c != nil { + c.Terminate(s.ctx) + } + } +} + +// proxyRequest sends an HTTP GET to target through the matcher-gated proxy and +// returns the response body. +func (s *RoutingSuite) proxyRequest(target string) string { + cmd := []string{ + "curl", "-s", + "-x", "http://gost-proxy:8080", + target, + } + _, out, err := s.echoC.Exec(s.ctx, cmd) + s.Require().NoError(err) + body, err := io.ReadAll(out) + s.Require().NoError(err) + return string(body) +} + +// TestMatchedHost verifies that a request whose Host matches the node matcher is +// relayed upstream and reaches the echo server. +func (s *RoutingSuite) TestMatchedHost() { + s.Require().Contains(s.proxyRequest("http://tcp-echo:5678/"), "hello-gost") +} + +// TestUnmatchedHost verifies that a request whose Host does not match the node +// matcher is excluded (no eligible node) and never reaches the echo server. +func (s *RoutingSuite) TestUnmatchedHost() { + s.Require().NotContains(s.proxyRequest("http://other.local:5678/"), "hello-gost") +} + +func TestRoutingSuite(t *testing.T) { + suite.Run(t, new(RoutingSuite)) +} diff --git a/tests/e2e/testdata/routing/server.yaml b/tests/e2e/testdata/routing/server.yaml new file mode 100644 index 0000000..f782e18 --- /dev/null +++ b/tests/e2e/testdata/routing/server.yaml @@ -0,0 +1,27 @@ +# Forward proxy gated by a node-level routing matcher. The only chain node is a +# relay to an upstream proxy, but it is only eligible when the request Host +# matches Host(`tcp-echo`). A request to tcp-echo:5678 matches, is relayed +# upstream, and reaches the echo server ("hello-gost"); a request to any other +# host is excluded (no eligible node) and never reaches the echo server. +services: + - name: proxy + addr: ":8080" + handler: + type: http + chain: chain-0 + listener: + type: tcp + +chains: + - name: chain-0 + hops: + - name: hop-0 + nodes: + - name: node-0 + addr: gost-upstream:8081 + matcher: + rule: 'Host(`tcp-echo`)' + connector: + type: http + dialer: + type: tcp diff --git a/tests/e2e/testdata/routing/upstream.yaml b/tests/e2e/testdata/routing/upstream.yaml new file mode 100644 index 0000000..4c739c6 --- /dev/null +++ b/tests/e2e/testdata/routing/upstream.yaml @@ -0,0 +1,9 @@ +# Upstream relay target for the routing matcher test: a plain forward proxy +# that reaches the echo server directly. +services: + - name: proxy + addr: ":8081" + handler: + type: http + listener: + type: tcp