package authsession import ( "bytes" "context" "fmt" "io" "net" "os" "os/exec" "path/filepath" "runtime" "strings" "syscall" "testing" "time" "galaxy/authsession/internal/adapters/userservice" "galaxy/authsession/internal/domain/common" "galaxy/authsession/internal/domain/userresolution" "galaxy/authsession/internal/ports" "github.com/alicebob/miniredis/v2" ) func TestUserServiceRESTClientWorksAgainstRealUserServiceRuntime(t *testing.T) { redisServer := miniredis.RunT(t) internalAddr := freeTCPAddress(t) binaryPath := buildUserServiceBinary(t) process := startUserServiceProcess(t, binaryPath, map[string]string{ "USERSERVICE_INTERNAL_HTTP_ADDR": internalAddr, "USERSERVICE_REDIS_ADDR": redisServer.Addr(), }) waitForTCP(t, process, internalAddr) client, err := userservice.NewRESTClient(userservice.Config{ BaseURL: "http://" + internalAddr, RequestTimeout: 500 * time.Millisecond, }) if err != nil { t.Fatalf("NewRESTClient() error = %v, want nil", err) } t.Cleanup(func() { _ = client.Close() }) creatableEmail := common.Email("pilot@example.com") resolution, err := client.ResolveByEmail(context.Background(), creatableEmail) if err != nil { t.Fatalf("ResolveByEmail(creatable) error = %v, want nil", err) } if got, want := resolution.Kind, userresolution.KindCreatable; got != want { t.Fatalf("ResolveByEmail(creatable).Kind = %q, want %q", got, want) } created, err := client.EnsureUserByEmail(context.Background(), ports.EnsureUserInput{ Email: creatableEmail, RegistrationContext: &ports.RegistrationContext{ PreferredLanguage: "en", TimeZone: "Europe/Kaliningrad", }, }) if err != nil { t.Fatalf("EnsureUserByEmail(created) error = %v, want nil", err) } if got, want := created.Outcome, ports.EnsureUserOutcomeCreated; got != want { t.Fatalf("EnsureUserByEmail(created).Outcome = %q, want %q", got, want) } if created.UserID.IsZero() { t.Fatalf("EnsureUserByEmail(created).UserID = zero, want non-zero") } existing, err := client.ResolveByEmail(context.Background(), creatableEmail) if err != nil { t.Fatalf("ResolveByEmail(existing) error = %v, want nil", err) } if got, want := existing.Kind, userresolution.KindExisting; got != want { t.Fatalf("ResolveByEmail(existing).Kind = %q, want %q", got, want) } if got, want := existing.UserID, created.UserID; got != want { t.Fatalf("ResolveByEmail(existing).UserID = %q, want %q", got, want) } exists, err := client.ExistsByUserID(context.Background(), created.UserID) if err != nil { t.Fatalf("ExistsByUserID(existing) error = %v, want nil", err) } if !exists { t.Fatalf("ExistsByUserID(existing) = false, want true") } blocked, err := client.BlockByUserID(context.Background(), ports.BlockUserByIDInput{ UserID: created.UserID, ReasonCode: userresolution.BlockReasonCode("policy_blocked"), }) if err != nil { t.Fatalf("BlockByUserID() error = %v, want nil", err) } if got, want := blocked.Outcome, ports.BlockUserOutcomeBlocked; got != want { t.Fatalf("BlockByUserID().Outcome = %q, want %q", got, want) } if got, want := blocked.UserID, created.UserID; got != want { t.Fatalf("BlockByUserID().UserID = %q, want %q", got, want) } repeated, err := client.BlockByEmail(context.Background(), ports.BlockUserByEmailInput{ Email: creatableEmail, ReasonCode: userresolution.BlockReasonCode("policy_blocked"), }) if err != nil { t.Fatalf("BlockByEmail(repeated) error = %v, want nil", err) } if got, want := repeated.Outcome, ports.BlockUserOutcomeAlreadyBlocked; got != want { t.Fatalf("BlockByEmail(repeated).Outcome = %q, want %q", got, want) } if got, want := repeated.UserID, created.UserID; got != want { t.Fatalf("BlockByEmail(repeated).UserID = %q, want %q", got, want) } blockedResolution, err := client.ResolveByEmail(context.Background(), creatableEmail) if err != nil { t.Fatalf("ResolveByEmail(blocked) error = %v, want nil", err) } if got, want := blockedResolution.Kind, userresolution.KindBlocked; got != want { t.Fatalf("ResolveByEmail(blocked).Kind = %q, want %q", got, want) } if got, want := blockedResolution.BlockReasonCode, userresolution.BlockReasonCode("policy_blocked"); got != want { t.Fatalf("ResolveByEmail(blocked).BlockReasonCode = %q, want %q", got, want) } } type userServiceProcess struct { cmd *exec.Cmd doneCh chan struct{} logs bytes.Buffer } func startUserServiceProcess(t *testing.T, binaryPath string, env map[string]string) *userServiceProcess { t.Helper() cmd := exec.Command(binaryPath) cmd.Env = mergeEnvironment(os.Environ(), env) process := &userServiceProcess{ cmd: cmd, doneCh: make(chan struct{}), } cmd.Stdout = &process.logs cmd.Stderr = &process.logs if err := cmd.Start(); err != nil { t.Fatalf("start user service process: %v", err) } go func() { _ = cmd.Wait() close(process.doneCh) }() t.Cleanup(func() { stopUserServiceProcess(t, process) if t.Failed() { t.Logf("userservice logs:\n%s", process.logs.String()) } }) return process } func stopUserServiceProcess(t *testing.T, process *userServiceProcess) { t.Helper() if process == nil || process.cmd == nil || process.cmd.Process == nil { return } select { case <-process.doneCh: return default: } _ = process.cmd.Process.Signal(syscall.SIGTERM) select { case <-process.doneCh: case <-time.After(5 * time.Second): _ = process.cmd.Process.Kill() <-process.doneCh } } func waitForTCP(t *testing.T, process *userServiceProcess, address string) { t.Helper() deadline := time.Now().Add(10 * time.Second) for time.Now().Before(deadline) { select { case <-process.doneCh: t.Fatalf("userservice exited before %s became reachable\n%s", address, process.logs.String()) default: } conn, err := net.DialTimeout("tcp", address, 100*time.Millisecond) if err == nil { _ = conn.Close() return } time.Sleep(25 * time.Millisecond) } t.Fatalf("userservice did not become reachable at %s\n%s", address, process.logs.String()) } func freeTCPAddress(t *testing.T) string { t.Helper() listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("reserve free TCP address: %v", err) } defer listener.Close() return listener.Addr().String() } func buildUserServiceBinary(t *testing.T) string { t.Helper() outputPath := filepath.Join(t.TempDir(), "userservice") cmd := exec.Command("go", "build", "-o", outputPath, "./user/cmd/userservice") cmd.Dir = repositoryRoot(t) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("build userservice binary: %v\n%s", err, output) } return outputPath } func repositoryRoot(t *testing.T) string { t.Helper() _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("resolve repository root: runtime caller unavailable") } return filepath.Clean(filepath.Join(filepath.Dir(file), "..")) } func mergeEnvironment(base []string, overrides map[string]string) []string { values := make(map[string]string, len(base)+len(overrides)) for _, entry := range base { name, value, ok := strings.Cut(entry, "=") if ok { values[name] = value } } for name, value := range overrides { values[name] = value } merged := make([]string, 0, len(values)) for name, value := range values { merged = append(merged, fmt.Sprintf("%s=%s", name, value)) } return merged } var _ io.Writer = (*bytes.Buffer)(nil)