refactor: replace HTTP error codes with IRC numeric replies for all IRC commands
All checks were successful
check / check (push) Successful in 58s

IRC commands (PRIVMSG, JOIN, PART, NICK, TOPIC, etc.) now respond with
proper IRC numeric replies delivered through the message queue instead of
HTTP status codes. HTTP error codes are now reserved exclusively for
transport-level concerns: auth failures (401), malformed requests (400),
and server errors (500).

Changes:
- Add params column to messages table for IRC-style parameters
- Add Params field to IRCMessage struct and update all queries
- Add respondIRCError helper for consistent IRC error delivery
- Add RPL_WELCOME (001) on session creation and login
- Add RPL_TOPIC/RPL_NOTOPIC (332/331), RPL_NAMREPLY (353),
  RPL_ENDOFNAMES (366) on JOIN
- Add RPL_TOPIC (332) on TOPIC set
- Replace HTTP 404 with ERR_NOSUCHCHANNEL (403) and ERR_NOSUCHNICK (401)
- Replace HTTP 409 with ERR_NICKNAMEINUSE (433)
- Replace HTTP 403 with ERR_NOTONCHANNEL (442)
- Replace HTTP 400 with ERR_NEEDMOREPARAMS (461), ERR_ERRONEUSNICKNAME (432),
  and ERR_UNKNOWNCOMMAND (421) where appropriate
- Change PRIVMSG/NOTICE success from HTTP 201 to HTTP 200
- Update all tests to verify IRC numerics in message queue
- Add new tests for RPL_WELCOME and JOIN numerics
- Update README to document new numeric reply behavior

closes #54
This commit is contained in:
clawbot
2026-03-08 01:32:02 -08:00
parent c0e344d6fc
commit f9c145ad09
7 changed files with 627 additions and 174 deletions

View File

