check / check (push) Waiting to run
USERHOST, VERSION, ADMIN, INFO, TIME, KILL and WALLOPS work on the HTTP API and on the IRC listener. The USERHOST reply, the VERSION and INFO text, KILL and WALLOPS are each one service function that both transports call. User mode +w selects who receives WALLOPS. MODE on your own nick queries or changes your user modes through one parser on both transports: the whole mode string is checked before anything is stored, and both flags are stored in one transaction. KILL closes the victim's IRC connection after sending it KILL and ERROR. The sessions table gains is_wallops in 001_initial.sql, as the pre-1.0 rule in REPO_POLICIES.md asks. Model: opus-5-5
101 lines
2.2 KiB
Go
101 lines
2.2 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"
|
|
)
|
|
|
|
// newKillVictim returns a Conn for the nick victim whose
|
|
// socket is one end of a net.Pipe, and the other end, from
|
|
// which the test reads what the victim is sent. A write to
|
|
// the pipe blocks until the other end reads.
|
|
func newKillVictim(t *testing.T) (*ircserver.Conn, net.Conn) {
|
|
t.Helper()
|
|
|
|
serverSide, clientSide := net.Pipe()
|
|
|
|
t.Cleanup(func() { _ = clientSide.Close() })
|
|
|
|
log := slog.New(slog.NewTextHandler(
|
|
os.Stderr,
|
|
&slog.HandlerOptions{Level: slog.LevelError},
|
|
))
|
|
cfg := &config.Config{ServerName: testServerName}
|
|
|
|
return ircserver.NewTestConn(
|
|
log, cfg, serverSide, "victim",
|
|
), clientSide
|
|
}
|
|
|
|
// TestDisconnectDoesNotBlockOnUnresponsiveVictim checks
|
|
// that Disconnect returns although the victim never reads,
|
|
// so a KILL cannot stall the operator who sent it.
|
|
func TestDisconnectDoesNotBlockOnUnresponsiveVictim(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
victim, _ := newKillVictim(t)
|
|
returned := make(chan struct{})
|
|
|
|
go func() {
|
|
victim.Disconnect("killed by oper")
|
|
close(returned)
|
|
}()
|
|
|
|
select {
|
|
case <-returned:
|
|
case <-time.After(testTimeout):
|
|
t.Fatal("Disconnect waited for the victim to read")
|
|
}
|
|
}
|
|
|
|
// TestDisconnectNotifiesAndClosesVictim checks that a
|
|
// victim that reads is sent KILL and ERROR, and that its
|
|
// connection is then closed.
|
|
func TestDisconnectNotifiesAndClosesVictim(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
victim, clientSide := newKillVictim(t)
|
|
received := make(chan []string, 1)
|
|
|
|
go func() {
|
|
var lines []string
|
|
|
|
scanner := bufio.NewScanner(clientSide)
|
|
for scanner.Scan() {
|
|
lines = append(lines, scanner.Text())
|
|
}
|
|
|
|
received <- lines
|
|
}()
|
|
|
|
victim.Disconnect("killed by oper")
|
|
|
|
var lines []string
|
|
|
|
select {
|
|
case lines = <-received:
|
|
case <-time.After(testTimeout):
|
|
t.Fatal("the victim's connection was not closed")
|
|
}
|
|
|
|
joined := strings.Join(lines, "\n")
|
|
|
|
if !strings.Contains(joined, "KILL victim :killed by oper") {
|
|
t.Errorf("missing KILL line, got: %q", joined)
|
|
}
|
|
|
|
if !strings.Contains(joined, "ERROR :Closing Link:") {
|
|
t.Errorf("missing ERROR line, got: %q", joined)
|
|
}
|
|
}
|