Files
gost/tests/e2e/failcodes_test.go
ginuerzh 555dacd244 chore: bump x to v0.15.0 (hopGroup, failCodes fix)
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.
2026-07-27 22:13:34 +08:00

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,
})
}