package app import ( "context" "io" "log/slog" "net" "net/http" "os" "testing" "time" "galaxy/lobby/internal/api/internalhttp" "galaxy/lobby/internal/api/publichttp" "galaxy/lobby/internal/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" testcontainers "github.com/testcontainers/testcontainers-go" rediscontainer "github.com/testcontainers/testcontainers-go/modules/redis" ) const ( realRuntimeSmokeEnv = "LOBBY_REAL_RUNTIME_SMOKE" realRuntimeRedisImage = "redis:7" ) // TestRealRuntimeCompatibility boots the full Runtime against a real Redis // container, verifies that both HTTP listeners serve /healthz and /readyz, // and asserts graceful shutdown on context cancellation. The test is skipped // unless LOBBY_REAL_RUNTIME_SMOKE=1 because it depends on Docker. func TestRealRuntimeCompatibility(t *testing.T) { if os.Getenv(realRuntimeSmokeEnv) != "1" { t.Skipf("set %s=1 to run the real runtime smoke suite", realRuntimeSmokeEnv) } ctx := context.Background() redisContainer, err := rediscontainer.Run(ctx, realRuntimeRedisImage) require.NoError(t, err) testcontainers.CleanupContainer(t, redisContainer) redisAddr, err := redisContainer.Endpoint(ctx, "") require.NoError(t, err) cfg := config.DefaultConfig() cfg.Redis.Addr = redisAddr cfg.UserService.BaseURL = "http://127.0.0.1:1" cfg.GM.BaseURL = "http://127.0.0.1:1" cfg.PublicHTTP.Addr = mustFreeAddr(t) cfg.InternalHTTP.Addr = mustFreeAddr(t) cfg.ShutdownTimeout = 2 * time.Second cfg.Telemetry.TracesExporter = "none" cfg.Telemetry.MetricsExporter = "none" runtime, err := NewRuntime(context.Background(), cfg, testLogger()) require.NoError(t, err) defer func() { require.NoError(t, runtime.Close()) }() runCtx, cancel := context.WithCancel(context.Background()) defer cancel() runErrCh := make(chan error, 1) go func() { runErrCh <- runtime.Run(runCtx) }() client := newTestHTTPClient(t) waitForRuntimeReady(t, client, cfg.PublicHTTP.Addr, publichttp.ReadyzPath) waitForRuntimeReady(t, client, cfg.InternalHTTP.Addr, internalhttp.ReadyzPath) assertHTTPStatus(t, client, "http://"+cfg.PublicHTTP.Addr+publichttp.HealthzPath, http.StatusOK) assertHTTPStatus(t, client, "http://"+cfg.PublicHTTP.Addr+publichttp.ReadyzPath, http.StatusOK) assertHTTPStatus(t, client, "http://"+cfg.InternalHTTP.Addr+internalhttp.HealthzPath, http.StatusOK) assertHTTPStatus(t, client, "http://"+cfg.InternalHTTP.Addr+internalhttp.ReadyzPath, http.StatusOK) cancel() waitForRunResult(t, runErrCh, cfg.ShutdownTimeout+2*time.Second) } func testLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } func newTestHTTPClient(t *testing.T) *http.Client { t.Helper() transport := &http.Transport{DisableKeepAlives: true} t.Cleanup(transport.CloseIdleConnections) return &http.Client{ Timeout: 500 * time.Millisecond, Transport: transport, } } func waitForRuntimeReady(t *testing.T, client *http.Client, addr string, path string) { t.Helper() require.Eventually(t, func() bool { request, err := http.NewRequest(http.MethodGet, "http://"+addr+path, nil) if err != nil { return false } response, err := client.Do(request) if err != nil { return false } defer response.Body.Close() _, _ = io.Copy(io.Discard, response.Body) return response.StatusCode == http.StatusOK }, 5*time.Second, 25*time.Millisecond, "lobby runtime did not become reachable on %s", addr) } func waitForRunResult(t *testing.T, runErrCh <-chan error, waitTimeout time.Duration) { t.Helper() var err error require.Eventually(t, func() bool { select { case err = <-runErrCh: return true default: return false } }, waitTimeout, 10*time.Millisecond, "lobby runtime did not stop") require.NoError(t, err) } func assertHTTPStatus(t *testing.T, client *http.Client, target string, want int) { t.Helper() request, err := http.NewRequest(http.MethodGet, target, nil) require.NoError(t, err) response, err := client.Do(request) require.NoError(t, err) defer response.Body.Close() _, _ = io.Copy(io.Discard, response.Body) require.Equal(t, want, response.StatusCode) } func mustFreeAddr(t *testing.T) string { t.Helper() listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) defer func() { assert.NoError(t, listener.Close()) }() return listener.Addr().String() }