check / check (push) Canceled after 0s
History was selected by msg_to, so whoever next held a released nick, or created a deleted channel again under its old name, could read the earlier holder's messages. Each message row now records the session a DM or INVITE was sent to (to_session_id) and the channel a channel message was sent to (to_channel_id), and GET /api/v1/history selects by the one that applies, using its index. A session's DM history now also follows it across nick changes. A DM or channel message whose recipient is deleted before it is stored gets the unknown nick or channel error. Model: opus-5-5
272 lines
6.8 KiB
Go
272 lines
6.8 KiB
Go
package ircserver_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/neoirc/internal/ircserver"
|
|
)
|
|
|
|
// queueMessage queues a message for the user with nick
|
|
// recipient the way the HTTP API sends one: stored once, then
|
|
// queued to the recipient's session.
|
|
func (env *testEnv) queueMessage(
|
|
t *testing.T,
|
|
recipient, command, from, to string,
|
|
body []string,
|
|
) {
|
|
t.Helper()
|
|
|
|
ctx := t.Context()
|
|
|
|
bodyJSON, err := json.Marshal(body)
|
|
if err != nil {
|
|
t.Fatalf("marshal body: %v", err)
|
|
}
|
|
|
|
msgID, _, err := env.database.InsertMessage(
|
|
ctx, command, from, to, 0, 0, nil, bodyJSON, nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("insert message: %v", err)
|
|
}
|
|
|
|
sessionID, err := env.database.GetSessionByNick(ctx, recipient)
|
|
if err != nil {
|
|
t.Fatalf("get session of %s: %v", recipient, err)
|
|
}
|
|
|
|
err = env.database.EnqueueToSession(ctx, sessionID, msgID)
|
|
if err != nil {
|
|
t.Fatalf("enqueue: %v", err)
|
|
}
|
|
|
|
env.brk.Notify(sessionID)
|
|
}
|
|
|
|
// TestRelayKeepsOneWaitChannel checks that an idle IRC
|
|
// listener connection holds at most one broker wait channel
|
|
// across several relay ticks, and none once it closes.
|
|
func TestRelayKeepsOneWaitChannel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const idleTicks = 5
|
|
|
|
env := newTestEnv(t)
|
|
|
|
client := env.dial(t)
|
|
client.register("idle")
|
|
|
|
sessionID, err := env.database.GetSessionByNick(t.Context(), "idle")
|
|
if err != nil {
|
|
t.Fatalf("get session of idle: %v", err)
|
|
}
|
|
|
|
for tick := range idleTicks {
|
|
time.Sleep(ircserver.PollInterval)
|
|
|
|
got := env.brk.ListenerCount(sessionID)
|
|
if got > 1 {
|
|
t.Fatalf("tick %d: got %d wait channels, want at most 1",
|
|
tick+1, got)
|
|
}
|
|
}
|
|
|
|
err = client.conn.Close()
|
|
if err != nil {
|
|
t.Fatalf("close: %v", err)
|
|
}
|
|
|
|
deadline := time.Now().Add(testTimeout)
|
|
|
|
for env.brk.ListenerCount(sessionID) != 0 {
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("after close: got %d wait channels, want 0",
|
|
env.brk.ListenerCount(sessionID))
|
|
}
|
|
|
|
time.Sleep(relayCheckStep)
|
|
}
|
|
}
|
|
|
|
// TestRelayMultiLineBody checks that each line of a PRIVMSG
|
|
// or NOTICE body sent over the HTTP API reaches an IRC
|
|
// listener user as its own line.
|
|
func TestRelayMultiLineBody(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
alice := env.dial(t)
|
|
alice.register("alice")
|
|
|
|
for _, command := range []string{"PRIVMSG", "NOTICE"} {
|
|
env.queueMessage(
|
|
t, "alice", command, "carol", "alice",
|
|
[]string{"first line", "second line"},
|
|
)
|
|
|
|
lines := alice.readUntil(func(l string) bool {
|
|
return strings.Contains(l, "second line")
|
|
})
|
|
|
|
want := []string{
|
|
":carol!carol@* " + command + " alice :first line",
|
|
":carol!carol@* " + command + " alice :second line",
|
|
}
|
|
|
|
got := lines[max(0, len(lines)-len(want)):]
|
|
if !slices.Equal(got, want) {
|
|
t.Errorf("%s: got %q, want %q", command, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRelayModeParameters checks that a MODE change reaches
|
|
// IRC listener members with its values as separate
|
|
// parameters, both for the bodies the HTTP API stores and
|
|
// for a change made on the IRC listener.
|
|
func TestRelayModeParameters(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
alice := env.dial(t)
|
|
alice.register("alice")
|
|
|
|
for _, step := range []struct {
|
|
body []string
|
|
want string
|
|
}{
|
|
{[]string{"+k", "secret"}, "+k secret"},
|
|
{[]string{"-k", "*"}, "-k *"},
|
|
{[]string{"+l", "50"}, "+l 50"},
|
|
{[]string{"-l"}, "-l"},
|
|
{[]string{"+b", "*!*@*.example.com"}, "+b *!*@*.example.com"},
|
|
{[]string{"-b", "*!*@*.example.com"}, "-b *!*@*.example.com"},
|
|
{[]string{"+o bob"}, "+o bob"},
|
|
{[]string{"+m"}, "+m"},
|
|
} {
|
|
env.queueMessage(t, "alice", "MODE", "op", "#modes", step.body)
|
|
|
|
lines := alice.readUntil(func(l string) bool {
|
|
return strings.Contains(l, " MODE ")
|
|
})
|
|
|
|
got := lines[len(lines)-1]
|
|
want := ":op!op@* MODE #modes " + step.want
|
|
|
|
if got != want {
|
|
t.Errorf("body %q: got %q, want %q", step.body, got, want)
|
|
}
|
|
}
|
|
|
|
bob := env.dial(t)
|
|
bob.register("bob")
|
|
|
|
alice.joinAndDrain("#modes")
|
|
bob.joinAndDrain("#modes")
|
|
|
|
alice.send("MODE #modes +v bob")
|
|
|
|
const change = " MODE #modes +v bob"
|
|
|
|
for name, client := range map[string]*testClient{
|
|
"alice": alice, "bob": bob,
|
|
} {
|
|
lines := client.readUntil(func(l string) bool {
|
|
return strings.Contains(l, " MODE ")
|
|
})
|
|
|
|
got := lines[len(lines)-1]
|
|
if !strings.HasSuffix(got, change) {
|
|
t.Errorf("%s: got %q, want it to end in %q", name, got, change)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRelayLineBreaks checks that a CR, LF or NUL in a stored
|
|
// body line, key or mask never reaches an IRC listener user
|
|
// as a line break, so no extra line arrives, and that a body
|
|
// line keeps its parameter and leading ':' once they are
|
|
// removed.
|
|
func TestRelayLineBreaks(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
bob := env.dial(t)
|
|
bob.register("bob")
|
|
|
|
env.queueMessage(t, "bob", "PRIVMSG", "carol", "bob", []string{
|
|
"one\r\n:admin!a@b PRIVMSG bob :hi",
|
|
"two\n:admin!a@b NOTICE bob :hi\x00",
|
|
})
|
|
env.queueMessage(t, "bob", "MODE", "op", "#breaks",
|
|
[]string{"+k", "key\r\nERROR :Closing link"})
|
|
env.queueMessage(t, "bob", "MODE", "op", "#breaks",
|
|
[]string{"+b", "*!*@*\r\n:admin!a@b KICK #breaks bob"})
|
|
env.queueMessage(t, "bob", "PRIVMSG", "carol", "bob",
|
|
[]string{"\r\n", "\x00:hi"})
|
|
env.queueMessage(t, "bob", "PRIVMSG", "carol", "bob",
|
|
[]string{"the end"})
|
|
|
|
lines := bob.readUntil(func(l string) bool {
|
|
return strings.HasSuffix(l, " :the end")
|
|
})
|
|
|
|
want := []string{
|
|
":carol!carol@* PRIVMSG bob :one:admin!a@b PRIVMSG bob :hi",
|
|
":carol!carol@* PRIVMSG bob :two:admin!a@b NOTICE bob :hi",
|
|
":op!op@* MODE #breaks +k :keyERROR :Closing link",
|
|
":op!op@* MODE #breaks +b :*!*@*:admin!a@b KICK #breaks bob",
|
|
":carol!carol@* PRIVMSG bob :",
|
|
":carol!carol@* PRIVMSG bob ::hi",
|
|
":carol!carol@* PRIVMSG bob :the end",
|
|
}
|
|
|
|
got := lines[max(0, len(lines)-len(want)):]
|
|
if !slices.Equal(got, want) {
|
|
t.Errorf("got %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// TestRelayNickDiffersOnlyInCase checks that an IRC listener
|
|
// user alice receives a JOIN, PRIVMSG, PART, NICK and QUIT
|
|
// from Alice in a channel they share: nicks are
|
|
// case-sensitive, so Alice is someone else.
|
|
func TestRelayNickDiffersOnlyInCase(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
alice := env.dial(t)
|
|
alice.register("alice")
|
|
alice.joinAndDrain("#case")
|
|
|
|
capitalAlice := env.dial(t)
|
|
capitalAlice.register("Alice")
|
|
|
|
for _, step := range []struct {
|
|
line, want string
|
|
}{
|
|
{"JOIN #case", ":Alice!Alice@* JOIN #case"},
|
|
{"PRIVMSG #case :hi there", ":Alice!Alice@* PRIVMSG #case :hi there"},
|
|
{"PART #case :see you", ":Alice!Alice@* PART #case :see you"},
|
|
{"JOIN #case", ":Alice!Alice@* JOIN #case"},
|
|
{"NICK Alicia", ":Alice!Alice@* NICK Alicia"},
|
|
{"NICK Alice", ":Alicia!Alicia@* NICK Alice"}, // back for QUIT
|
|
{"QUIT", ":Alice!Alice@* QUIT :Quit: Connection closed"},
|
|
} {
|
|
capitalAlice.send(step.line)
|
|
|
|
got := alice.readLine()
|
|
if got != step.want {
|
|
t.Errorf("%s: got %q, want %q", step.line, got, step.want)
|
|
}
|
|
}
|
|
}
|