check / check (push) Waiting to run
README.md rules that nicks are case-sensitive, but the IRC listener's relay and user MODE on both transports compared nicks ignoring case: an IRC listener user alice never saw a PRIVMSG, NOTICE, JOIN, PART, NICK or QUIT from Alice, and MODE Alice from alice was taken as her own nick. These checks now use ==. Tests cover alice receiving each of these from Alice, MODE Alice from alice getting 502 on both transports, and Alice registering while alice exists. README.md drops the text that described the old behaviour. Model: opus-5-5
338 lines
8.6 KiB
Go
338 lines
8.6 KiB
Go
//nolint:paralleltest // global viper, as in api_test.go
|
|
package handlers_test
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/neoirc/pkg/irc"
|
|
)
|
|
|
|
// Each test starts one server and reuses it for all its
|
|
// checks, and makes as few requests as it can: servers,
|
|
// sessions and requests are what makes this package's tests
|
|
// slow.
|
|
|
|
// sendAndPoll sends cmd from the session token and returns
|
|
// the messages after lastID that the session has been sent,
|
|
// and the ID of the last of them.
|
|
func sendAndPoll(
|
|
tserver *testServer,
|
|
token string,
|
|
lastID int64,
|
|
cmd map[string]any,
|
|
) ([]map[string]any, int64) {
|
|
tserver.t.Helper()
|
|
|
|
tserver.sendCommand(token, cmd)
|
|
|
|
return tserver.pollMessages(token, lastID)
|
|
}
|
|
|
|
// newOperSession creates a session for nick, makes it a
|
|
// server operator, and returns its token and the ID of the
|
|
// last message it has been sent.
|
|
func newOperSession(
|
|
tserver *testServer, nick string,
|
|
) (string, int64) {
|
|
tserver.t.Helper()
|
|
|
|
token := tserver.createSession(nick)
|
|
|
|
_, lastID := sendAndPoll(tserver, token, 0, map[string]any{
|
|
commandKey: irc.CmdOper,
|
|
bodyKey: []string{testOperName, testOperPassword},
|
|
})
|
|
|
|
return token, lastID
|
|
}
|
|
|
|
// numericBody returns the first body line of the message in
|
|
// msgs with the given numeric, and fails the test if there
|
|
// is none.
|
|
func numericBody(
|
|
t *testing.T, msgs []map[string]any, numeric string,
|
|
) string {
|
|
t.Helper()
|
|
|
|
msg := findNumericWithParams(msgs, numeric)
|
|
if msg == nil {
|
|
t.Fatalf("expected numeric %s, got %v", numeric, msgs)
|
|
}
|
|
|
|
lines, _ := msg[bodyKey].([]any)
|
|
if len(lines) == 0 {
|
|
return ""
|
|
}
|
|
|
|
line, _ := lines[0].(string)
|
|
|
|
return line
|
|
}
|
|
|
|
func TestUserhost(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
|
|
token := tserver.createSession("alice")
|
|
tserver.createSession("bob")
|
|
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
msgs, lastID := sendAndPoll(tserver, token, lastID, map[string]any{
|
|
commandKey: irc.CmdUserhost,
|
|
bodyKey: []string{"alice", "bob"},
|
|
})
|
|
|
|
body := numericBody(t, msgs, "302")
|
|
if !strings.HasPrefix(body, "alice=+alice@") ||
|
|
!strings.Contains(body, " bob=+bob@") {
|
|
t.Errorf("expected alice and bob, got %q", body)
|
|
}
|
|
|
|
msgs, lastID = sendAndPoll(tserver, token, lastID, map[string]any{
|
|
commandKey: irc.CmdUserhost,
|
|
bodyKey: []string{"ghost"},
|
|
})
|
|
|
|
body = numericBody(t, msgs, "302")
|
|
if body != "" {
|
|
t.Errorf("expected an empty reply for ghost, got %q", body)
|
|
}
|
|
|
|
msgs, _ = sendAndPoll(tserver, token, lastID, map[string]any{
|
|
commandKey: irc.CmdUserhost,
|
|
})
|
|
if !findNumeric(msgs, "461") {
|
|
t.Errorf("expected ERR_NEEDMOREPARAMS (461), got %v", msgs)
|
|
}
|
|
}
|
|
|
|
func TestVersionAdminInfoTime(t *testing.T) {
|
|
tserver := newTestServer(t)
|
|
|
|
token := tserver.createSession("frank")
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
msgs, lastID := sendAndPoll(tserver, token, lastID, map[string]any{
|
|
commandKey: irc.CmdVersion,
|
|
})
|
|
|
|
params := getNumericParams(findNumericWithParams(msgs, "351"))
|
|
if len(params) == 0 || params[0] != "neoirc-test-test." {
|
|
t.Errorf("expected RPL_VERSION neoirc-test-test., got %v", msgs)
|
|
}
|
|
|
|
for _, check := range []struct {
|
|
command string
|
|
numerics []string
|
|
}{
|
|
{irc.CmdAdmin, []string{"256", "257", "258", "259"}},
|
|
{irc.CmdInfo, []string{"371", "374"}},
|
|
{irc.CmdTime, []string{"391"}},
|
|
} {
|
|
msgs, lastID = sendAndPoll(tserver, token, lastID, map[string]any{
|
|
commandKey: check.command,
|
|
})
|
|
|
|
for _, numeric := range check.numerics {
|
|
if !findNumeric(msgs, numeric) {
|
|
t.Errorf(
|
|
"%s: expected %s, got %v",
|
|
check.command, numeric, msgs,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKill(t *testing.T) {
|
|
tserver := newTestServerWithOper(t)
|
|
|
|
victimToken := tserver.createSession("victim")
|
|
observerToken := tserver.createSession("observer")
|
|
notOperToken := tserver.createSession("notoper")
|
|
operToken, operLastID := newOperSession(tserver, "killer")
|
|
|
|
for _, token := range []string{victimToken, observerToken} {
|
|
tserver.sendCommand(token, map[string]any{
|
|
commandKey: joinCmd,
|
|
toKey: "#killtest",
|
|
})
|
|
}
|
|
|
|
_, notOperLastID := tserver.pollMessages(notOperToken, 0)
|
|
|
|
msgs, _ := sendAndPoll(tserver, notOperToken, notOperLastID, map[string]any{
|
|
commandKey: irc.CmdKill,
|
|
bodyKey: []string{"victim"},
|
|
})
|
|
if !findNumeric(msgs, "481") {
|
|
t.Errorf("expected ERR_NOPRIVILEGES (481), got %v", msgs)
|
|
}
|
|
|
|
for _, check := range []struct {
|
|
numeric string
|
|
body []string
|
|
}{
|
|
{"461", []string{}},
|
|
{"401", []string{"ghost"}},
|
|
{"483", []string{"killer"}},
|
|
} {
|
|
msgs, operLastID = sendAndPoll(tserver, operToken, operLastID, map[string]any{
|
|
commandKey: irc.CmdKill,
|
|
bodyKey: check.body,
|
|
})
|
|
if !findNumeric(msgs, check.numeric) {
|
|
t.Errorf(
|
|
"KILL %v: expected %s, got %v",
|
|
check.body, check.numeric, msgs,
|
|
)
|
|
}
|
|
}
|
|
|
|
_, observerLastID := tserver.pollMessages(observerToken, 0)
|
|
|
|
status, result := tserver.sendCommand(operToken, map[string]any{
|
|
commandKey: irc.CmdKill,
|
|
bodyKey: []string{"victim", "go away"},
|
|
})
|
|
if status != http.StatusOK || result[statusKey] != "ok" {
|
|
t.Fatalf("expected 200 and status ok, got %d: %v", status, result)
|
|
}
|
|
|
|
msgs, _ = tserver.pollMessages(observerToken, observerLastID)
|
|
if !findMessage(msgs, irc.CmdQuit, "victim") {
|
|
t.Errorf("expected the observer to see QUIT, got %v", msgs)
|
|
}
|
|
|
|
status, _ = tserver.getState(victimToken)
|
|
if status != http.StatusUnauthorized {
|
|
t.Errorf("expected the victim's session gone, got %d", status)
|
|
}
|
|
}
|
|
|
|
func TestWallops(t *testing.T) {
|
|
tserver := newTestServerWithOper(t)
|
|
|
|
receiverToken := tserver.createSession("receiver")
|
|
plainToken := tserver.createSession("plain")
|
|
operToken, operLastID := newOperSession(tserver, "walloper")
|
|
|
|
_, receiverLastID := sendAndPoll(tserver, receiverToken, 0, map[string]any{
|
|
commandKey: modeCmd,
|
|
toKey: "receiver",
|
|
bodyKey: []string{"+w"},
|
|
})
|
|
_, plainLastID := tserver.pollMessages(plainToken, 0)
|
|
|
|
msgs, plainLastID := sendAndPoll(tserver, plainToken, plainLastID, map[string]any{
|
|
commandKey: irc.CmdWallops,
|
|
bodyKey: []string{"not allowed"},
|
|
})
|
|
if !findNumeric(msgs, "481") {
|
|
t.Errorf("expected ERR_NOPRIVILEGES (481), got %v", msgs)
|
|
}
|
|
|
|
msgs, _ = sendAndPoll(tserver, operToken, operLastID, map[string]any{
|
|
commandKey: irc.CmdWallops,
|
|
})
|
|
if !findNumeric(msgs, "461") {
|
|
t.Errorf("expected ERR_NEEDMOREPARAMS (461), got %v", msgs)
|
|
}
|
|
|
|
tserver.sendCommand(operToken, map[string]any{
|
|
commandKey: irc.CmdWallops,
|
|
bodyKey: []string{"server going down"},
|
|
})
|
|
|
|
msgs, _ = tserver.pollMessages(receiverToken, receiverLastID)
|
|
if !findMessage(msgs, irc.CmdWallops, "walloper") {
|
|
t.Errorf("expected WALLOPS for the +w user, got %v", msgs)
|
|
}
|
|
|
|
msgs, _ = tserver.pollMessages(plainToken, plainLastID)
|
|
if findMessage(msgs, irc.CmdWallops, "walloper") {
|
|
t.Errorf("WALLOPS reached a user without +w: %v", msgs)
|
|
}
|
|
}
|
|
|
|
func TestUserMode(t *testing.T) {
|
|
const (
|
|
nick = "alice"
|
|
other = "other"
|
|
)
|
|
|
|
tserver := newTestServerWithOper(t)
|
|
|
|
token := tserver.createSession(nick)
|
|
otherToken := tserver.createSession(other)
|
|
operToken, operLastID := newOperSession(tserver, "deoper")
|
|
|
|
// mode sends MODE for target from token, with modeStr
|
|
// unless it is empty, and returns the session's next
|
|
// messages.
|
|
mode := func(
|
|
token string, lastID int64, target, modeStr string,
|
|
) ([]map[string]any, int64) {
|
|
cmd := map[string]any{commandKey: modeCmd, toKey: target}
|
|
if modeStr != "" {
|
|
cmd[bodyKey] = []string{modeStr}
|
|
}
|
|
|
|
return sendAndPoll(tserver, token, lastID, cmd)
|
|
}
|
|
|
|
_, otherLastID := mode(otherToken, 0, other, "+w")
|
|
_, lastID := tserver.pollMessages(token, 0)
|
|
|
|
for _, check := range []struct {
|
|
target, modeStr, numeric, modes string
|
|
}{
|
|
{nick, "+w", "221", "+w"},
|
|
{"Alice", "", "502", ""},
|
|
{"Alice", "-w", "502", ""},
|
|
{nick, "", "221", "+w"},
|
|
{nick, "-w", "221", "+"},
|
|
{nick, "+z", "501", ""},
|
|
{nick, "+o", "501", ""},
|
|
{other, "", "502", ""},
|
|
{other, "-w", "502", ""},
|
|
} {
|
|
var msgs []map[string]any
|
|
|
|
msgs, lastID = mode(token, lastID, check.target, check.modeStr)
|
|
|
|
if check.numeric != "221" {
|
|
if !findNumeric(msgs, check.numeric) ||
|
|
findNumeric(msgs, "221") {
|
|
t.Errorf(
|
|
"MODE %s %s: expected only %s, got %v",
|
|
check.target, check.modeStr,
|
|
check.numeric, msgs,
|
|
)
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
got := numericBody(t, msgs, "221")
|
|
if got != check.modes {
|
|
t.Errorf(
|
|
"MODE %s %s: expected %q, got %q",
|
|
check.target, check.modeStr, check.modes, got,
|
|
)
|
|
}
|
|
}
|
|
|
|
msgs, _ := mode(otherToken, otherLastID, other, "")
|
|
if got := numericBody(t, msgs, "221"); got != "+w" {
|
|
t.Errorf("%s's modes changed to %q", other, got)
|
|
}
|
|
|
|
msgs, _ = mode(operToken, operLastID, "deoper", "-o")
|
|
if got := numericBody(t, msgs, "221"); got != "+" {
|
|
t.Errorf("after -o: expected +, got %q", got)
|
|
}
|
|
}
|