feat: implement Tier 3 utility IRC commands
All checks were successful
check / check (push) Successful in 59s
All checks were successful
check / check (push) Successful in 59s
Implement all 7 utility IRC commands from issue #87: User commands: - USERHOST: quick lookup of user@host for up to 5 nicks (RPL 302) - VERSION: server version string using globals.Version (RPL 351) - ADMIN: server admin contact info (RPL 256-259) - INFO: server software info text (RPL 371/374) - TIME: server local time in RFC format (RPL 391) Oper commands: - KILL: forcibly disconnect a user (requires is_oper), broadcasts QUIT to all shared channels, cleans up sessions - WALLOPS: broadcast message to all users with +w usermode (requires is_oper) Supporting changes: - Add is_wallops column to sessions table in 001_initial.sql - Add user mode +w tracking via MODE nick +w/-w - User mode queries now return actual modes (+o, +w) - MODE -o allows de-opering yourself; MODE +o rejected - MODE for other users returns ERR_USERSDONTMATCH (502) - Extract dispatch helpers to reduce dispatchCommand complexity Tests cover all commands including error cases, oper checks, user mode set/unset, KILL broadcast, WALLOPS delivery, and edge cases (self-kill, nonexistent users, missing params). closes #87
This commit is contained in:
982
internal/handlers/utility_test.go
Normal file
982
internal/handlers/utility_test.go
Normal file
@@ -0,0 +1,982 @@
|
||||
// Tests for Tier 3 utility IRC commands: USERHOST,
|
||||
// VERSION, ADMIN, INFO, TIME, KILL, WALLOPS.
|
||||
//
|
||||
//nolint:paralleltest
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// --- USERHOST ---
|
||||
|
||||
func TestUserhostSingleNick(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("alice")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "USERHOST",
|
||||
bodyKey: []string{"alice"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 302 RPL_USERHOST.
|
||||
msg := findNumericWithParams(msgs, "302")
|
||||
if msg == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_USERHOST (302), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
// Body should contain "alice" with the
|
||||
// nick=+user@host format.
|
||||
body := getNumericBody(msg)
|
||||
if !strings.Contains(body, "alice") {
|
||||
t.Fatalf(
|
||||
"expected body to contain 'alice', got %q",
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
||||
// '+' means not away.
|
||||
if !strings.Contains(body, "=+") {
|
||||
t.Fatalf(
|
||||
"expected not-away prefix '=+', got %q",
|
||||
body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserhostMultipleNicks(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token1 := tserver.createSession("bob")
|
||||
token2 := tserver.createSession("carol")
|
||||
|
||||
_ = token2
|
||||
|
||||
_, lastID := tserver.pollMessages(token1, 0)
|
||||
|
||||
tserver.sendCommand(token1, map[string]any{
|
||||
commandKey: "USERHOST",
|
||||
bodyKey: []string{"bob", "carol"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token1, lastID)
|
||||
|
||||
msg := findNumericWithParams(msgs, "302")
|
||||
if msg == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_USERHOST (302), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
body := getNumericBody(msg)
|
||||
if !strings.Contains(body, "bob") {
|
||||
t.Fatalf(
|
||||
"expected body to contain 'bob', got %q",
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
||||
if !strings.Contains(body, "carol") {
|
||||
t.Fatalf(
|
||||
"expected body to contain 'carol', got %q",
|
||||
body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserhostNonexistentNick(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("dave")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "USERHOST",
|
||||
bodyKey: []string{"nobody"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Should still get 302 but with empty body.
|
||||
msg := findNumericWithParams(msgs, "302")
|
||||
if msg == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_USERHOST (302), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserhostNoParams(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("eve")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "USERHOST",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 461 ERR_NEEDMOREPARAMS.
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserhostShowsOper(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
token := tserver.createSession("opernick")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Authenticate as oper.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
_, lastID = tserver.pollMessages(token, lastID)
|
||||
|
||||
// USERHOST should show '*' for oper.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "USERHOST",
|
||||
bodyKey: []string{"opernick"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
msg := findNumericWithParams(msgs, "302")
|
||||
if msg == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_USERHOST (302), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
body := getNumericBody(msg)
|
||||
if !strings.Contains(body, "opernick*=") {
|
||||
t.Fatalf(
|
||||
"expected oper '*' in reply, got %q",
|
||||
body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserhostShowsAway(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("awaynick")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Set away.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "AWAY",
|
||||
bodyKey: []string{"gone fishing"},
|
||||
})
|
||||
|
||||
_, lastID = tserver.pollMessages(token, lastID)
|
||||
|
||||
// USERHOST should show '-' for away.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "USERHOST",
|
||||
bodyKey: []string{"awaynick"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
msg := findNumericWithParams(msgs, "302")
|
||||
if msg == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_USERHOST (302), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
body := getNumericBody(msg)
|
||||
if !strings.Contains(body, "=-") {
|
||||
t.Fatalf(
|
||||
"expected away prefix '=-' in reply, got %q",
|
||||
body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- VERSION ---
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("frank")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "VERSION",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 351 RPL_VERSION.
|
||||
msg := findNumericWithParams(msgs, "351")
|
||||
if msg == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_VERSION (351), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
params := getNumericParams(msg)
|
||||
if len(params) == 0 {
|
||||
t.Fatal("expected VERSION params, got none")
|
||||
}
|
||||
|
||||
// First param should contain version string.
|
||||
if !strings.Contains(params[0], "test") {
|
||||
t.Fatalf(
|
||||
"expected version to contain 'test', got %q",
|
||||
params[0],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- ADMIN ---
|
||||
|
||||
func TestAdmin(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("grace")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "ADMIN",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 256 RPL_ADMINME.
|
||||
if !findNumeric(msgs, "256") {
|
||||
t.Fatalf(
|
||||
"expected RPL_ADMINME (256), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
// Expect 257 RPL_ADMINLOC1.
|
||||
if !findNumeric(msgs, "257") {
|
||||
t.Fatalf(
|
||||
"expected RPL_ADMINLOC1 (257), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
// Expect 258 RPL_ADMINLOC2.
|
||||
if !findNumeric(msgs, "258") {
|
||||
t.Fatalf(
|
||||
"expected RPL_ADMINLOC2 (258), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
// Expect 259 RPL_ADMINEMAIL.
|
||||
if !findNumeric(msgs, "259") {
|
||||
t.Fatalf(
|
||||
"expected RPL_ADMINEMAIL (259), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- INFO ---
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("hank")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "INFO",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 371 RPL_INFO (at least one).
|
||||
if !findNumeric(msgs, "371") {
|
||||
t.Fatalf(
|
||||
"expected RPL_INFO (371), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
// Expect 374 RPL_ENDOFINFO.
|
||||
if !findNumeric(msgs, "374") {
|
||||
t.Fatalf(
|
||||
"expected RPL_ENDOFINFO (374), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- TIME ---
|
||||
|
||||
func TestTime(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("iris")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "TIME",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 391 RPL_TIME.
|
||||
msg := findNumericWithParams(msgs, "391")
|
||||
if msg == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_TIME (391), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- KILL ---
|
||||
|
||||
func TestKillSuccess(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
// Create the victim first.
|
||||
victimToken := tserver.createSession("victim")
|
||||
_ = victimToken
|
||||
|
||||
// Create oper user.
|
||||
operToken := tserver.createSession("killer")
|
||||
_, lastID := tserver.pollMessages(operToken, 0)
|
||||
|
||||
// Authenticate as oper.
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
_, lastID = tserver.pollMessages(operToken, lastID)
|
||||
|
||||
// Kill the victim.
|
||||
status, result := tserver.sendCommand(
|
||||
operToken, map[string]any{
|
||||
commandKey: "KILL",
|
||||
bodyKey: []string{"victim", "go away"},
|
||||
},
|
||||
)
|
||||
|
||||
if status != 200 {
|
||||
t.Fatalf("expected 200, got %d: %v", status, result)
|
||||
}
|
||||
|
||||
resultStatus, _ := result[statusKey].(string)
|
||||
if resultStatus != "ok" {
|
||||
t.Fatalf(
|
||||
"expected status ok, got %v",
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
// Verify the victim's session is gone by trying
|
||||
// to WHOIS them.
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "WHOIS",
|
||||
toKey: "victim",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(operToken, lastID)
|
||||
|
||||
// Should get 401 ERR_NOSUCHNICK.
|
||||
if !findNumeric(msgs, "401") {
|
||||
t.Fatalf(
|
||||
"expected victim to be gone (401), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKillNotOper(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
_ = tserver.createSession("target")
|
||||
|
||||
token := tserver.createSession("notoper")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "KILL",
|
||||
bodyKey: []string{"target", "no reason"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 481 ERR_NOPRIVILEGES.
|
||||
if !findNumeric(msgs, "481") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NOPRIVILEGES (481), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKillNoParams(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
token := tserver.createSession("opertest")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
_, lastID = tserver.pollMessages(token, lastID)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "KILL",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 461 ERR_NEEDMOREPARAMS.
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// sendOperKillCommand is a helper that creates an oper
|
||||
// session, authenticates, then sends KILL with the given
|
||||
// target nick, and returns the resulting messages.
|
||||
func sendOperKillCommand(
|
||||
t *testing.T,
|
||||
tserver *testServer,
|
||||
operNick, targetNick string,
|
||||
) []map[string]any {
|
||||
t.Helper()
|
||||
|
||||
token := tserver.createSession(operNick)
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
_, lastID = tserver.pollMessages(token, lastID)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "KILL",
|
||||
bodyKey: []string{targetNick},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
return msgs
|
||||
}
|
||||
|
||||
func TestKillNonexistentUser(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
msgs := sendOperKillCommand(
|
||||
t, tserver, "opertest2", "ghost",
|
||||
)
|
||||
|
||||
// Expect 401 ERR_NOSUCHNICK.
|
||||
if !findNumeric(msgs, "401") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NOSUCHNICK (401), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKillSelf(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
msgs := sendOperKillCommand(
|
||||
t, tserver, "selfkiller", "selfkiller",
|
||||
)
|
||||
|
||||
// Expect 483 ERR_CANTKILLSERVER.
|
||||
if !findNumeric(msgs, "483") {
|
||||
t.Fatalf(
|
||||
"expected ERR_CANTKILLSERVER (483), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKillBroadcastsQuit(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
// Create victim and join a channel.
|
||||
victimToken := tserver.createSession("vuser")
|
||||
|
||||
tserver.sendCommand(victimToken, map[string]any{
|
||||
commandKey: joinCmd,
|
||||
toKey: "#killtest",
|
||||
})
|
||||
|
||||
// Create observer and join same channel.
|
||||
observerToken := tserver.createSession("observer")
|
||||
|
||||
tserver.sendCommand(observerToken, map[string]any{
|
||||
commandKey: joinCmd,
|
||||
toKey: "#killtest",
|
||||
})
|
||||
|
||||
_, lastObs := tserver.pollMessages(observerToken, 0)
|
||||
|
||||
// Create oper.
|
||||
operToken := tserver.createSession("theoper2")
|
||||
|
||||
tserver.pollMessages(operToken, 0)
|
||||
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
tserver.pollMessages(operToken, 0)
|
||||
|
||||
// Kill the victim.
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "KILL",
|
||||
bodyKey: []string{"vuser", "testing kill"},
|
||||
})
|
||||
|
||||
// Observer should see a QUIT message.
|
||||
msgs, _ := tserver.pollMessages(observerToken, lastObs)
|
||||
|
||||
foundQuit := false
|
||||
|
||||
for _, msg := range msgs {
|
||||
cmd, _ := msg["command"].(string)
|
||||
if cmd == "QUIT" {
|
||||
from, _ := msg["from"].(string)
|
||||
if from == "vuser" {
|
||||
foundQuit = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundQuit {
|
||||
t.Fatalf(
|
||||
"expected QUIT from vuser, got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- WALLOPS ---
|
||||
|
||||
func TestWallopsSuccess(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
// Create receiver with +w.
|
||||
receiverToken := tserver.createSession("receiver")
|
||||
|
||||
tserver.sendCommand(receiverToken, map[string]any{
|
||||
commandKey: "MODE",
|
||||
toKey: "receiver",
|
||||
bodyKey: []string{"+w"},
|
||||
})
|
||||
|
||||
_, lastRecv := tserver.pollMessages(receiverToken, 0)
|
||||
|
||||
// Create oper.
|
||||
operToken := tserver.createSession("walloper")
|
||||
|
||||
tserver.pollMessages(operToken, 0)
|
||||
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
tserver.pollMessages(operToken, 0)
|
||||
|
||||
// Also set +w on oper so they receive it too.
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "MODE",
|
||||
toKey: "walloper",
|
||||
bodyKey: []string{"+w"},
|
||||
})
|
||||
|
||||
tserver.pollMessages(operToken, 0)
|
||||
|
||||
// Send WALLOPS.
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "WALLOPS",
|
||||
bodyKey: []string{"server going down"},
|
||||
})
|
||||
|
||||
// Receiver should get the WALLOPS message.
|
||||
msgs, _ := tserver.pollMessages(receiverToken, lastRecv)
|
||||
|
||||
foundWallops := false
|
||||
|
||||
for _, msg := range msgs {
|
||||
cmd, _ := msg["command"].(string)
|
||||
if cmd == "WALLOPS" {
|
||||
foundWallops = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundWallops {
|
||||
t.Fatalf(
|
||||
"expected WALLOPS message, got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWallopsNotOper(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("notoper2")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "WALLOPS",
|
||||
bodyKey: []string{"hello"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 481 ERR_NOPRIVILEGES.
|
||||
if !findNumeric(msgs, "481") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NOPRIVILEGES (481), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWallopsNoParams(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
token := tserver.createSession("operempty")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
_, lastID = tserver.pollMessages(token, lastID)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "WALLOPS",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 461 ERR_NEEDMOREPARAMS.
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWallopsNotReceivedWithoutW(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
// Create receiver WITHOUT +w.
|
||||
receiverToken := tserver.createSession("nowallops")
|
||||
_, lastRecv := tserver.pollMessages(receiverToken, 0)
|
||||
|
||||
// Create oper.
|
||||
operToken := tserver.createSession("walloper2")
|
||||
|
||||
tserver.pollMessages(operToken, 0)
|
||||
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
tserver.pollMessages(operToken, 0)
|
||||
|
||||
// Send WALLOPS.
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "WALLOPS",
|
||||
bodyKey: []string{"secret message"},
|
||||
})
|
||||
|
||||
// Receiver should NOT get the WALLOPS message.
|
||||
msgs, _ := tserver.pollMessages(receiverToken, lastRecv)
|
||||
|
||||
for _, msg := range msgs {
|
||||
cmd, _ := msg["command"].(string)
|
||||
if cmd == "WALLOPS" {
|
||||
t.Fatalf(
|
||||
"did not expect WALLOPS for user "+
|
||||
"without +w, got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- User Mode +w ---
|
||||
|
||||
func TestUserModeSetW(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("wmoder")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Set +w.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "MODE",
|
||||
toKey: "wmoder",
|
||||
bodyKey: []string{"+w"},
|
||||
})
|
||||
|
||||
msgs, lastID := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 221 RPL_UMODEIS with "+w".
|
||||
msg := findNumericWithParams(msgs, "221")
|
||||
if msg == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_UMODEIS (221), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
body := getNumericBody(msg)
|
||||
if !strings.Contains(body, "w") {
|
||||
t.Fatalf(
|
||||
"expected mode string to contain 'w', got %q",
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
||||
// Now query mode.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "MODE",
|
||||
toKey: "wmoder",
|
||||
})
|
||||
|
||||
msgs, _ = tserver.pollMessages(token, lastID)
|
||||
|
||||
msg = findNumericWithParams(msgs, "221")
|
||||
if msg == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_UMODEIS (221) on query, got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
body = getNumericBody(msg)
|
||||
if !strings.Contains(body, "w") {
|
||||
t.Fatalf(
|
||||
"expected mode '+w' in query, got %q",
|
||||
body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserModeUnsetW(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("wunsetter")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Set +w first.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "MODE",
|
||||
toKey: "wunsetter",
|
||||
bodyKey: []string{"+w"},
|
||||
})
|
||||
|
||||
_, lastID = tserver.pollMessages(token, lastID)
|
||||
|
||||
// Unset -w.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "MODE",
|
||||
toKey: "wunsetter",
|
||||
bodyKey: []string{"-w"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
msg := findNumericWithParams(msgs, "221")
|
||||
if msg == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_UMODEIS (221), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
body := getNumericBody(msg)
|
||||
if strings.Contains(body, "w") {
|
||||
t.Fatalf(
|
||||
"expected 'w' to be removed, got %q",
|
||||
body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserModeUnknownFlag(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("badmode")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "MODE",
|
||||
toKey: "badmode",
|
||||
bodyKey: []string{"+z"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 501 ERR_UMODEUNKNOWNFLAG.
|
||||
if !findNumeric(msgs, "501") {
|
||||
t.Fatalf(
|
||||
"expected ERR_UMODEUNKNOWNFLAG (501), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserModeCannotSetO(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("tryoper")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Try to set +o via MODE (should fail).
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "MODE",
|
||||
toKey: "tryoper",
|
||||
bodyKey: []string{"+o"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 501 ERR_UMODEUNKNOWNFLAG.
|
||||
if !findNumeric(msgs, "501") {
|
||||
t.Fatalf(
|
||||
"expected ERR_UMODEUNKNOWNFLAG (501), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserModeDeoper(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
token := tserver.createSession("deoper")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Authenticate as oper.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
_, lastID = tserver.pollMessages(token, lastID)
|
||||
|
||||
// Use MODE -o to de-oper.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "MODE",
|
||||
toKey: "deoper",
|
||||
bodyKey: []string{"-o"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
msg := findNumericWithParams(msgs, "221")
|
||||
if msg == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_UMODEIS (221), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
body := getNumericBody(msg)
|
||||
if strings.Contains(body, "o") {
|
||||
t.Fatalf(
|
||||
"expected 'o' to be removed, got %q",
|
||||
body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserModeCannotChangeOtherUser(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
_ = tserver.createSession("other")
|
||||
|
||||
token := tserver.createSession("changer")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Try to change another user's mode.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "MODE",
|
||||
toKey: "other",
|
||||
bodyKey: []string{"+w"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 502 ERR_USERSDONTMATCH.
|
||||
if !findNumeric(msgs, "502") {
|
||||
t.Fatalf(
|
||||
"expected ERR_USERSDONTMATCH (502), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// getNumericBody extracts the body text from a numeric
|
||||
// message. The body is stored as a JSON array; this
|
||||
// returns the first element.
|
||||
func getNumericBody(msg map[string]any) string {
|
||||
raw, exists := msg["body"]
|
||||
if !exists || raw == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
arr, isArr := raw.([]any)
|
||||
if !isArr || len(arr) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
str, isStr := arr[0].(string)
|
||||
if !isStr {
|
||||
return ""
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
Reference in New Issue
Block a user