feat: game lobby service
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"galaxy/lobby/internal/config"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeComponent struct {
|
||||
runErr error
|
||||
shutdownErr error
|
||||
runHook func(context.Context) error
|
||||
shutdownHook func(context.Context) error
|
||||
runCount atomic.Int32
|
||||
downCount atomic.Int32
|
||||
blockForCtx bool
|
||||
}
|
||||
|
||||
func (component *fakeComponent) Run(ctx context.Context) error {
|
||||
component.runCount.Add(1)
|
||||
if component.runHook != nil {
|
||||
return component.runHook(ctx)
|
||||
}
|
||||
if component.blockForCtx {
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
return component.runErr
|
||||
}
|
||||
|
||||
func (component *fakeComponent) Shutdown(ctx context.Context) error {
|
||||
component.downCount.Add(1)
|
||||
if component.shutdownHook != nil {
|
||||
return component.shutdownHook(ctx)
|
||||
}
|
||||
|
||||
return component.shutdownErr
|
||||
}
|
||||
|
||||
func newCfg() config.Config {
|
||||
return config.Config{ShutdownTimeout: time.Second}
|
||||
}
|
||||
|
||||
func TestAppValidateRejectsNonPositiveTimeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New(config.Config{}, &fakeComponent{blockForCtx: true})
|
||||
err := app.Run(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "shutdown timeout must be positive")
|
||||
}
|
||||
|
||||
func TestAppValidateRejectsNilComponent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New(newCfg(), nil)
|
||||
err := app.Run(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "component 0 is nil")
|
||||
}
|
||||
|
||||
func TestAppRunCancelledByContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
component := &fakeComponent{blockForCtx: true}
|
||||
app := New(newCfg(), component)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- app.Run(ctx)
|
||||
}()
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
require.NoError(t, err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("app did not stop after cancellation")
|
||||
}
|
||||
|
||||
assert.Equal(t, int32(1), component.runCount.Load())
|
||||
assert.Equal(t, int32(1), component.downCount.Load())
|
||||
}
|
||||
|
||||
func TestAppRunPropagatesComponentError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
failing := &fakeComponent{runErr: errors.New("boom")}
|
||||
blocking := &fakeComponent{blockForCtx: true}
|
||||
app := New(newCfg(), failing, blocking)
|
||||
|
||||
err := app.Run(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "boom")
|
||||
assert.Equal(t, int32(1), blocking.downCount.Load())
|
||||
}
|
||||
|
||||
func TestAppRunEarlyCleanExit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
short := &fakeComponent{} // returns immediately without error
|
||||
blocking := &fakeComponent{blockForCtx: true}
|
||||
app := New(newCfg(), short, blocking)
|
||||
|
||||
err := app.Run(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "exited without error before shutdown")
|
||||
}
|
||||
|
||||
func TestAppRunShutdownCollectsErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
component := &fakeComponent{
|
||||
blockForCtx: true,
|
||||
shutdownErr: errors.New("shutdown-boom"),
|
||||
}
|
||||
app := New(newCfg(), component)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- app.Run(ctx)
|
||||
}()
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
cancel()
|
||||
|
||||
err := <-errCh
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "shutdown-boom")
|
||||
}
|
||||
|
||||
func TestAppRunNoComponentsBlocksUntilCancel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New(newCfg())
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- app.Run(ctx)
|
||||
}()
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
require.NoError(t, err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("app did not exit after cancel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRunNilContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New(newCfg(), &fakeComponent{blockForCtx: true})
|
||||
err := app.Run(nil) //nolint:staticcheck // test exercises the nil-context guard.
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "nil context")
|
||||
}
|
||||
Reference in New Issue
Block a user