package app import ( "bytes" "context" "io" "net" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "galaxy/authsession/internal/config" "github.com/alicebob/miniredis/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" ) func TestNewRuntimeStartsAndStopsHTTPServers(t *testing.T) { t.Parallel() redisServer := miniredis.RunT(t) cfg := config.DefaultConfig() cfg.Redis.Addr = redisServer.Addr() cfg.PublicHTTP.Addr = mustFreeAddr(t) cfg.InternalHTTP.Addr = mustFreeAddr(t) runtime, err := NewRuntime(context.Background(), cfg, zap.NewNop(), nil) 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.App.Run(runCtx) }() require.Eventually(t, func() bool { response, err := http.Post( "http://"+cfg.PublicHTTP.Addr+"/api/v1/public/auth/send-email-code", "application/json", bytes.NewBufferString(`{"email":"pilot@example.com"}`), ) if err != nil { return false } defer response.Body.Close() _, _ = io.ReadAll(response.Body) return response.StatusCode == http.StatusOK }, 5*time.Second, 25*time.Millisecond) require.Eventually(t, func() bool { response, err := http.Get("http://" + cfg.InternalHTTP.Addr + "/api/v1/internal/sessions/missing") if err != nil { return false } defer response.Body.Close() _, _ = io.ReadAll(response.Body) return response.StatusCode == http.StatusNotFound }, 5*time.Second, 25*time.Millisecond) cancel() require.NoError(t, <-runErrCh) } func TestNewRuntimeUsesRESTUserDirectoryWhenConfigured(t *testing.T) { t.Parallel() redisServer := miniredis.RunT(t) userService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet && r.URL.Path == "/api/v1/internal/users/user-1/exists" { w.Header().Set("Content-Type", "application/json") _, _ = io.WriteString(w, `{"exists":true}`) return } http.NotFound(w, r) })) defer userService.Close() cfg := config.DefaultConfig() cfg.Redis.Addr = redisServer.Addr() cfg.PublicHTTP.Addr = mustFreeAddr(t) cfg.InternalHTTP.Addr = mustFreeAddr(t) cfg.UserService.Mode = "rest" cfg.UserService.BaseURL = userService.URL cfg.UserService.RequestTimeout = 250 * time.Millisecond runtime, err := NewRuntime(context.Background(), cfg, zap.NewNop(), nil) 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.App.Run(runCtx) }() require.Eventually(t, func() bool { response, err := http.Post( "http://"+cfg.InternalHTTP.Addr+"/api/v1/internal/users/user-1/sessions/revoke-all", "application/json", bytes.NewBufferString(`{"reason_code":"logout_all","actor":{"type":"system"}}`), ) if err != nil { return false } defer response.Body.Close() payload, err := io.ReadAll(response.Body) if err != nil { return false } return response.StatusCode == http.StatusOK && bytes.Contains(payload, []byte(`"outcome":"no_active_sessions"`)) && bytes.Contains(payload, []byte(`"user_id":"user-1"`)) }, 5*time.Second, 25*time.Millisecond) cancel() require.NoError(t, <-runErrCh) } func TestNewRuntimeUsesRESTMailSenderWhenConfigured(t *testing.T) { t.Parallel() redisServer := miniredis.RunT(t) var calls atomic.Int64 mailService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost && r.URL.Path == "/api/v1/internal/login-code-deliveries" { calls.Add(1) w.Header().Set("Content-Type", "application/json") _, _ = io.WriteString(w, `{"outcome":"suppressed"}`) return } http.NotFound(w, r) })) defer mailService.Close() cfg := config.DefaultConfig() cfg.Redis.Addr = redisServer.Addr() cfg.PublicHTTP.Addr = mustFreeAddr(t) cfg.InternalHTTP.Addr = mustFreeAddr(t) cfg.MailService.Mode = "rest" cfg.MailService.BaseURL = mailService.URL cfg.MailService.RequestTimeout = 250 * time.Millisecond runtime, err := NewRuntime(context.Background(), cfg, zap.NewNop(), nil) 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.App.Run(runCtx) }() require.Eventually(t, func() bool { response, err := http.Post( "http://"+cfg.PublicHTTP.Addr+"/api/v1/public/auth/send-email-code", "application/json", bytes.NewBufferString(`{"email":"pilot@example.com"}`), ) if err != nil { return false } defer response.Body.Close() payload, err := io.ReadAll(response.Body) if err != nil { return false } return response.StatusCode == http.StatusOK && bytes.Contains(payload, []byte(`"challenge_id":"`)) && calls.Load() == 1 }, 5*time.Second, 25*time.Millisecond) cancel() require.NoError(t, <-runErrCh) } 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() }