check / check (push) Waiting to run
The handlers tests ran one at a time because config.New writes the global viper instance. Each test spends most of its time in SQLite under the race detector, so run in a row they took longer than the 20-second target. Every test and subtest now calls t.Parallel(), as the tests in the other server packages already do. A mutex in newTestServerWith makes tests start their servers one at a time, since config.New writes viper and the server reads it as it starts. No test is removed or otherwise changed. Model: opus-5-5
347 lines
8.6 KiB
Go
347 lines
8.6 KiB
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) {
|
|
t.Parallel()
|
|
|
|
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) {
|
|
t.Parallel()
|
|
|
|
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) {
|
|
t.Parallel()
|
|
|
|
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) {
|
|
t.Parallel()
|
|
|
|
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) {
|
|
t.Parallel()
|
|
|
|
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)
|
|
}
|
|
}
|