Files
galaxy-game/authsession/user_service_real_runtime_compatibility_test.go
T
2026-04-10 19:05:02 +02:00

274 lines
7.3 KiB
Go

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)