Add the tier 3 utility IRC commands (closes #87)
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
This commit is contained in:
2026-10-08 01:13:36 +00:00
parent b119b094f5
commit a7faabb68f
19 changed files with 2498 additions and 127 deletions
+78 -5
View File
@@ -36,9 +36,39 @@ type testEnv struct {
srv *ircserver.Server
}
// testServerName is the SERVER_NAME of newTestEnv.
const testServerName = "test.irc"
func newTestEnv(t *testing.T) *testEnv {
t.Helper()
return newTestEnvWithConfig(t, &config.Config{
ServerName: testServerName,
MOTD: "Welcome to test IRC",
})
}
// newTestEnvWithOper is newTestEnv with the operator name
// testoper and password testpass.
func newTestEnvWithOper(t *testing.T) *testEnv {
t.Helper()
return newTestEnvWithConfig(t, &config.Config{
ServerName: testServerName,
MOTD: "Welcome to test IRC",
OperName: "testoper",
OperPassword: "testpass",
})
}
// newTestEnvWithConfig starts an IRC server with cfg on a
// fresh database.
func newTestEnvWithConfig(
t *testing.T,
cfg *config.Config,
) *testEnv {
t.Helper()
dsn := fmt.Sprintf(
"file:%s?mode=memory&cache=shared&_journal_mode=WAL",
t.Name(),
@@ -67,11 +97,6 @@ func newTestEnv(t *testing.T) *testEnv {
brk := broker.New()
cfg := &config.Config{ //nolint:exhaustruct
ServerName: "test.irc",
MOTD: "Welcome to test IRC",
}
var listenConfig net.ListenConfig
listener, err := listenConfig.Listen(t.Context(), "tcp", "127.0.0.1:0")
@@ -215,6 +240,33 @@ func (tc *testClient) register(nick string) []string {
})
}
// readUntilClosed returns the lines received until the
// server closes the connection, and fails the test if it is
// still open after testTimeout.
func (tc *testClient) readUntilClosed() []string {
tc.t.Helper()
_ = tc.conn.SetReadDeadline(
time.Now().Add(testTimeout),
)
var lines []string
for tc.scanner.Scan() {
lines = append(lines, tc.scanner.Text())
}
err := tc.scanner.Err()
if err != nil {
tc.t.Fatalf(
"connection not closed: %v (lines: %v)",
err, lines,
)
}
return lines
}
// assertContains checks that at least one line matches the
// given substring.
func assertContains(
@@ -233,6 +285,27 @@ func assertContains(
t.Errorf("did not find %q in output: %s", substr, description)
}
// assertNotContains checks that no line matches the given
// substring.
func assertNotContains(
t *testing.T,
lines []string,
substr, description string,
) {
t.Helper()
for _, line := range lines {
if strings.Contains(line, substr) {
t.Errorf(
"unexpectedly found %q in output: %s",
substr, description,
)
return
}
}
}
// joinAndDrain joins a channel and reads until
// RPL_ENDOFNAMES.
func (tc *testClient) joinAndDrain(channel string) {