Some checks failed
check / check (push) Failing after 16s
db: TestSetSessionUserModesIsAtomic installs a trigger that rejects the is_oper UPDATE after the is_wallops UPDATE has run, proving the '+w-o' partial-failure the old independent-UPDATE loop exhibited is gone. ircserver: TestDisconnectDoesNotBlockOnUnresponsiveVictim drives Disconnect against a net.Pipe victim that never reads and requires the call to return promptly; verified to fail against the synchronous implementation. Its counterpart asserts the notification is still delivered and the socket still closed.
129 lines
2.9 KiB
Go
129 lines
2.9 KiB
Go
package ircserver_test
|
|
|
|
import (
|
|
"bufio"
|
|
"log/slog"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/neoirc/internal/config"
|
|
"sneak.berlin/go/neoirc/internal/ircserver"
|
|
)
|
|
|
|
// disconnectBudget is how long Disconnect is allowed to
|
|
// take when the victim never reads. It is far below the
|
|
// 30s writeTimeout that the old synchronous implementation
|
|
// would have burned on each of its two writes.
|
|
const disconnectBudget = 2 * time.Second
|
|
|
|
// TestDisconnectDoesNotBlockOnUnresponsiveVictim proves
|
|
// that an operator KILL cannot be stalled by its target.
|
|
// The victim's socket is a net.Pipe, so every write blocks
|
|
// until the peer reads and the peer here never does. The
|
|
// old implementation performed both notification writes on
|
|
// the killer's goroutine, which wedged the operator's own
|
|
// serve() loop on the IRC path and the API request on the
|
|
// HTTP path for as long as the victim cared to stay silent.
|
|
func TestDisconnectDoesNotBlockOnUnresponsiveVictim(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
serverSide, clientSide := net.Pipe()
|
|
|
|
t.Cleanup(func() {
|
|
_ = clientSide.Close()
|
|
})
|
|
|
|
log := slog.New(slog.NewTextHandler(
|
|
os.Stderr,
|
|
&slog.HandlerOptions{Level: slog.LevelError}, //nolint:exhaustruct
|
|
))
|
|
cfg := &config.Config{ //nolint:exhaustruct
|
|
ServerName: "test.irc",
|
|
}
|
|
|
|
victim := ircserver.NewTestConn(
|
|
log, cfg, serverSide, "victim",
|
|
)
|
|
|
|
returned := make(chan struct{})
|
|
|
|
go func() {
|
|
victim.Disconnect("killed by oper")
|
|
close(returned)
|
|
}()
|
|
|
|
select {
|
|
case <-returned:
|
|
case <-time.After(disconnectBudget):
|
|
t.Fatal(
|
|
"Disconnect blocked on the victim's socket; " +
|
|
"the killer must not be held hostage",
|
|
)
|
|
}
|
|
}
|
|
|
|
// TestDisconnectNotifiesAndClosesVictim is the other half
|
|
// of the contract: moving the notification off the killer's
|
|
// goroutine must not lose it. A victim that does read gets
|
|
// both the KILL and the ERROR line, and then its socket is
|
|
// closed so its read loop unblocks.
|
|
func TestDisconnectNotifiesAndClosesVictim(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
serverSide, clientSide := net.Pipe()
|
|
|
|
t.Cleanup(func() {
|
|
_ = clientSide.Close()
|
|
})
|
|
|
|
log := slog.New(slog.NewTextHandler(
|
|
os.Stderr,
|
|
&slog.HandlerOptions{Level: slog.LevelError}, //nolint:exhaustruct
|
|
))
|
|
cfg := &config.Config{ //nolint:exhaustruct
|
|
ServerName: "test.irc",
|
|
}
|
|
|
|
victim := ircserver.NewTestConn(
|
|
log, cfg, serverSide, "victim",
|
|
)
|
|
|
|
lines := make(chan []string, 1)
|
|
|
|
go func() {
|
|
var got []string
|
|
|
|
scanner := bufio.NewScanner(clientSide)
|
|
for scanner.Scan() {
|
|
got = append(got, scanner.Text())
|
|
}
|
|
|
|
lines <- got
|
|
}()
|
|
|
|
victim.Disconnect("killed by oper")
|
|
|
|
var got []string
|
|
|
|
select {
|
|
case got = <-lines:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("victim socket was never closed")
|
|
}
|
|
|
|
joined := strings.Join(got, "\n")
|
|
|
|
if !strings.Contains(joined, "KILL victim") {
|
|
t.Errorf("missing KILL line, got: %q", joined)
|
|
}
|
|
|
|
if !strings.Contains(joined, "ERROR :Closing Link:") {
|
|
t.Errorf("missing ERROR line, got: %q", joined)
|
|
}
|
|
}
|