PING a quiet IRC listener client before closing it (closes #155)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
serve closed a connection after five minutes with nothing read and never sent a PING, so a quiet registered client lost its session. It now sends PING :<server> after pingInterval with nothing read, and closes the connection when nothing is read within pongDeadline after that. Every line read starts both waits again. readTimeout is removed. Unregistered connections are sent the PING too, so a PONG before registration is now ignored instead of answered with ERR_NOTREGISTERED. Model: opus-5-5
This commit is contained in:
@@ -13,10 +13,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/fx/fxtest"
|
||||
"sneak.berlin/go/neoirc/internal/broker"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
"sneak.berlin/go/neoirc/internal/db"
|
||||
"sneak.berlin/go/neoirc/internal/globals"
|
||||
"sneak.berlin/go/neoirc/internal/ircserver"
|
||||
"sneak.berlin/go/neoirc/internal/logger"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
@@ -70,6 +73,22 @@ func newTestEnvWithConfig(
|
||||
) *testEnv {
|
||||
t.Helper()
|
||||
|
||||
return newTestEnvWithPingTimes(
|
||||
t, cfg, ircserver.PingInterval, ircserver.PongDeadline,
|
||||
)
|
||||
}
|
||||
|
||||
// newTestEnvWithPingTimes is newTestEnvWithConfig, except
|
||||
// that the server sends a connection a PING after interval
|
||||
// with nothing read, and closes it when nothing is read
|
||||
// within deadline after that.
|
||||
func newTestEnvWithPingTimes(
|
||||
t *testing.T,
|
||||
cfg *config.Config,
|
||||
interval, deadline time.Duration,
|
||||
) *testEnv {
|
||||
t.Helper()
|
||||
|
||||
// A random name, so that no other test environment, not
|
||||
// even an earlier run of the same test, can share it.
|
||||
dsn := fmt.Sprintf(
|
||||
@@ -126,7 +145,9 @@ func newTestEnvWithConfig(
|
||||
&slog.HandlerOptions{Level: slog.LevelError}, //nolint:exhaustruct
|
||||
))
|
||||
|
||||
srv := ircserver.NewTestServer(log, cfg, database, brk)
|
||||
srv := ircserver.NewTestServer(
|
||||
log, cfg, database, brk, interval, deadline,
|
||||
)
|
||||
|
||||
err = srv.Start(addr)
|
||||
if err != nil {
|
||||
@@ -174,6 +195,34 @@ func (env *testEnv) dial(t *testing.T) *testClient {
|
||||
}
|
||||
}
|
||||
|
||||
// The ping interval and PONG deadline for
|
||||
// newTestEnvWithShortPings. They differ, so that a test can
|
||||
// tell them apart; the PONG deadline is how long a test
|
||||
// client has to answer a PING.
|
||||
const (
|
||||
testPingInterval = time.Second
|
||||
testPongDeadline = 300 * time.Millisecond
|
||||
)
|
||||
|
||||
// newTestEnvWithShortPings starts an IRC server that uses
|
||||
// testPingInterval and testPongDeadline in place of
|
||||
// pingInterval and pongDeadline.
|
||||
func newTestEnvWithShortPings(t *testing.T) *testEnv {
|
||||
t.Helper()
|
||||
|
||||
return newTestEnvWithPingTimes(
|
||||
t,
|
||||
&config.Config{ServerName: testServerName},
|
||||
testPingInterval, testPongDeadline,
|
||||
)
|
||||
}
|
||||
|
||||
// isServerPing reports whether line is the PING the test
|
||||
// server sends a quiet client.
|
||||
func isServerPing(line string) bool {
|
||||
return line == "PING :"+testServerName
|
||||
}
|
||||
|
||||
// testClient wraps a raw TCP connection with helpers.
|
||||
type testClient struct {
|
||||
t *testing.T
|
||||
@@ -272,6 +321,24 @@ func (tc *testClient) readUntilClosed() []string {
|
||||
return lines
|
||||
}
|
||||
|
||||
// readServerPing reads until the PING the test server sends a
|
||||
// quiet client, and fails the test if it came sooner than
|
||||
// testPingInterval after lastSent, a time just before the
|
||||
// client sent its last line.
|
||||
func (tc *testClient) readServerPing(lastSent time.Time) {
|
||||
tc.t.Helper()
|
||||
|
||||
tc.readUntil(isServerPing)
|
||||
|
||||
waited := time.Since(lastSent)
|
||||
if waited < testPingInterval {
|
||||
tc.t.Errorf(
|
||||
"PING came %v after the client's last line, want at least %v",
|
||||
waited, testPingInterval,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// assertContains checks that at least one line matches the
|
||||
// given substring.
|
||||
func assertContains(
|
||||
@@ -388,6 +455,129 @@ func TestPingPong(t *testing.T) {
|
||||
assertContains(t, lines, "PONG", "PONG response")
|
||||
}
|
||||
|
||||
// TestQuietClientAnsweringPingStaysConnected checks that a
|
||||
// registered client that sends nothing is sent a PING once
|
||||
// the ping interval has passed since its last line, and stays
|
||||
// connected as long as it answers each one, whether with a
|
||||
// PONG or with another line.
|
||||
func TestQuietClientAnsweringPingStaysConnected(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
answer string
|
||||
}{
|
||||
{name: "PONG", answer: "PONG :" + testServerName},
|
||||
{name: "PING of its own", answer: "PING :answer"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnvWithShortPings(t)
|
||||
client := env.dial(t)
|
||||
|
||||
lastSent := time.Now()
|
||||
|
||||
client.register("answers")
|
||||
|
||||
// The second PING comes later than a client that
|
||||
// answers none stays connected.
|
||||
for range 2 {
|
||||
client.readServerPing(lastSent)
|
||||
|
||||
lastSent = time.Now()
|
||||
|
||||
client.send(tt.answer)
|
||||
}
|
||||
|
||||
client.sendAndExpect("PING :still here", "still here")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestQuietClientNotAnsweringPingIsClosed checks that a
|
||||
// registered client that does not answer the PING is
|
||||
// disconnected once the PONG deadline has passed after it.
|
||||
func TestQuietClientNotAnsweringPingIsClosed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// latest leaves room for a loaded host, but is sooner than
|
||||
// a read deadline of two ping intervals would close it.
|
||||
const (
|
||||
earliest = testPingInterval + testPongDeadline
|
||||
latest = 2 * testPingInterval
|
||||
)
|
||||
|
||||
env := newTestEnvWithShortPings(t)
|
||||
client := env.dial(t)
|
||||
|
||||
lastSent := time.Now()
|
||||
|
||||
client.register("silent")
|
||||
client.readServerPing(lastSent)
|
||||
client.readUntilClosed()
|
||||
|
||||
waited := time.Since(lastSent)
|
||||
if waited < earliest || waited >= latest {
|
||||
t.Errorf(
|
||||
"closed %v after the client's last line, want %v to %v",
|
||||
waited, earliest, latest,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnregisteredClientAnswersPing checks that a PONG before
|
||||
// registration, the answer to the PING unregistered clients
|
||||
// are sent too, gets no ERR_NOTREGISTERED.
|
||||
func TestUnregisteredClientAnswersPing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnvWithShortPings(t)
|
||||
client := env.dial(t)
|
||||
|
||||
client.readUntil(isServerPing)
|
||||
client.send("PONG :" + testServerName)
|
||||
|
||||
lines := client.sendAndExpect("PING :after", "PONG")
|
||||
assertNotContains(t, lines, " 451 ", "ERR_NOTREGISTERED")
|
||||
}
|
||||
|
||||
// TestNewPingTimes checks that a server made by New sends a
|
||||
// PING after 90 seconds with nothing read, and closes the
|
||||
// connection 30 seconds after that, as README.md states.
|
||||
func TestNewPingTimes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
wantInterval = 90 * time.Second
|
||||
wantDeadline = 30 * time.Second
|
||||
)
|
||||
|
||||
log, err := logger.New(nil, logger.Params{
|
||||
Globals: &globals.Globals{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// With no IRC_LISTEN_ADDR, New starts no listener and
|
||||
// uses nothing else from its params.
|
||||
srv := ircserver.New(fxtest.NewLifecycle(t), ircserver.Params{
|
||||
Logger: log,
|
||||
Config: &config.Config{},
|
||||
})
|
||||
|
||||
interval, deadline := srv.PingTimes()
|
||||
if interval != wantInterval || deadline != wantDeadline {
|
||||
t.Errorf(
|
||||
"PING after %v and close %v later, want %v and %v",
|
||||
interval, deadline, wantInterval, wantDeadline,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIdleCleanupKeepsActiveUser checks that a user who
|
||||
// sends commands is kept by the idle cleanup, and a user who
|
||||
// sends nothing is not.
|
||||
|
||||
Reference in New Issue
Block a user