feat: backend service
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
package testenv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
tcnetwork "github.com/testcontainers/testcontainers-go/network"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
)
|
||||
|
||||
// GatewayContainer wraps a running galaxy/gateway:integration
|
||||
// container.
|
||||
type GatewayContainer struct {
|
||||
Container testcontainers.Container
|
||||
HTTPHost string
|
||||
HTTPPort int
|
||||
HTTPURL string
|
||||
GRPCHost string
|
||||
GRPCPort int
|
||||
GRPCAddr string
|
||||
|
||||
// ResponseSignerPublic is the Ed25519 public key the gateway uses
|
||||
// to sign responses and push events. Tests verify signatures
|
||||
// against this value.
|
||||
ResponseSignerPublic ed25519.PublicKey
|
||||
}
|
||||
|
||||
// GatewayOptions tunes a gateway container before it boots.
|
||||
type GatewayOptions struct {
|
||||
NetworkAlias string
|
||||
NetworkName string
|
||||
BackendHTTPURL string
|
||||
BackendGRPCURL string
|
||||
RedisAddr string
|
||||
GatewayClientID string
|
||||
Extra map[string]string
|
||||
}
|
||||
|
||||
// StartGateway boots galaxy/gateway:integration with the supplied
|
||||
// options.
|
||||
func StartGateway(t *testing.T, opts GatewayOptions) *GatewayContainer {
|
||||
t.Helper()
|
||||
EnsureGatewayImage(t)
|
||||
|
||||
if opts.NetworkAlias == "" {
|
||||
opts.NetworkAlias = "gateway"
|
||||
}
|
||||
if opts.GatewayClientID == "" {
|
||||
opts.GatewayClientID = "integration-gateway"
|
||||
}
|
||||
|
||||
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("generate ed25519 key: %v", err)
|
||||
}
|
||||
keyDER, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal ed25519 key: %v", err)
|
||||
}
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
|
||||
keyPath := filepath.Join(t.TempDir(), "gateway-signer.pem")
|
||||
if err := writeFile(keyPath, keyPEM); err != nil {
|
||||
t.Fatalf("write signer key: %v", err)
|
||||
}
|
||||
|
||||
containerKey := "/etc/galaxy/gateway-signer.pem"
|
||||
env := map[string]string{
|
||||
"GATEWAY_PUBLIC_HTTP_ADDR": ":8080",
|
||||
"GATEWAY_AUTHENTICATED_GRPC_ADDR": ":9090",
|
||||
"GATEWAY_LOG_LEVEL": "debug",
|
||||
"GATEWAY_REDIS_MASTER_ADDR": opts.RedisAddr,
|
||||
"GATEWAY_REDIS_PASSWORD": RedisIntegrationPassword,
|
||||
"GATEWAY_BACKEND_HTTP_URL": opts.BackendHTTPURL,
|
||||
"GATEWAY_BACKEND_GRPC_PUSH_URL": opts.BackendGRPCURL,
|
||||
"GATEWAY_BACKEND_GATEWAY_CLIENT_ID": opts.GatewayClientID,
|
||||
"GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH": containerKey,
|
||||
// Loosen anti-abuse so happy-path scenarios aren't rate-limited.
|
||||
// Negative-path edge tests tighten these per-test.
|
||||
"GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_REQUESTS": "10000",
|
||||
"GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_BURST": "1000",
|
||||
"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_REQUESTS": "10000",
|
||||
"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_BURST": "1000",
|
||||
"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_REQUESTS": "10000",
|
||||
"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_BURST": "1000",
|
||||
"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_REQUESTS": "10000",
|
||||
"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_BURST": "1000",
|
||||
"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_REQUESTS": "10000",
|
||||
"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_BURST": "1000",
|
||||
}
|
||||
for k, v := range opts.Extra {
|
||||
env[k] = v
|
||||
}
|
||||
|
||||
req := testcontainers.ContainerRequest{
|
||||
Image: GatewayImage,
|
||||
ExposedPorts: []string{"8080/tcp", "9090/tcp"},
|
||||
Env: env,
|
||||
WaitingFor: wait.ForHTTP("/healthz").
|
||||
WithPort("8080/tcp").
|
||||
WithStartupTimeout(60 * time.Second),
|
||||
Files: []testcontainers.ContainerFile{
|
||||
{
|
||||
HostFilePath: keyPath,
|
||||
ContainerFilePath: containerKey,
|
||||
// 0o444 so the distroless `nonroot` user (uid 65532)
|
||||
// inside the gateway image can read the integration
|
||||
// signer key. The key is ephemeral and never leaves
|
||||
// the test process, so widening the mode is safe.
|
||||
FileMode: 0o444,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gcr := &testcontainers.GenericContainerRequest{ContainerRequest: req}
|
||||
if opts.NetworkName != "" {
|
||||
_ = tcnetwork.WithNetwork([]string{opts.NetworkAlias}, &testcontainers.DockerNetwork{Name: opts.NetworkName}).Customize(gcr)
|
||||
}
|
||||
gcr.Started = true
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
defer cancel()
|
||||
container, err := testcontainers.GenericContainer(ctx, *gcr)
|
||||
if err != nil {
|
||||
t.Fatalf("start gateway container: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := testcontainers.TerminateContainer(container); err != nil {
|
||||
t.Logf("terminate gateway: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
host, err := container.Host(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("gateway host: %v", err)
|
||||
}
|
||||
port, err := container.MappedPort(ctx, "8080/tcp")
|
||||
if err != nil {
|
||||
t.Fatalf("gateway port: %v", err)
|
||||
}
|
||||
grpcPort, err := container.MappedPort(ctx, "9090/tcp")
|
||||
if err != nil {
|
||||
t.Fatalf("gateway grpc port: %v", err)
|
||||
}
|
||||
return &GatewayContainer{
|
||||
Container: container,
|
||||
HTTPHost: host,
|
||||
HTTPPort: int(port.Num()),
|
||||
HTTPURL: fmt.Sprintf("http://%s:%d", host, port.Num()),
|
||||
GRPCHost: host,
|
||||
GRPCPort: int(grpcPort.Num()),
|
||||
GRPCAddr: fmt.Sprintf("%s:%d", host, grpcPort.Num()),
|
||||
ResponseSignerPublic: pub,
|
||||
}
|
||||
}
|
||||
|
||||
func writeFile(path string, content []byte) error {
|
||||
return writeFileFn(path, content)
|
||||
}
|
||||
Reference in New Issue
Block a user