Send peers the IRC listener user's QUIT reason (closes #216)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
handleQuit now records the reason on the connection, and cleanup passes it to BroadcastQuit, the same service call the HTTP API's QUIT uses. A QUIT without a reason still gives Client quit. A connection that ends without a QUIT still gives Connection closed. The relay test that expected Connection closed for a bare QUIT now expects Client quit. Model: opus-5-5
This commit was merged in pull request #239.
This commit is contained in:
@@ -371,6 +371,7 @@ func (c *Conn) handleQuit(msg *Message) {
|
|||||||
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
c.closed = true
|
c.closed = true
|
||||||
|
c.quitReason = reason
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ type Conn struct {
|
|||||||
lastQueueID int64
|
lastQueueID int64
|
||||||
closed bool
|
closed bool
|
||||||
killed bool
|
killed bool
|
||||||
|
|
||||||
|
// quitReason is the reason in the QUIT that peers are
|
||||||
|
// sent when the connection ends. The client's QUIT
|
||||||
|
// replaces it.
|
||||||
|
quitReason string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConn(
|
func newConn(
|
||||||
@@ -95,6 +100,7 @@ func newConn(
|
|||||||
svc: svc,
|
svc: svc,
|
||||||
serverSfx: srvName,
|
serverSfx: srvName,
|
||||||
remoteIP: host,
|
remoteIP: host,
|
||||||
|
quitReason: "Connection closed",
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.commands = conn.buildCommandMap()
|
conn.commands = conn.buildCommandMap()
|
||||||
@@ -278,6 +284,7 @@ func (c *Conn) cleanup(ctx context.Context) {
|
|||||||
wasKilled := c.killed
|
wasKilled := c.killed
|
||||||
sessID := c.sessionID
|
sessID := c.sessionID
|
||||||
nick := c.nick
|
nick := c.nick
|
||||||
|
quitReason := c.quitReason
|
||||||
c.closed = true
|
c.closed = true
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
@@ -287,9 +294,7 @@ func (c *Conn) cleanup(ctx context.Context) {
|
|||||||
// KILL has already sent the QUIT and deleted the
|
// KILL has already sent the QUIT and deleted the
|
||||||
// session.
|
// session.
|
||||||
if !wasKilled {
|
if !wasKilled {
|
||||||
c.svc.BroadcastQuit(
|
c.svc.BroadcastQuit(ctx, sessID, nick, quitReason)
|
||||||
ctx, sessID, nick, "Connection closed",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ func TestRelayNickDiffersOnlyInCase(t *testing.T) {
|
|||||||
{"JOIN #case", ":Alice!Alice@* JOIN #case"},
|
{"JOIN #case", ":Alice!Alice@* JOIN #case"},
|
||||||
{"NICK Alicia", ":Alice!Alice@* NICK Alicia"},
|
{"NICK Alicia", ":Alice!Alice@* NICK Alicia"},
|
||||||
{"NICK Alice", ":Alicia!Alicia@* NICK Alice"}, // back for QUIT
|
{"NICK Alice", ":Alicia!Alicia@* NICK Alice"}, // back for QUIT
|
||||||
{"QUIT", ":Alice!Alice@* QUIT :Quit: Connection closed"},
|
{"QUIT", ":Alice!Alice@* QUIT :Quit: Client quit"},
|
||||||
} {
|
} {
|
||||||
capitalAlice.send(step.line)
|
capitalAlice.send(step.line)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -24,6 +25,7 @@ import (
|
|||||||
"sneak.berlin/go/neoirc/internal/globals"
|
"sneak.berlin/go/neoirc/internal/globals"
|
||||||
"sneak.berlin/go/neoirc/internal/ircserver"
|
"sneak.berlin/go/neoirc/internal/ircserver"
|
||||||
"sneak.berlin/go/neoirc/internal/logger"
|
"sneak.berlin/go/neoirc/internal/logger"
|
||||||
|
"sneak.berlin/go/neoirc/pkg/irc"
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
@@ -873,6 +875,112 @@ func TestQuit(t *testing.T) {
|
|||||||
assertContains(t, lines, "goodbye", "QUIT reason")
|
assertContains(t, lines, "goodbye", "QUIT reason")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestQuitReasonReachesPeers checks the reason that an IRC
|
||||||
|
// listener user's channel peers see in its QUIT, on both the
|
||||||
|
// IRC listener and the HTTP API. It is the reason the user
|
||||||
|
// gave, Client quit for a QUIT without one, or Connection
|
||||||
|
// closed when the connection ends without a QUIT.
|
||||||
|
func TestQuitReasonReachesPeers(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, step := range []struct {
|
||||||
|
name, quit, want string
|
||||||
|
}{
|
||||||
|
{"QUIT with a reason", "QUIT :gone home", "gone home"},
|
||||||
|
{"QUIT without a reason", irc.CmdQuit, "Client quit"},
|
||||||
|
{"connection closed without QUIT", "", "Connection closed"},
|
||||||
|
} {
|
||||||
|
t.Run(step.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ircLine, apiBody := quitSeenByPeers(t, step.quit)
|
||||||
|
|
||||||
|
want := ":quitter!quitter@* QUIT :Quit: " + step.want
|
||||||
|
if ircLine != want {
|
||||||
|
t.Errorf("IRC peer got %q, want %q", ircLine, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(apiBody) != 1 || apiBody[0] != step.want {
|
||||||
|
t.Errorf("HTTP API peer got %q, want [%q]", apiBody, step.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// quitSeenByPeers starts a server on which quitter, ircpeer on
|
||||||
|
// the IRC listener and apipeer on the HTTP API share a channel.
|
||||||
|
// quitter sends the line quit, or closes its connection when
|
||||||
|
// quit is empty. It returns the QUIT line ircpeer is sent and
|
||||||
|
// the body of the QUIT apipeer is sent.
|
||||||
|
func quitSeenByPeers(t *testing.T, quit string) (string, []string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
env := newTestEnv(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
ircPeer := env.dial(t)
|
||||||
|
ircPeer.register("ircpeer")
|
||||||
|
ircPeer.joinAndDrain("#bye")
|
||||||
|
|
||||||
|
apiPeerID, apiPeerClientID, _, err := env.database.CreateSession(
|
||||||
|
ctx, "apipeer", "apipeer", "", "",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create peer session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
channelID, err := env.database.GetChannelByName(ctx, "#bye")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get channel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = env.database.JoinChannel(ctx, channelID, apiPeerID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("join peer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
quitter := env.dial(t)
|
||||||
|
quitter.register("quitter")
|
||||||
|
quitter.joinAndDrain("#bye")
|
||||||
|
|
||||||
|
if quit == "" {
|
||||||
|
err = quitter.conn.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("close: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quitter.send(quit)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := ircPeer.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " QUIT ")
|
||||||
|
})
|
||||||
|
|
||||||
|
// The QUIT is queued to both peers at once, so apipeer has
|
||||||
|
// it by now.
|
||||||
|
msgs, _, err := env.database.PollMessages(ctx, apiPeerClientID, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("poll peer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, msg := range msgs {
|
||||||
|
if msg.Command == irc.CmdQuit && msg.From == "quitter" {
|
||||||
|
var body []string
|
||||||
|
|
||||||
|
err = json.Unmarshal(msg.Body, &body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode QUIT body %s: %v", msg.Body, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines[len(lines)-1], body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatalf("apipeer was not sent the QUIT: %v", msgs)
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
// TestStopEndsSessions checks that stopping the server ends
|
// TestStopEndsSessions checks that stopping the server ends
|
||||||
// a registered connection's session before Stop returns, as
|
// a registered connection's session before Stop returns, as
|
||||||
// closing the connection does: the session, its client and
|
// closing the connection does: the session, its client and
|
||||||
|
|||||||
Reference in New Issue
Block a user