Some checks failed
check / check (push) Failing after 46s
Add a traditional IRC wire protocol listener (RFC 1459/2812) on configurable port (default :6667), sharing business logic with the HTTP API via a new service layer. - IRC listener: NICK, USER, PASS, JOIN, PART, PRIVMSG, NOTICE, TOPIC, MODE, KICK, QUIT, NAMES, LIST, WHOIS, WHO, AWAY, OPER, INVITE, LUSERS, MOTD, PING/PONG, CAP - Service layer: shared logic for both transports including channel join (with Tier 2 checks: ban/invite/key/limit), message send (with ban + moderation checks), nick change, topic, kick, mode, quit broadcast, away, oper, invite - BroadcastQuit uses FanOut pattern (one insert, N enqueues) - HTTP handlers delegate to service for all command logic - Tier 2 mode operations (+b/+i/+s/+k/+l) use service methods Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
329 lines
6.3 KiB
Go
329 lines
6.3 KiB
Go
package ircserver_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.eeqj.de/sneak/neoirc/internal/ircserver"
|
|
)
|
|
|
|
//nolint:funlen // table-driven test
|
|
func TestParseMessage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want *ircserver.Message
|
|
wantNil bool
|
|
}{
|
|
{
|
|
name: "empty",
|
|
input: "",
|
|
want: nil,
|
|
wantNil: true,
|
|
},
|
|
{
|
|
name: "simple command",
|
|
input: "PING",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "PING",
|
|
Params: nil,
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "command with one param",
|
|
input: "NICK alice",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "NICK",
|
|
Params: []string{"alice"},
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "command case insensitive",
|
|
input: "nick Alice",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "NICK",
|
|
Params: []string{"Alice"},
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "privmsg with trailing",
|
|
input: "PRIVMSG #general :hello world",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "PRIVMSG",
|
|
Params: []string{"#general", "hello world"},
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "with prefix",
|
|
input: ":server.example.com 001 alice :Welcome to IRC",
|
|
want: &ircserver.Message{
|
|
Prefix: "server.example.com",
|
|
Command: "001",
|
|
Params: []string{"alice", "Welcome to IRC"},
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "user command",
|
|
input: "USER alice 0 * :Alice Smith",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "USER",
|
|
Params: []string{
|
|
"alice", "0", "*", "Alice Smith",
|
|
},
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "join channel",
|
|
input: "JOIN #general",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "JOIN",
|
|
Params: []string{"#general"},
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "quit with trailing",
|
|
input: "QUIT :leaving now",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "QUIT",
|
|
Params: []string{"leaving now"},
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "quit without reason",
|
|
input: "QUIT",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "QUIT",
|
|
Params: nil,
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "mode query",
|
|
input: "MODE #general",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "MODE",
|
|
Params: []string{"#general"},
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "kick with reason",
|
|
input: "KICK #general bob :misbehaving",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "KICK",
|
|
Params: []string{
|
|
"#general", "bob", "misbehaving",
|
|
},
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "empty trailing",
|
|
input: "PRIVMSG #general :",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "PRIVMSG",
|
|
Params: []string{"#general", ""},
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "pass command",
|
|
input: "PASS mysecret",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "PASS",
|
|
Params: []string{"mysecret"},
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "ping with server",
|
|
input: "PING :irc.example.com",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "PING",
|
|
Params: []string{"irc.example.com"},
|
|
},
|
|
wantNil: false,
|
|
},
|
|
{
|
|
name: "topic with trailing spaces",
|
|
input: "TOPIC #general :Welcome to the channel!",
|
|
want: &ircserver.Message{
|
|
Prefix: "",
|
|
Command: "TOPIC",
|
|
Params: []string{
|
|
"#general",
|
|
"Welcome to the channel!",
|
|
},
|
|
},
|
|
wantNil: false,
|
|
},
|
|
}
|
|
|
|
for _, testCase := range tests {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := ircserver.ParseMessage(testCase.input)
|
|
if testCase.wantNil {
|
|
if got != nil {
|
|
t.Fatalf("expected nil, got %+v", got)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if got == nil {
|
|
t.Fatal("expected non-nil message")
|
|
}
|
|
|
|
if got.Prefix != testCase.want.Prefix {
|
|
t.Errorf(
|
|
"prefix: got %q, want %q",
|
|
got.Prefix, testCase.want.Prefix,
|
|
)
|
|
}
|
|
|
|
if got.Command != testCase.want.Command {
|
|
t.Errorf(
|
|
"command: got %q, want %q",
|
|
got.Command, testCase.want.Command,
|
|
)
|
|
}
|
|
|
|
if len(got.Params) != len(testCase.want.Params) {
|
|
t.Fatalf(
|
|
"params length: got %d, want %d (%v vs %v)",
|
|
len(got.Params),
|
|
len(testCase.want.Params),
|
|
got.Params,
|
|
testCase.want.Params,
|
|
)
|
|
}
|
|
|
|
for i, p := range got.Params {
|
|
if p != testCase.want.Params[i] {
|
|
t.Errorf(
|
|
"param[%d]: got %q, want %q",
|
|
i, p, testCase.want.Params[i],
|
|
)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatMessage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
prefix string
|
|
command string
|
|
params []string
|
|
want string
|
|
}{
|
|
{
|
|
name: "simple command",
|
|
prefix: "",
|
|
command: "PING",
|
|
params: nil,
|
|
want: "PING",
|
|
},
|
|
{
|
|
name: "with prefix",
|
|
prefix: "server",
|
|
command: "PONG",
|
|
params: []string{"server"},
|
|
want: ":server PONG server",
|
|
},
|
|
{
|
|
name: "privmsg with trailing",
|
|
prefix: "alice!alice@host",
|
|
command: "PRIVMSG",
|
|
params: []string{"#general", "hello world"},
|
|
want: ":alice!alice@host PRIVMSG #general :hello world",
|
|
},
|
|
{
|
|
name: "numeric reply",
|
|
prefix: "server",
|
|
command: "001",
|
|
params: []string{"alice", "Welcome to IRC"},
|
|
want: ":server 001 alice :Welcome to IRC",
|
|
},
|
|
{
|
|
name: "empty trailing",
|
|
prefix: "server",
|
|
command: "PRIVMSG",
|
|
params: []string{"#chan", ""},
|
|
want: ":server PRIVMSG #chan :",
|
|
},
|
|
}
|
|
|
|
for _, testCase := range tests {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := ircserver.FormatMessage(
|
|
testCase.prefix, testCase.command, testCase.params...,
|
|
)
|
|
if got != testCase.want {
|
|
t.Errorf("got %q, want %q", got, testCase.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseFormatRoundTrip(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Round-trip only works for lines where the last
|
|
// parameter either contains a space (gets ':' prefix
|
|
// on format) or is a non-trailing single token.
|
|
lines := []string{
|
|
"PING",
|
|
"NICK alice",
|
|
"PRIVMSG #general :hello world",
|
|
"JOIN #general",
|
|
"MODE #general",
|
|
}
|
|
|
|
for _, line := range lines {
|
|
msg := ircserver.ParseMessage(line)
|
|
if msg == nil {
|
|
t.Fatalf("failed to parse: %q", line)
|
|
}
|
|
|
|
formatted := ircserver.FormatMessage(
|
|
msg.Prefix, msg.Command, msg.Params...,
|
|
)
|
|
if formatted != line {
|
|
t.Errorf(
|
|
"round-trip failed: input %q, got %q",
|
|
line, formatted,
|
|
)
|
|
}
|
|
}
|
|
}
|