feat: user service
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user