check / check (push) Waiting to run
The IRC listener relayed only the first line of a PRIVMSG or NOTICE body; it now sends one line per body line. A relayed MODE change now carries its key, limit or mask, each value its own parameter, also in the echo to the user who made it. CR, LF and NUL are removed from each parameter and from each whole line sent to an IRC listener connection, so no stored value can start a line of its own. The channel name check moved into service.JoinChannel, so the IRC listener's JOIN refuses the same names with 403. Model: opus-5-5 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
188 lines
4.7 KiB
Go
188 lines
4.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)
|
|
}
|
|
}
|