mirror of
https://github.com/go-gost/gost.git
synced 2026-10-08 20:15:46 +00:00
Add e2e regression test for http.failCodes selector bug — FIFO selector + failCodes=429 on first node proves node-429 is excluded after first 429 response and all subsequent requests go to node-good.
144 lines
3.9 KiB
Go
144 lines
3.9 KiB
Go
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/moby/moby/client"
|
|
"github.com/stretchr/testify/suite"
|
|
"github.com/testcontainers/testcontainers-go"
|
|
"github.com/testcontainers/testcontainers-go/wait"
|
|
)
|
|
|
|
type FailCodesSuite struct {
|
|
suite.Suite
|
|
ctx context.Context
|
|
echoC testcontainers.Container
|
|
echoIP string
|
|
statusC testcontainers.Container
|
|
}
|
|
|
|
func (s *FailCodesSuite) SetupSuite() {
|
|
s.ctx = context.Background()
|
|
|
|
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
|
|
s.Require().NoError(err)
|
|
s.echoC = echoC
|
|
|
|
echoIP, err := echoC.ContainerIP(s.ctx)
|
|
s.Require().NoError(err)
|
|
s.echoIP = echoIP
|
|
|
|
statusC, err := RunStatusBackendContainer(s.ctx, SharedNetworkName, 429)
|
|
s.Require().NoError(err)
|
|
s.statusC = statusC
|
|
}
|
|
|
|
func (s *FailCodesSuite) TearDownSuite() {
|
|
if s.echoC != nil {
|
|
s.echoC.Terminate(s.ctx)
|
|
}
|
|
if s.statusC != nil {
|
|
s.statusC.Terminate(s.ctx)
|
|
}
|
|
}
|
|
|
|
func (s *FailCodesSuite) sendRawHTTP(gostC testcontainers.Container, host, port string) string {
|
|
req := fmt.Sprintf(
|
|
"GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n",
|
|
s.echoIP,
|
|
)
|
|
encoded := base64.StdEncoding.EncodeToString([]byte(req))
|
|
cmd := []string{"sh", "-c",
|
|
fmt.Sprintf("echo %s | base64 -d | nc -w 5 %s %s", encoded, host, port)}
|
|
_, out, _ := gostC.Exec(s.ctx, cmd)
|
|
b, _ := io.ReadAll(out)
|
|
return string(b)
|
|
}
|
|
|
|
// TestFailCodesConvergence: reverse proxy (tcp handler + sniffing) with FIFO.
|
|
// node-429 is first. After the first 429 response, failCodes marks it (Count=1).
|
|
// FailFilter (maxFails=1) excludes it. All subsequent requests go to node-good.
|
|
func (s *FailCodesSuite) TestFailCodesConvergence() {
|
|
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
|
|
"testdata/failcodes/failcodes.yaml",
|
|
nil,
|
|
"8080/tcp",
|
|
)
|
|
s.Require().NoError(err)
|
|
defer func() {
|
|
DumpLogs(s.T(), s.ctx, "gost-failcodes", gostC)
|
|
gostC.Terminate(s.ctx)
|
|
}()
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
const totalRequests = 20
|
|
var successCount, failCount int
|
|
|
|
for range totalRequests {
|
|
body := s.sendRawHTTP(gostC, "127.0.0.1", "8080")
|
|
|
|
if strings.Contains(body, "hello-gost") {
|
|
successCount++
|
|
s.T().Logf(" ✓ 200 (node-good)")
|
|
} else if strings.Contains(body, "status-429") {
|
|
failCount++
|
|
s.T().Logf(" ✗ 429 (node-429)")
|
|
} else {
|
|
s.T().Logf(" ? %s", strings.TrimSpace(body)[:80])
|
|
}
|
|
}
|
|
|
|
s.T().Logf("Results: %d successes, %d failures out of %d requests",
|
|
successCount, failCount, totalRequests)
|
|
|
|
// After the first 429 marks node-429, FailFilter excludes it for 10s.
|
|
// At most 1-2 requests may fail before the marker is set.
|
|
s.Require().LessOrEqual(failCount, 2,
|
|
"failCodes: node-429 must be excluded after first 429 response. "+
|
|
"Got %d failures out of %d requests", failCount, totalRequests)
|
|
}
|
|
|
|
func TestFailCodesSuite(t *testing.T) {
|
|
suite.Run(t, new(FailCodesSuite))
|
|
}
|
|
|
|
func RunStatusBackendContainer(ctx context.Context, networkName string, statusCode int) (testcontainers.Container, error) {
|
|
req := testcontainers.ContainerRequest{
|
|
FromDockerfile: testcontainers.FromDockerfile{
|
|
Context: ".",
|
|
Dockerfile: "Dockerfile",
|
|
Repo: "gost-e2e",
|
|
Tag: "latest",
|
|
KeepImage: true,
|
|
BuildOptionsModifier: func(opts *client.ImageBuildOptions) {
|
|
opts.NetworkMode = "host"
|
|
},
|
|
},
|
|
Networks: []string{networkName},
|
|
NetworkAliases: map[string][]string{
|
|
networkName: {"status-backend"},
|
|
},
|
|
Files: []testcontainers.ContainerFile{
|
|
{HostFilePath: "scripts/http_status_backend.py", ContainerFilePath: "/scripts/http_status_backend.py", FileMode: 0644},
|
|
},
|
|
ExposedPorts: []string{"5680/tcp"},
|
|
Cmd: []string{
|
|
"python3", "/scripts/http_status_backend.py",
|
|
fmt.Sprintf("%d", statusCode), "5680",
|
|
},
|
|
WaitingFor: wait.ForExposedPort(),
|
|
}
|
|
|
|
return testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
|
ContainerRequest: req,
|
|
Started: true,
|
|
})
|
|
}
|