mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-11 03:13:45 +00:00
cmd/noitu-server had no tests at all: every env* helper is pure, and config parsing is exactly where a production misconfiguration hides. waitForGamesToFinish narrows its *wsapi.Server parameter to the single method it calls, so the poll-then-check timing can be driven by a fake without a live server behind it.
200 lines
5.8 KiB
Go
200 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestEnvFallsBackWhenUnsetOrBlank covers env's whole contract: an unset
|
|
// variable and a blank one are the same "nothing configured" signal, since an
|
|
// operator's empty override should not silently defeat a default.
|
|
func TestEnvFallsBackWhenUnsetOrBlank(t *testing.T) {
|
|
const key = "NOITU_TEST_ENV_STRING"
|
|
|
|
if got := env(key, "fallback"); got != "fallback" {
|
|
t.Errorf("unset: env = %q, want the fallback", got)
|
|
}
|
|
|
|
t.Setenv(key, " ")
|
|
if got := env(key, "fallback"); got != "fallback" {
|
|
t.Errorf("blank: env = %q, want the fallback", got)
|
|
}
|
|
|
|
t.Setenv(key, " configured ")
|
|
if got := env(key, "fallback"); got != "configured" {
|
|
t.Errorf("set: env = %q, want the trimmed value", got)
|
|
}
|
|
}
|
|
|
|
// TestEnvIntRejectsInvalidAndNegative: a production misconfiguration here
|
|
// must fall back loudly rather than silently becoming zero or panicking.
|
|
func TestEnvIntRejectsInvalidAndNegative(t *testing.T) {
|
|
const key = "NOITU_TEST_ENV_INT"
|
|
|
|
if got := envInt(key, 7); got != 7 {
|
|
t.Errorf("unset: envInt = %d, want the fallback", got)
|
|
}
|
|
|
|
tests := []struct {
|
|
name, value string
|
|
want int
|
|
}{
|
|
{"valid", "42", 42},
|
|
{"zero is a real value", "0", 0},
|
|
{"negative rejected", "-1", 7},
|
|
{"not a number rejected", "many", 7},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Setenv(key, tc.value)
|
|
if got := envInt(key, 7); got != tc.want {
|
|
t.Errorf("envInt(%q) = %d, want %d", tc.value, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEnvDurationRejectsInvalidAndZero: unlike envNonNegDuration, a plain
|
|
// timing variable rejects zero along with garbage — every one of today's
|
|
// durations (turn limit, grace) has to be positive to mean anything.
|
|
func TestEnvDurationRejectsInvalidAndZero(t *testing.T) {
|
|
const key = "NOITU_TEST_ENV_DURATION"
|
|
|
|
if got := envDuration(key, time.Second); got != time.Second {
|
|
t.Errorf("unset: envDuration = %v, want the fallback", got)
|
|
}
|
|
|
|
tests := []struct {
|
|
name, value string
|
|
want time.Duration
|
|
}{
|
|
{"valid", "30s", 30 * time.Second},
|
|
{"zero rejected", "0s", time.Second},
|
|
{"negative rejected", "-5s", time.Second},
|
|
{"not a duration rejected", "soon", time.Second},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Setenv(key, tc.value)
|
|
if got := envDuration(key, time.Second); got != tc.want {
|
|
t.Errorf("envDuration(%q) = %v, want %v", tc.value, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEnvNonNegDurationAcceptsZero is the one difference from envDuration:
|
|
// NOITU_DRAIN_TIMEOUT=0 is a deliberate "do not wait", not a typo, and must
|
|
// not be rejected the way a zero turn limit would be.
|
|
func TestEnvNonNegDurationAcceptsZero(t *testing.T) {
|
|
const key = "NOITU_TEST_ENV_NONNEG_DURATION"
|
|
|
|
tests := []struct {
|
|
name, value string
|
|
want time.Duration
|
|
}{
|
|
{"valid", "10s", 10 * time.Second},
|
|
{"zero accepted", "0s", 0},
|
|
{"negative rejected", "-1s", 5 * time.Second},
|
|
{"not a duration rejected", "soon", 5 * time.Second},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Setenv(key, tc.value)
|
|
if got := envNonNegDuration(key, 5*time.Second); got != tc.want {
|
|
t.Errorf("envNonNegDuration(%q) = %v, want %v", tc.value, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEnvListSplitsAndTrims covers the shape NOITU_ALLOWED_ORIGINS and
|
|
// NOITU_TRUSTED_PROXIES both rely on: comma-separated, whitespace around each
|
|
// entry ignored, and empty entries dropped rather than becoming a stray "".
|
|
func TestEnvListSplitsAndTrims(t *testing.T) {
|
|
const key = "NOITU_TEST_ENV_LIST"
|
|
|
|
if got := envList(key); got != nil {
|
|
t.Errorf("unset: envList = %v, want nil", got)
|
|
}
|
|
|
|
t.Setenv(key, " a , b ,, c")
|
|
got := envList(key)
|
|
want := []string{"a", "b", "c"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("envList = %v, want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("envList[%d] = %q, want %q", i, got[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// fakeGameCounter answers LiveGameCount from a value a test can change while
|
|
// the drain loop is polling it, so the loop can be driven without a real
|
|
// server or rooms behind it.
|
|
type fakeGameCounter struct {
|
|
mu sync.Mutex
|
|
live int64
|
|
}
|
|
|
|
func (f *fakeGameCounter) set(n int64) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.live = n
|
|
}
|
|
|
|
func (f *fakeGameCounter) LiveGameCount() int64 {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.live
|
|
}
|
|
|
|
// TestWaitForGamesToFinishReturnsAsSoonAsTheCountReachesZero: the loop must
|
|
// not sit out its whole timeout once the last game has actually ended.
|
|
func TestWaitForGamesToFinishReturnsAsSoonAsTheCountReachesZero(t *testing.T) {
|
|
counter := &fakeGameCounter{live: 1}
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
waitForGamesToFinish(counter, time.Minute)
|
|
close(done)
|
|
}()
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
counter.set(0)
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("waitForGamesToFinish did not return once the count reached zero")
|
|
}
|
|
}
|
|
|
|
// TestWaitForGamesToFinishRespectsTheTimeout: a game that never ends must not
|
|
// hang the drain forever — it has to give up once its budget is spent.
|
|
func TestWaitForGamesToFinishRespectsTheTimeout(t *testing.T) {
|
|
counter := &fakeGameCounter{live: 1}
|
|
|
|
start := time.Now()
|
|
waitForGamesToFinish(counter, 250*time.Millisecond)
|
|
if elapsed := time.Since(start); elapsed < 250*time.Millisecond {
|
|
t.Errorf("returned after %v, want at least the 250ms timeout", elapsed)
|
|
}
|
|
}
|
|
|
|
// TestWaitForGamesToFinishReturnsImmediatelyOnZeroTimeout: a zero or negative
|
|
// timeout is today's original behaviour — rooms are torn down with whatever
|
|
// they were doing rather than waited on at all.
|
|
func TestWaitForGamesToFinishReturnsImmediatelyOnZeroTimeout(t *testing.T) {
|
|
counter := &fakeGameCounter{live: 1}
|
|
|
|
start := time.Now()
|
|
waitForGamesToFinish(counter, 0)
|
|
if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
|
|
t.Errorf("a zero timeout took %v, want an immediate return", elapsed)
|
|
}
|
|
}
|