USERHOST, VERSION, ADMIN, INFO, TIME, KILL and WALLOPS work on the HTTP API and on the IRC listener. The USERHOST reply, the VERSION, ADMIN 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, without the operator waiting on it. 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 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #96.
This commit is contained in:
+31
-13
@@ -1015,10 +1015,10 @@ func (hdlr *Handlers) dispatchCommand(
|
||||
hdlr.handleQuit(
|
||||
writer, request, sessionID, nick, body,
|
||||
)
|
||||
case irc.CmdOper:
|
||||
hdlr.handleOper(
|
||||
case irc.CmdOper, irc.CmdKill, irc.CmdWallops:
|
||||
hdlr.dispatchOperCommand(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, bodyLines,
|
||||
sessionID, clientID, nick, command, bodyLines,
|
||||
)
|
||||
case irc.CmdMotd, irc.CmdPing:
|
||||
hdlr.dispatchInfoCommand(
|
||||
@@ -1075,6 +1075,27 @@ func (hdlr *Handlers) dispatchQueryCommand(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
)
|
||||
case irc.CmdUserhost:
|
||||
hdlr.handleUserhost(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, bodyLines,
|
||||
)
|
||||
case irc.CmdVersion:
|
||||
hdlr.handleVersion(
|
||||
writer, request, sessionID, clientID, nick,
|
||||
)
|
||||
case irc.CmdAdmin:
|
||||
hdlr.handleAdmin(
|
||||
writer, request, sessionID, clientID, nick,
|
||||
)
|
||||
case irc.CmdInfo:
|
||||
hdlr.handleInfo(
|
||||
writer, request, sessionID, clientID, nick,
|
||||
)
|
||||
case irc.CmdTime:
|
||||
hdlr.handleTime(
|
||||
writer, request, sessionID, clientID, nick,
|
||||
)
|
||||
default:
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
@@ -1930,8 +1951,9 @@ func (hdlr *Handlers) handleQuit(
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleMode handles the MODE command for channels and
|
||||
// users. Currently supports query-only (no mode changes).
|
||||
// handleMode handles the MODE command: a target that starts
|
||||
// with # is a channel, handled by handleChannelMode, and any
|
||||
// other target a nick, handled by handleUserMode.
|
||||
func (hdlr *Handlers) handleMode(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
@@ -1951,15 +1973,11 @@ func (hdlr *Handlers) handleMode(
|
||||
|
||||
channel := target
|
||||
if !strings.HasPrefix(channel, "#") {
|
||||
// User mode query — return empty modes.
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
irc.RplUmodeIs, nick, nil, "+",
|
||||
hdlr.handleUserMode(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, target,
|
||||
bodyLines,
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{statusKey: "ok"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -293,6 +293,7 @@ func newTestHandlers(
|
||||
Config: cfg,
|
||||
Database: database,
|
||||
Broker: brk,
|
||||
Globals: globs,
|
||||
})
|
||||
|
||||
hdlr, err := handlers.New(lifecycle, handlers.Params{ //nolint:exhaustruct
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
// dispatchOperCommand routes the server operator commands
|
||||
// OPER, KILL and WALLOPS to their handlers.
|
||||
func (hdlr *Handlers) dispatchOperCommand(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, command string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
switch command {
|
||||
case irc.CmdOper:
|
||||
hdlr.handleOper(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, bodyLines,
|
||||
)
|
||||
case irc.CmdKill:
|
||||
hdlr.handleKill(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, bodyLines,
|
||||
)
|
||||
case irc.CmdWallops:
|
||||
hdlr.handleWallops(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, bodyLines,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// handleUserhost handles USERHOST: user@host for each of
|
||||
// up to five nicks given in the body.
|
||||
func (hdlr *Handlers) handleUserhost(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
lines := bodyLines()
|
||||
if len(lines) == 0 {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
irc.ErrNeedMoreParams, nick,
|
||||
[]string{irc.CmdUserhost},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
reply, err := hdlr.svc.UserhostReply(
|
||||
request.Context(), lines, hdlr.serverName(),
|
||||
)
|
||||
if hdlr.handleServiceError(
|
||||
writer, request, clientID, sessionID, nick, err,
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
irc.RplUserHost, nick, nil, reply,
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{statusKey: "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleVersion handles VERSION.
|
||||
func (hdlr *Handlers) handleVersion(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID, irc.RplVersion, nick,
|
||||
[]string{
|
||||
hdlr.svc.ServerVersion() + ".",
|
||||
hdlr.serverName(),
|
||||
},
|
||||
"",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{statusKey: "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleAdmin handles ADMIN.
|
||||
func (hdlr *Handlers) handleAdmin(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
srvName := hdlr.serverName()
|
||||
location1, location2, email := hdlr.svc.AdminLines(srvName)
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplAdminMe, nick,
|
||||
[]string{srvName}, "Administrative info",
|
||||
)
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplAdminLoc1, nick, nil, location1,
|
||||
)
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplAdminLoc2, nick, nil, location2,
|
||||
)
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplAdminEmail, nick, nil, email,
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{statusKey: "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleInfo handles INFO.
|
||||
func (hdlr *Handlers) handleInfo(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
for _, line := range hdlr.svc.InfoLines() {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplInfo, nick, nil, line,
|
||||
)
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplEndOfInfo, nick, nil,
|
||||
"End of /INFO list",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{statusKey: "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleTime handles TIME: the server's local time.
|
||||
func (hdlr *Handlers) handleTime(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID, irc.RplTime, nick,
|
||||
[]string{hdlr.serverName()},
|
||||
time.Now().Format(time.RFC1123),
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{statusKey: "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleKill handles KILL: the body is the target nick and
|
||||
// an optional reason.
|
||||
func (hdlr *Handlers) handleKill(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
lines := bodyLines()
|
||||
|
||||
targetNick := ""
|
||||
if len(lines) > 0 {
|
||||
targetNick = strings.TrimSpace(lines[0])
|
||||
}
|
||||
|
||||
if targetNick == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
irc.ErrNeedMoreParams, nick,
|
||||
[]string{irc.CmdKill},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
reason := ""
|
||||
if len(lines) > 1 {
|
||||
reason = lines[1]
|
||||
}
|
||||
|
||||
err := hdlr.svc.KillUser(
|
||||
request.Context(), sessionID, nick,
|
||||
targetNick, reason,
|
||||
)
|
||||
if hdlr.handleServiceError(
|
||||
writer, request, clientID, sessionID, nick, err,
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{statusKey: "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleWallops handles WALLOPS: the body is the message.
|
||||
func (hdlr *Handlers) handleWallops(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
lines := bodyLines()
|
||||
if len(lines) == 0 {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
irc.ErrNeedMoreParams, nick,
|
||||
[]string{irc.CmdWallops},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err := hdlr.svc.SendWallops(
|
||||
request.Context(), sessionID, nick,
|
||||
strings.Join(lines, " "),
|
||||
)
|
||||
if hdlr.handleServiceError(
|
||||
writer, request, clientID, sessionID, nick, err,
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{statusKey: "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleUserMode handles MODE for a nick: a query when the
|
||||
// body is empty, otherwise a change by the mode string in
|
||||
// the body. Only the caller's own nick, in any letter case,
|
||||
// is allowed.
|
||||
func (hdlr *Handlers) handleUserMode(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, target string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
if !strings.EqualFold(target, nick) {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
irc.ErrUsersDoNotMatch, nick, nil,
|
||||
"Can't change mode for other users",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := request.Context()
|
||||
|
||||
var (
|
||||
modes string
|
||||
err error
|
||||
)
|
||||
|
||||
lines := bodyLines()
|
||||
if len(lines) > 0 {
|
||||
modes, err = hdlr.svc.ApplyUserMode(
|
||||
ctx, sessionID, lines[0],
|
||||
)
|
||||
} else {
|
||||
modes, err = hdlr.svc.QueryUserMode(ctx, sessionID)
|
||||
}
|
||||
|
||||
if hdlr.handleServiceError(
|
||||
writer, request, clientID, sessionID, nick, err,
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplUmodeIs, nick, nil, modes,
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{statusKey: "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
//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"},
|
||||
{nick, "", "221", "+w"},
|
||||
{strings.ToUpper(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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user