Todas las comprobaciones han sido exitosas
check / check (push) Successful in 58s
Add a backward-compatible IRC protocol listener (RFC 1459/2812) that allows standard IRC clients (irssi, weechat, hexchat, etc.) to connect directly via TCP. Key features: - TCP listener on configurable port (IRC_LISTEN_ADDR env var, e.g. :6667) - Full IRC wire protocol parsing and formatting - Connection registration (NICK + USER + optional PASS) - Channel operations: JOIN, PART, MODE, TOPIC, NAMES, LIST, KICK, INVITE - Messaging: PRIVMSG, NOTICE (channel and direct) - Info commands: WHO, WHOIS, LUSERS, MOTD, AWAY - Operator support: OPER (with configured credentials) - PING/PONG keepalive - CAP negotiation (for modern client compatibility) - Full bridge to HTTP/JSON API (shared DB, broker, sessions) - Real-time message relay via broker notifications - Comprehensive test suite (parser + integration tests) The IRC listener is an optional component — disabled when IRC_LISTEN_ADDR is empty (the default). The Broker is now an Fx-provided dependency shared between HTTP handlers and the IRC server. closes #89
329 líneas
6.3 KiB
Go
329 líneas
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,
|
|
)
|
|
}
|
|
}
|
|
}
|