177 lines
6.1 KiB
Go
177 lines
6.1 KiB
Go
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",
|
|
// Identity-bucket limits sit on top of the class limits and are
|
|
// keyed by the request identity (email for send-email-code,
|
|
// challenge_id for confirm-email-code). The defaults are
|
|
// purposely tight in production (3 sends per email per window);
|
|
// happy-path scenarios that re-issue codes for the same email
|
|
// would otherwise trip the limiter mid-test.
|
|
"GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS": "10000",
|
|
"GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST": "1000",
|
|
"GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS": "10000",
|
|
"GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_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)
|
|
}
|