check / check (push) Waiting to run
README.md rules that nicks are case-sensitive, but the IRC listener's relay and user MODE on both transports compared nicks ignoring case: an IRC listener user alice never saw a PRIVMSG, NOTICE, JOIN, PART, NICK or QUIT from Alice, and MODE Alice from alice was taken as her own nick. These checks now use ==. Tests cover alice receiving each of these from Alice, MODE Alice from alice getting 502 on both transports, and Alice registering while alice exists. README.md drops the text that described the old behaviour. Model: opus-5-5
224 lines
5.7 KiB
Go
224 lines
5.7 KiB
Go
package ircserver_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// 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, 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|