@@ -91,7 +91,7 @@ func (hdlr *Handlers) fanOut(
sessionIDs []int64,
) (string, error) {
dbID, msgUUID, err := hdlr.params.Database.InsertMessage(
request.Context(), command, from, target, body, nil,
request.Context(), command, from, target, nil, body, nil,
)
if err != nil {
return "", fmt.Errorf("insert message: %w", err)
@@ -185,7 +185,7 @@ func (hdlr *Handlers) handleCreateSession(
return
}
hdlr.deliverMOTD(request, clientID, sessionID)
hdlr.deliverMOTD(request, clientID, sessionID, payload.Nick)
hdlr.respondJSON(writer, request, map[string]any{
"id": sessionID,
@@ -219,49 +219,76 @@ func (hdlr *Handlers) handleCreateSessionError(
)
}
// deliverWelcome sends the RPL_WELCOME (001) numeric to a
// new client.
func (hdlr *Handlers) deliverWelcome(
request *http.Request,
clientID int64,
nick string,
) {
ctx := request.Context()
hdlr.enqueueNumeric(
ctx, clientID, "001", nick, nil,
"Welcome to the network, "+nick,
)
}
// deliverMOTD sends the MOTD as IRC numeric messages to a
// new client.
func (hdlr *Handlers) deliverMOTD(
request *http.Request,
clientID, sessionID int64,
nick string,
) {
motd := hdlr.params.Config.MOTD
serverName := hdlr.params.Config.ServerName
if serverName == "" {
serverName = "neoirc"
}
if motd == "" {
return
}
srvName := hdlr.serverName()
ctx := request.Context()
hdlr.deliverWelcome(request, clientID, nick)
if motd == "" {
hdlr.broker.Notify(sessionID)
return
}
hdlr.enqueueNumeric(
ctx, clientID, "375", serverName,
"- "+serverName+" Message of the Day -",
ctx, clientID, "375", nick, nil,
"- "+srvName+" Message of the Day -",
)
for line := range strings.SplitSeq(motd, "\n") {
hdlr.enqueueNumeric(
ctx, clientID, "372", serverName,
ctx, clientID, "372", nick, nil,
"- "+line,
)
}
hdlr.enqueueNumeric(
ctx, clientID, "376", serverName,
ctx, clientID, "376", nick, nil,
"End of /MOTD command.",
)
hdlr.broker.Notify(sessionID)
}
func (hdlr *Handlers) serverName() string {
name := hdlr.params.Config.ServerName
if name == "" {
return "neoirc"
}
return name
}
func (hdlr *Handlers) enqueueNumeric(
ctx context.Context,
clientID int64,
command, serverName, text string,
command, nick string,
params []string,
text string,
) {
body, err := json.Marshal([]string{text})
if err != nil {
@@ -272,9 +299,22 @@ func (hdlr *Handlers) enqueueNumeric(
return
}
var paramsJSON json.RawMessage
if len(params) > 0 {
paramsJSON, err = json.Marshal(params)
if err != nil {
hdlr.log.Error(
"marshal numeric params", "error", err,
)
return
}
}
dbID, _, insertErr := hdlr.params.Database.InsertMessage(
ctx, command, serverName, "",
json.RawMessage(body), nil,
ctx, command, hdlr.serverName(), nick,
paramsJSON, json.RawMessage(body), nil,
)
if insertErr != nil {
hdlr.log.Error(
@@ -532,7 +572,7 @@ func (hdlr *Handlers) HandleSendCommand() http.HandlerFunc {
writer, request.Body, hdlr.maxBodySize(),
)
sessionID, _, nick, ok :=
sessionID, clientID, nick, ok :=
hdlr.requireAuth(writer, request)
if !ok {
return
@@ -582,7 +622,8 @@ func (hdlr *Handlers) HandleSendCommand() http.HandlerFunc {
}
hdlr.dispatchCommand(
writer, request, sessionID, nick,
writer, request,
sessionID, clientID, nick,
payload.Command, payload.To,
payload.Body, bodyLines,
)
@@ -592,7 +633,7 @@ func (hdlr *Handlers) HandleSendCommand() http.HandlerFunc {
func (hdlr *Handlers) dispatchCommand(
writer http.ResponseWriter,
request *http.Request,
sessionID int64,
sessionID, clientID int64,
nick, command, target string,
body json.RawMessage,
bodyLines func() []string,
@@ -600,24 +641,30 @@ func (hdlr *Handlers) dispatchCommand(
switch command {
case cmdPrivmsg, "NOTICE":
hdlr.handlePrivmsg(
writer, request, sessionID, nick,
writer, request,
sessionID, clientID, nick,
command, target, body, bodyLines,
)
case "JOIN":
hdlr.handleJoin(
writer, request, sessionID, nick, target,
writer, request,
sessionID, clientID, nick, target,
)
case "PART":
hdlr.handlePart(
writer, request, sessionID, nick, target, body,
writer, request,
sessionID, clientID, nick, target, body,
)
case "NICK":
hdlr.handleNick(
writer, request, sessionID, nick, bodyLines,
writer, request,
sessionID, clientID, nick, bodyLines,
)
case "TOPIC":
hdlr.handleTopic(
writer, request, nick, target, body, bodyLines,
writer, request,
sessionID, clientID, nick,
target, body, bodyLines,
)
case "QUIT":
hdlr.handleQuit(
@@ -627,50 +674,63 @@ func (hdlr *Handlers) dispatchCommand(
hdlr.respondJSON(writer, request,
map[string]string{
"command": "PONG",
"from": hdlr.params.Config.ServerName,
"from": hdlr.serverName(),
},
http.StatusOK)
default:
hdlr.respondError(
writer, request,
"unknown command: "+command,
http.StatusBadRequest,
hdlr.enqueueNumeric(
request.Context(), clientID,
"421", nick, []string{command},
"Unknown command",
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "error"},
http.StatusOK)
}
}
func (hdlr *Handlers) handlePrivmsg(
writer http.ResponseWriter,
request *http.Request,
sessionID int64,
sessionID, clientID int64,
nick, command, target string,
body json.RawMessage,
bodyLines func() []string,
) {
if target == "" {
hdlr.respondError(
writer, request,
"to field required",
http.StatusBadRequest,
hdlr.enqueueNumeric(
request.Context(), clientID,
"461", nick, []string{command},
"Not enough parameters",
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "error"},
http.StatusOK)
return
}
lines := bodyLines()
if len(lines) == 0 {
hdlr.respondError(
writer, request,
"body required",
http.StatusBadRequest,
hdlr.enqueueNumeric(
request.Context(), clientID,
"461", nick, []string{command},
"Not enough parameters",
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "error"},
http.StatusOK)
return
}
if strings.HasPrefix(target, "#") {
hdlr.handleChannelMsg(
writer, request, sessionID, nick,
writer, request,
sessionID, clientID, nick,
command, target, body,
)
@@ -678,15 +738,36 @@ func (hdlr *Handlers) handlePrivmsg(
}
hdlr.handleDirectMsg(
writer, request, sessionID, nick,
writer, request,
sessionID, clientID, nick,
command, target, body,
)
}
// respondIRCError enqueues a numeric error reply, notifies
// the broker, and sends HTTP 200 with {"status":"error"}.
func (hdlr *Handlers) respondIRCError(
writer http.ResponseWriter,
request *http.Request,
clientID, sessionID int64,
numeric, nick string,
params []string,
text string,
) {
hdlr.enqueueNumeric(
request.Context(), clientID,
numeric, nick, params, text,
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "error"},
http.StatusOK)
}
func (hdlr *Handlers) handleChannelMsg(
writer http.ResponseWriter,
request *http.Request,
sessionID int64,
sessionID, clientID int64,
nick, command, target string,
body json.RawMessage,
) {
@@ -694,10 +775,10 @@ func (hdlr *Handlers) handleChannelMsg(
request.Context(), target,
)
if err != nil {
hdlr.respondError(
writer, request,
"channel not found",
http.StatusNotFound,
hdlr.respondIRCError(
writer, request, clientID, sessionID,
"403", nick, []string{target},
"No such channel",
)
return
@@ -720,15 +801,27 @@ func (hdlr *Handlers) handleChannelMsg(
}
if !isMember {
hdlr.respondError(
writer, request,
"not a member of this channel",
http.StatusForbidden,
hdlr.respondIRCError(
writer, request, clientID, sessionID,
"442", nick, []string{target},
"You're not on that channel",
)
return
}
hdlr.sendChannelMsg(
writer, request, command, nick, target, body, chID,
)
}
func (hdlr *Handlers) sendChannelMsg(
writer http.ResponseWriter,
request *http.Request,
command, nick, target string,
body json.RawMessage,
chID int64,
) {
memberIDs, err := hdlr.params.Database.GetChannelMemberIDs(
request.Context(), chID,
)
@@ -761,13 +854,13 @@ func (hdlr *Handlers) handleChannelMsg(
hdlr.respondJSON(writer, request,
map[string]string{"id": msgUUID, "status": "sent"},
http.StatusCreated)
http.StatusOK)
}
func (hdlr *Handlers) handleDirectMsg(
writer http.ResponseWriter,
request *http.Request,
sessionID int64,
sessionID, clientID int64,
nick, command, target string,
body json.RawMessage,
) {
@@ -775,11 +868,15 @@ func (hdlr *Handlers) handleDirectMsg(
request.Context(), target,
)
if err != nil {
hdlr.respondError(
writer, request,
"user not found",
http.StatusNotFound,
hdlr.enqueueNumeric(
request.Context(), clientID,
"401", nick, []string{target},
"No such nick/channel",
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "error"},
http.StatusOK)
return
}
@@ -805,20 +902,20 @@ func (hdlr *Handlers) handleDirectMsg(
hdlr.respondJSON(writer, request,
map[string]string{"id": msgUUID, "status": "sent"},
http.StatusCreated)
http.StatusOK)
}
func (hdlr *Handlers) handleJoin(
writer http.ResponseWriter,
request *http.Request,
sessionID int64,
sessionID, clientID int64,
nick, target string,
) {
if target == "" {
hdlr.respondError(
writer, request,
"to field required",
http.StatusBadRequest,
hdlr.respondIRCError(
writer, request, clientID, sessionID,
"461", nick, []string{"JOIN"},
"Not enough parameters",
)
return
@@ -830,15 +927,27 @@ func (hdlr *Handlers) handleJoin(
}
if !validChannelRe.MatchString(channel) {
hdlr.respondError(
writer, request,
"invalid channel name",
http.StatusBadRequest,
hdlr.respondIRCError(
writer, request, clientID, sessionID,
"403", nick, []string{channel},
"No such channel",
)
return
}
hdlr.executeJoin(
writer, request,
sessionID, clientID, nick, channel,
)
}
func (hdlr *Handlers) executeJoin(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick, channel string,
) {
chID, err := hdlr.params.Database.GetOrCreateChannel(
request.Context(), channel,
)
@@ -879,6 +988,10 @@ func (hdlr *Handlers) handleJoin(
request, "JOIN", nick, channel, nil, memberIDs,
)
hdlr.deliverJoinNumerics(
request, clientID, sessionID, nick, channel, chID,
)
hdlr.respondJSON(writer, request,
map[string]string{
"status": "joined",
@@ -887,19 +1000,96 @@ func (hdlr *Handlers) handleJoin(
http.StatusOK)
}
// deliverJoinNumerics sends RPL_TOPIC/RPL_NOTOPIC,
// RPL_NAMREPLY, and RPL_ENDOFNAMES to the joining client.
func (hdlr *Handlers) deliverJoinNumerics(
request *http.Request,
clientID, sessionID int64,
nick, channel string,
chID int64,
) {
ctx := request.Context()
chInfo, err := hdlr.params.Database.GetChannelByName(
ctx, channel,
)
if err == nil {
_ = chInfo // chInfo is the ID; topic comes from DB.
}
// Get topic from channel info.
channels, listErr := hdlr.params.Database.ListChannels(
ctx, sessionID,
)
topic := ""
if listErr == nil {
for _, ch := range channels {
if ch.Name == channel {
topic = ch.Topic
break
}
}
}
if topic != "" {
hdlr.enqueueNumeric(
ctx, clientID, "332", nick,
[]string{channel}, topic,
)
} else {
hdlr.enqueueNumeric(
ctx, clientID, "331", nick,
[]string{channel}, "No topic is set",
)
}
// Get member list for NAMES reply.
members, memErr := hdlr.params.Database.ChannelMembers(
ctx, chID,
)
if memErr == nil && len(members) > 0 {
nicks := make([]string, 0, len(members))
for _, mem := range members {
nicks = append(nicks, mem.Nick)
}
hdlr.enqueueNumeric(
ctx, clientID, "353", nick,
[]string{"=", channel},
strings.Join(nicks, " "),
)
}
hdlr.enqueueNumeric(
ctx, clientID, "366", nick,
[]string{channel}, "End of /NAMES list",
)
hdlr.broker.Notify(sessionID)
}
func (hdlr *Handlers) handlePart(
writer http.ResponseWriter,
request *http.Request,
sessionID int64,
sessionID, clientID int64,
nick, target string,
body json.RawMessage,
) {
if target == "" {
hdlr.respondError(
writer, request,
"to field required",
http.StatusBadRequest,
hdlr.enqueueNumeric(
request.Context(), clientID,
"461", nick, []string{"PART"},
"Not enough parameters",
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "error"},
http.StatusOK)
return
}
@@ -913,11 +1103,15 @@ func (hdlr *Handlers) handlePart(
request.Context(), channel,
)
if err != nil {
hdlr.respondError(
writer, request,
"channel not found",
http.StatusNotFound,
hdlr.enqueueNumeric(
request.Context(), clientID,
"403", nick, []string{channel},
"No such channel",
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "error"},
http.StatusOK)
return
}
@@ -961,16 +1155,16 @@ func (hdlr *Handlers) handlePart(
func (hdlr *Handlers) handleNick(
writer http.ResponseWriter,
request *http.Request,
sessionID int64,
sessionID, clientID int64,
nick string,
bodyLines func() []string,
) {
lines := bodyLines()
if len(lines) == 0 {
hdlr.respondError(
writer, request,
"body required (new nick)",
http.StatusBadRequest,
hdlr.respondIRCError(
writer, request, clientID, sessionID,
"461", nick, []string{"NICK"},
"Not enough parameters",
)
return
@@ -979,10 +1173,10 @@ func (hdlr *Handlers) handleNick(
newNick := strings.TrimSpace(lines[0])
if !validNickRe.MatchString(newNick) {
hdlr.respondError(
writer, request,
"invalid nick",
http.StatusBadRequest,
hdlr.respondIRCError(
writer, request, clientID, sessionID,
"432", nick, []string{newNick},
"Erroneous nickname",
)
return
@@ -998,15 +1192,27 @@ func (hdlr *Handlers) handleNick(
return
}
hdlr.executeNickChange(
writer, request,
sessionID, clientID, nick, newNick,
)
}
func (hdlr *Handlers) executeNickChange(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick, newNick string,
) {
err := hdlr.params.Database.ChangeNick(
request.Context(), sessionID, newNick,
)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE") {
hdlr.respondError(
writer, request,
"nick already in use",
http.StatusConflict,
hdlr.respondIRCError(
writer, request, clientID, sessionID,
"433", nick, []string{newNick},
"Nickname is already in use",
)
return
@@ -1056,7 +1262,7 @@ func (hdlr *Handlers) broadcastNick(
dbID, _, _ := hdlr.params.Database.InsertMessage(
request.Context(), "NICK", oldNick, "",
json.RawMessage(nickBody), nil,
nil, json.RawMessage(nickBody), nil,
)
_ = hdlr.params.Database.EnqueueToSession(
@@ -1088,15 +1294,16 @@ func (hdlr *Handlers) broadcastNick(
func (hdlr *Handlers) handleTopic(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick, target string,
body json.RawMessage,
bodyLines func() []string,
) {
if target == "" {
hdlr.respondError(
writer, request,
"to field required",
http.StatusBadRequest,
hdlr.respondIRCError(
writer, request, clientID, sessionID,
"461", nick, []string{"TOPIC"},
"Not enough parameters",
)
return
@@ -1104,46 +1311,60 @@ func (hdlr *Handlers) handleTopic(
lines := bodyLines()
if len(lines) == 0 {
hdlr.respondError(
writer, request,
"body required (topic text)",
http.StatusBadRequest,
hdlr.respondIRCError(
writer, request, clientID, sessionID,
"461", nick, []string{"TOPIC"},
"Not enough parameters",
)
return
}
topic := strings.Join(lines, " ")
channel := target
if !strings.HasPrefix(channel, "#") {
channel = "#" + channel
}
err := hdlr.params.Database.SetTopic(
request.Context(), channel, topic,
chID, err := hdlr.params.Database.GetChannelByName(
request.Context(), channel,
)
if err != nil {
hdlr.log.Error(
"set topic failed", "error", err,
)
hdlr.respondError(
writer, request,
"internal error",
http.StatusInternalServerError,
hdlr.respondIRCError(
writer, request, clientID, sessionID,
"403", nick, []string{channel},
"No such channel",
)
return
}
chID, err := hdlr.params.Database.GetChannelByName(
request.Context(), channel,
hdlr.executeTopic(
writer, request,
sessionID, clientID, nick,
channel, strings.Join(lines, " "),
body, chID,
)
if err != nil {
}
func (hdlr *Handlers) executeTopic(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick, channel, topic string,
body json.RawMessage,
chID int64,
) {
setErr := hdlr.params.Database.SetTopic(
request.Context(), channel, topic,
)
if setErr != nil {
hdlr.log.Error(
"set topic failed", "error", setErr,
)
hdlr.respondError(
writer, request,
"channel not found",
http.StatusNotFound,
"internal error",
http.StatusInternalServerError,
)
return
@@ -1157,6 +1378,12 @@ func (hdlr *Handlers) handleTopic(
request, "TOPIC", nick, channel, body, memberIDs,
)
hdlr.enqueueNumeric(
request.Context(), clientID,
"332", nick, []string{channel}, topic,
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{
"status": "ok", "topic": topic,
@@ -1182,7 +1409,8 @@ func (hdlr *Handlers) handleQuit(
if len(channels) > 0 {
dbID, _, _ = hdlr.params.Database.InsertMessage(
request.Context(), "QUIT", nick, "", body, nil,
request.Context(), "QUIT", nick, "",
nil, body, nil,
)
}
@@ -1431,7 +1659,8 @@ func (hdlr *Handlers) cleanupUser(
if len(channels) > 0 {
quitDBID, _, _ = hdlr.params.Database.InsertMessage(
ctx, "QUIT", nick, "", nil, nil,
ctx, "QUIT", nick, "",
nil, nil, nil,
)
}

View File

@@ -462,6 +462,19 @@ func findMessage(
return false
}
func findNumeric(
msgs []map[string]any,
numeric string,
) bool {
for _, msg := range msgs {
if msg[commandKey] == numeric {
return true
}
}
return false
}
// --- Tests ---
func TestCreateSessionValid(t *testing.T) {
@@ -473,6 +486,47 @@ func TestCreateSessionValid(t *testing.T) {
}
}
func TestWelcomeNumeric(t *testing.T) {
tserver := newTestServer(t)
token := tserver.createSession("welcomer")
msgs, _ := tserver.pollMessages(token, 0)
if !findNumeric(msgs, "001") {
t.Fatalf(
"expected RPL_WELCOME (001), got %v",
msgs,
)
}
}
func TestJoinNumerics(t *testing.T) {
tserver := newTestServer(t)
token := tserver.createSession("jnumtest")
_, lastID := tserver.pollMessages(token, 0)
tserver.sendCommand(token, map[string]any{
commandKey: joinCmd, toKey: "#numtest",
})
msgs, _ := tserver.pollMessages(token, lastID)
if !findNumeric(msgs, "353") {
t.Fatalf(
"expected RPL_NAMREPLY (353), got %v",
msgs,
)
}
if !findNumeric(msgs, "366") {
t.Fatalf(
"expected RPL_ENDOFNAMES (366), got %v",
msgs,
)
}
}
func TestCreateSessionDuplicate(t *testing.T) {
tserver := newTestServer(t)
tserver.createSession("alice")
@@ -668,11 +722,23 @@ func TestJoinMissingTo(t *testing.T) {
tserver := newTestServer(t)
token := tserver.createSession("joiner3")
// Drain initial MOTD/welcome numerics.
_, lastID := tserver.pollMessages(token, 0)
status, _ := tserver.sendCommand(
token, map[string]any{commandKey: joinCmd},
)
if status != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", status)
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
msgs, _ := tserver.pollMessages(token, lastID)
if !findNumeric(msgs, "461") {
t.Fatalf(
"expected ERR_NEEDMOREPARAMS (461), got %v",
msgs,
)
}
}
@@ -699,9 +765,9 @@ func TestChannelMessage(t *testing.T) {
bodyKey: []string{"hello world"},
},
)
if status != http.StatusCreated {
if status != http.StatusOK {
t.Fatalf(
"expected 201, got %d: %v", status, result,
"expected 200, got %d: %v", status, result,
)
}
@@ -728,11 +794,22 @@ func TestMessageMissingBody(t *testing.T) {
commandKey: joinCmd, toKey: "#test",
})
_, lastID := tserver.pollMessages(token, 0)
status, _ := tserver.sendCommand(token, map[string]any{
commandKey: privmsgCmd, toKey: "#test",
})
if status != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", status)
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
msgs, _ := tserver.pollMessages(token, lastID)
if !findNumeric(msgs, "461") {
t.Fatalf(
"expected ERR_NEEDMOREPARAMS (461), got %v",
msgs,
)
}
}
@@ -740,12 +817,23 @@ func TestMessageMissingTo(t *testing.T) {
tserver := newTestServer(t)
token := tserver.createSession("noto")
_, lastID := tserver.pollMessages(token, 0)
status, _ := tserver.sendCommand(token, map[string]any{
commandKey: privmsgCmd,
bodyKey: []string{"hello"},
})
if status != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", status)
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
msgs, _ := tserver.pollMessages(token, lastID)
if !findNumeric(msgs, "461") {
t.Fatalf(
"expected ERR_NEEDMOREPARAMS (461), got %v",
msgs,
)
}
}
@@ -759,6 +847,8 @@ func TestNonMemberCannotSend(t *testing.T) {
commandKey: joinCmd, toKey: "#private",
})
_, lastID := tserver.pollMessages(aliceToken, 0)
// Alice tries to send without joining.
status, _ := tserver.sendCommand(
aliceToken,
@@ -768,8 +858,17 @@ func TestNonMemberCannotSend(t *testing.T) {
bodyKey: []string{"sneaky"},
},
)
if status != http.StatusForbidden {
t.Fatalf("expected 403, got %d", status)
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
msgs, _ := tserver.pollMessages(aliceToken, lastID)
if !findNumeric(msgs, "442") {
t.Fatalf(
"expected ERR_NOTONCHANNEL (442), got %v",
msgs,
)
}
}
@@ -786,9 +885,9 @@ func TestDirectMessage(t *testing.T) {
bodyKey: []string{"hey bob"},
},
)
if status != http.StatusCreated {
if status != http.StatusOK {
t.Fatalf(
"expected 201, got %d: %v", status, result,
"expected 200, got %d: %v", status, result,
)
}
@@ -818,13 +917,24 @@ func TestDMToNonexistentUser(t *testing.T) {
tserver := newTestServer(t)
token := tserver.createSession("dmsender")
_, lastID := tserver.pollMessages(token, 0)
status, _ := tserver.sendCommand(token, map[string]any{
commandKey: privmsgCmd,
toKey: "nobody",
bodyKey: []string{"hello?"},
})
if status != http.StatusNotFound {
t.Fatalf("expected 404, got %d", status)
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
msgs, _ := tserver.pollMessages(token, lastID)
if !findNumeric(msgs, "401") {
t.Fatalf(
"expected ERR_NOSUCHNICK (401), got %v",
msgs,
)
}
}
@@ -871,12 +981,23 @@ func TestNickCollision(t *testing.T) {
tserver.createSession("taken_nick")
_, lastID := tserver.pollMessages(token, 0)
status, _ := tserver.sendCommand(token, map[string]any{
commandKey: "NICK",
bodyKey: []string{"taken_nick"},
})
if status != http.StatusConflict {
t.Fatalf("expected 409, got %d", status)
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
msgs, _ := tserver.pollMessages(token, lastID)
if !findNumeric(msgs, "433") {
t.Fatalf(
"expected ERR_NICKNAMEINUSE (433), got %v",
msgs,
)
}
}
@@ -884,12 +1005,23 @@ func TestNickInvalid(t *testing.T) {
tserver := newTestServer(t)
token := tserver.createSession("nickval")
_, lastID := tserver.pollMessages(token, 0)
status, _ := tserver.sendCommand(token, map[string]any{
commandKey: "NICK",
bodyKey: []string{"bad nick!"},
})
if status != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", status)
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
msgs, _ := tserver.pollMessages(token, lastID)
if !findNumeric(msgs, "432") {
t.Fatalf(
"expected ERR_ERRONEUSNICKNAME (432), got %v",
msgs,
)
}
}
@@ -897,11 +1029,22 @@ func TestNickEmptyBody(t *testing.T) {
tserver := newTestServer(t)
token := tserver.createSession("nicknobody")
_, lastID := tserver.pollMessages(token, 0)
status, _ := tserver.sendCommand(
token, map[string]any{commandKey: "NICK"},
)
if status != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", status)
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
msgs, _ := tserver.pollMessages(token, lastID)
if !findNumeric(msgs, "461") {
t.Fatalf(
"expected ERR_NEEDMOREPARAMS (461), got %v",
msgs,
)
}
}
@@ -938,12 +1081,23 @@ func TestTopicMissingTo(t *testing.T) {
tserver := newTestServer(t)
token := tserver.createSession("topicnoto")
_, lastID := tserver.pollMessages(token, 0)
status, _ := tserver.sendCommand(token, map[string]any{
commandKey: "TOPIC",
bodyKey: []string{"topic"},
})
if status != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", status)
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
msgs, _ := tserver.pollMessages(token, lastID)
if !findNumeric(msgs, "461") {
t.Fatalf(
"expected ERR_NEEDMOREPARAMS (461), got %v",
msgs,
)
}
}
@@ -955,11 +1109,22 @@ func TestTopicMissingBody(t *testing.T) {
commandKey: joinCmd, toKey: "#topictest",
})
_, lastID := tserver.pollMessages(token, 0)
status, _ := tserver.sendCommand(token, map[string]any{
commandKey: "TOPIC", toKey: "#topictest",
})
if status != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", status)
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
msgs, _ := tserver.pollMessages(token, lastID)
if !findNumeric(msgs, "461") {
t.Fatalf(
"expected ERR_NEEDMOREPARAMS (461), got %v",
msgs,
)
}
}
@@ -1027,11 +1192,22 @@ func TestUnknownCommand(t *testing.T) {
tserver := newTestServer(t)
token := tserver.createSession("cmdtest")
_, lastID := tserver.pollMessages(token, 0)
status, _ := tserver.sendCommand(
token, map[string]any{commandKey: "BOGUS"},
)
if status != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", status)
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
msgs, _ := tserver.pollMessages(token, lastID)
if !findNumeric(msgs, "421") {
t.Fatalf(
"expected ERR_UNKNOWNCOMMAND (421), got %v",
msgs,
)
}
}
@@ -1278,12 +1454,18 @@ func TestLongPollTimeout(t *testing.T) {
tserver := newTestServer(t)
token := tserver.createSession("lp_timeout")
// Drain initial welcome/MOTD numerics.
_, lastID := tserver.pollMessages(token, 0)
start := time.Now()
resp, err := doRequestAuth(
t,
http.MethodGet,
tserver.url(apiMessages+"?timeout=1"),
tserver.url(fmt.Sprintf(
"%s?timeout=1&after=%d",
apiMessages, lastID,
)),
token,
nil,
)

View File

@@ -80,7 +80,7 @@ func (hdlr *Handlers) handleRegister(
return
}
hdlr.deliverMOTD(request, clientID, sessionID)
hdlr.deliverMOTD(request, clientID, sessionID, payload.Nick)
hdlr.respondJSON(writer, request, map[string]any{
"id": sessionID,
@@ -162,7 +162,7 @@ func (hdlr *Handlers) handleLogin(
return
}
sessionID, _, token, err :=
sessionID, clientID, token, err :=
hdlr.params.Database.LoginUser(
request.Context(),
payload.Nick,
@@ -178,6 +178,10 @@ func (hdlr *Handlers) handleLogin(
return
}
hdlr.deliverMOTD(
request, clientID, sessionID, payload.Nick,
)
hdlr.respondJSON(writer, request, map[string]any{
"id": sessionID,
"nick": payload.Nick,