Merge branch 'main' into change-module-path-to-sneak-berlin
Some checks failed
check / check (push) Failing after 2m6s
Some checks failed
check / check (push) Failing after 2m6s
This commit is contained in:
913
internal/ircserver/integration_test.go
Normal file
913
internal/ircserver/integration_test.go
Normal file
@@ -0,0 +1,913 @@
|
|||||||
|
package ircserver_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestIntegrationTwoClients is a comprehensive integration
|
||||||
|
// test that spawns the IRC server programmatically, connects
|
||||||
|
// two real TCP clients, and verifies all major IRC features
|
||||||
|
// including cross-client message delivery.
|
||||||
|
//
|
||||||
|
// The test runs sequentially through IRC features because
|
||||||
|
// both clients share the same channel state. Each section
|
||||||
|
// builds on the previous one (e.g. alice and bob must be
|
||||||
|
// JOINed before PRIVMSG can be tested).
|
||||||
|
//
|
||||||
|
//nolint:cyclop,funlen,maintidx // integration test
|
||||||
|
func TestIntegrationTwoClients(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newTestEnv(t)
|
||||||
|
|
||||||
|
alice := env.dial(t)
|
||||||
|
bob := env.dial(t)
|
||||||
|
|
||||||
|
// ── Registration ──────────────────────────────────
|
||||||
|
|
||||||
|
aliceWelcome := alice.register("alice")
|
||||||
|
assertContains(
|
||||||
|
t, aliceWelcome, " 001 ", "RPL_WELCOME alice",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, aliceWelcome, " 002 ", "RPL_YOURHOST alice",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, aliceWelcome, " 003 ", "RPL_CREATED alice",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, aliceWelcome, " 004 ", "RPL_MYINFO alice",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, aliceWelcome, "alice",
|
||||||
|
"nick in welcome burst",
|
||||||
|
)
|
||||||
|
|
||||||
|
bobWelcome := bob.register("bob")
|
||||||
|
assertContains(
|
||||||
|
t, bobWelcome, " 001 ", "RPL_WELCOME bob",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, bobWelcome, "bob",
|
||||||
|
"nick in welcome burst",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── JOIN and cross-client visibility ──────────────
|
||||||
|
|
||||||
|
alice.send("JOIN #integration")
|
||||||
|
aliceJoinLines := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 366 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceJoinLines, "JOIN",
|
||||||
|
"alice receives JOIN echo",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, aliceJoinLines, " 366 ",
|
||||||
|
"RPL_ENDOFNAMES for alice",
|
||||||
|
)
|
||||||
|
|
||||||
|
bob.send("JOIN #integration")
|
||||||
|
bobJoinLines := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 366 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobJoinLines, "JOIN",
|
||||||
|
"bob receives JOIN echo",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Alice should see bob's JOIN via relay.
|
||||||
|
aliceSeesBob := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "JOIN") &&
|
||||||
|
strings.Contains(l, "bob")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceSeesBob, "bob",
|
||||||
|
"alice sees bob's JOIN",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── PRIVMSG (channel) — alice to bob ──────────────
|
||||||
|
|
||||||
|
alice.send("PRIVMSG #integration :hello from alice")
|
||||||
|
|
||||||
|
bobGetsMsg := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "hello from alice")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobGetsMsg, "hello from alice",
|
||||||
|
"bob receives alice's channel message",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── PRIVMSG (channel) — bob to alice ──────────────
|
||||||
|
|
||||||
|
bob.send("PRIVMSG #integration :hello from bob")
|
||||||
|
|
||||||
|
aliceGetsMsg := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "hello from bob")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceGetsMsg, "hello from bob",
|
||||||
|
"alice receives bob's channel message",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── PRIVMSG (DM) — alice to bob ──────────────────
|
||||||
|
|
||||||
|
alice.send("PRIVMSG bob :secret message")
|
||||||
|
|
||||||
|
bobDM := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "secret message")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobDM, "secret message",
|
||||||
|
"bob receives alice's DM",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, bobDM, "alice",
|
||||||
|
"DM from field is alice",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── PRIVMSG (DM) — bob to alice ──────────────────
|
||||||
|
|
||||||
|
bob.send("PRIVMSG alice :reply to you")
|
||||||
|
|
||||||
|
aliceDM := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "reply to you")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceDM, "reply to you",
|
||||||
|
"alice receives bob's DM",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── NOTICE (channel) ──────────────────────────────
|
||||||
|
|
||||||
|
alice.send("NOTICE #integration :notice msg")
|
||||||
|
|
||||||
|
bobNotice := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "notice msg")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobNotice, "NOTICE",
|
||||||
|
"bob receives NOTICE command",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, bobNotice, "notice msg",
|
||||||
|
"bob receives NOTICE text",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── NOTICE (DM) ──────────────────────────────────
|
||||||
|
|
||||||
|
bob.send("NOTICE alice :dm notice")
|
||||||
|
|
||||||
|
aliceNotice := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "dm notice")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceNotice, "dm notice",
|
||||||
|
"alice receives DM NOTICE",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── TOPIC ─────────────────────────────────────────
|
||||||
|
// alice is the channel creator so she is +o.
|
||||||
|
|
||||||
|
alice.send("TOPIC #integration :Integration Test Topic")
|
||||||
|
|
||||||
|
aliceTopic := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(
|
||||||
|
l, "Integration Test Topic",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceTopic, "Integration Test Topic",
|
||||||
|
"alice sees TOPIC echo",
|
||||||
|
)
|
||||||
|
|
||||||
|
bobTopic := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(
|
||||||
|
l, "Integration Test Topic",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobTopic, "Integration Test Topic",
|
||||||
|
"bob receives TOPIC change",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── MODE (query) ──────────────────────────────────
|
||||||
|
|
||||||
|
alice.send("MODE #integration")
|
||||||
|
|
||||||
|
aliceMode := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 324 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceMode, " 324 ",
|
||||||
|
"RPL_CHANNELMODEIS",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── MODE (+m moderated, then -m) ──────────────────
|
||||||
|
|
||||||
|
alice.send("MODE #integration +m")
|
||||||
|
|
||||||
|
aliceModeM := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "MODE") &&
|
||||||
|
strings.Contains(l, "+m")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceModeM, "+m",
|
||||||
|
"alice sees MODE +m echo",
|
||||||
|
)
|
||||||
|
|
||||||
|
bobModeM := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "+m")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobModeM, "+m",
|
||||||
|
"bob sees MODE +m relay",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Revert moderated mode.
|
||||||
|
alice.send("MODE #integration -m")
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "-m")
|
||||||
|
})
|
||||||
|
bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "-m")
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── MODE (+v voice, then -v) ──────────────────────
|
||||||
|
|
||||||
|
alice.send("MODE #integration +v bob")
|
||||||
|
|
||||||
|
aliceVoice := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "+v")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceVoice, "+v",
|
||||||
|
"alice sees +v echo",
|
||||||
|
)
|
||||||
|
|
||||||
|
bobVoice := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "+v")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobVoice, "+v",
|
||||||
|
"bob receives +v relay",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Remove voice.
|
||||||
|
alice.send("MODE #integration -v bob")
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "-v")
|
||||||
|
})
|
||||||
|
bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "-v")
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── NAMES ─────────────────────────────────────────
|
||||||
|
|
||||||
|
alice.send("NAMES #integration")
|
||||||
|
|
||||||
|
aliceNames := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 366 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceNames, " 353 ",
|
||||||
|
"RPL_NAMREPLY",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, aliceNames, " 366 ",
|
||||||
|
"RPL_ENDOFNAMES",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Both nicks should appear in the name list.
|
||||||
|
foundBothNames := false
|
||||||
|
for _, line := range aliceNames {
|
||||||
|
if strings.Contains(line, " 353 ") &&
|
||||||
|
strings.Contains(line, "alice") &&
|
||||||
|
strings.Contains(line, "bob") {
|
||||||
|
foundBothNames = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundBothNames {
|
||||||
|
t.Error("NAMES reply should list both alice and bob")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── LIST ──────────────────────────────────────────
|
||||||
|
|
||||||
|
alice.send("LIST")
|
||||||
|
|
||||||
|
aliceList := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 323 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceList, " 322 ",
|
||||||
|
"RPL_LIST entry",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, aliceList, "#integration",
|
||||||
|
"LIST includes #integration",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, aliceList, " 323 ", //nolint:misspell // IRC RPL_LISTEND
|
||||||
|
"RPL_LISTEND", //nolint:misspell // IRC term
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── WHO ───────────────────────────────────────────
|
||||||
|
|
||||||
|
bob.send("WHO #integration")
|
||||||
|
|
||||||
|
bobWho := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 315 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobWho, " 352 ",
|
||||||
|
"RPL_WHOREPLY",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, bobWho, " 315 ",
|
||||||
|
"RPL_ENDOFWHO",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── WHOIS ─────────────────────────────────────────
|
||||||
|
|
||||||
|
alice.send("WHOIS bob")
|
||||||
|
|
||||||
|
aliceWhois := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 318 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceWhois, " 311 ",
|
||||||
|
"RPL_WHOISUSER",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, aliceWhois, " 312 ",
|
||||||
|
"RPL_WHOISSERVER",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, aliceWhois, " 318 ",
|
||||||
|
"RPL_ENDOFWHOIS",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── WHOIS with channels ───────────────────────────
|
||||||
|
|
||||||
|
bob.send("WHOIS alice")
|
||||||
|
|
||||||
|
bobWhois := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 318 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobWhois, " 319 ",
|
||||||
|
"RPL_WHOISCHANNELS",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, bobWhois, "#integration",
|
||||||
|
"WHOIS shows #integration channel",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── LUSERS ────────────────────────────────────────
|
||||||
|
|
||||||
|
alice.send("LUSERS")
|
||||||
|
|
||||||
|
aliceLusers := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 255 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceLusers, " 251 ",
|
||||||
|
"RPL_LUSERCLIENT",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, aliceLusers, " 255 ",
|
||||||
|
"RPL_LUSERME",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── NICK change ───────────────────────────────────
|
||||||
|
|
||||||
|
bob.send("NICK bobby")
|
||||||
|
|
||||||
|
bobNick := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "NICK") &&
|
||||||
|
strings.Contains(l, "bobby")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobNick, "bobby",
|
||||||
|
"bob sees NICK change to bobby",
|
||||||
|
)
|
||||||
|
|
||||||
|
// alice should see the nick change relayed.
|
||||||
|
aliceNick := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "bobby")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceNick, "NICK",
|
||||||
|
"alice sees NICK command",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, aliceNick, "bobby",
|
||||||
|
"alice sees new nick bobby",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Change it back for remaining tests.
|
||||||
|
bob.send("NICK bob")
|
||||||
|
bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "bob")
|
||||||
|
})
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "NICK") &&
|
||||||
|
strings.Contains(l, "bob")
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Duplicate NICK ────────────────────────────────
|
||||||
|
|
||||||
|
bob.send("NICK alice")
|
||||||
|
|
||||||
|
bobDupNick := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 433 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobDupNick, " 433 ",
|
||||||
|
"ERR_NICKNAMEINUSE",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── KICK ──────────────────────────────────────────
|
||||||
|
// alice is op; she kicks bob.
|
||||||
|
|
||||||
|
alice.send("KICK #integration bob :testing kick")
|
||||||
|
|
||||||
|
aliceKick := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "KICK")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceKick, "KICK",
|
||||||
|
"alice sees KICK echo",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, aliceKick, "bob",
|
||||||
|
"KICK mentions bob",
|
||||||
|
)
|
||||||
|
|
||||||
|
bobKick := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "KICK")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobKick, "KICK",
|
||||||
|
"bob receives KICK",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, bobKick, "testing kick",
|
||||||
|
"KICK reason is relayed",
|
||||||
|
)
|
||||||
|
|
||||||
|
// bob rejoins.
|
||||||
|
bob.joinAndDrain("#integration")
|
||||||
|
|
||||||
|
// Drain alice's view of the rejoin.
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "JOIN") &&
|
||||||
|
strings.Contains(l, "bob")
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── KICK non-op should fail ───────────────────────
|
||||||
|
|
||||||
|
bob.send("KICK #integration alice :nope")
|
||||||
|
|
||||||
|
bobKickFail := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 482 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobKickFail, " 482 ",
|
||||||
|
"ERR_CHANOPRIVSNEEDED",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── TOPIC lock (+t default) ───────────────────────
|
||||||
|
// +t is default, so bob should not be able to set
|
||||||
|
// topic.
|
||||||
|
|
||||||
|
bob.send("TOPIC #integration :bob tries topic")
|
||||||
|
|
||||||
|
bobTopicFail := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 482 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobTopicFail, " 482 ",
|
||||||
|
"ERR_CHANOPRIVSNEEDED for topic",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── PING / PONG ───────────────────────────────────
|
||||||
|
|
||||||
|
alice.send("PING :testtoken")
|
||||||
|
|
||||||
|
alicePong := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "PONG")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, alicePong, "PONG",
|
||||||
|
"PONG response received",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── Unknown command ───────────────────────────────
|
||||||
|
|
||||||
|
bob.send("FOOBAR")
|
||||||
|
|
||||||
|
bobUnknown := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 421 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobUnknown, " 421 ",
|
||||||
|
"ERR_UNKNOWNCOMMAND",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── MOTD ──────────────────────────────────────────
|
||||||
|
|
||||||
|
alice.send("MOTD")
|
||||||
|
|
||||||
|
aliceMOTD := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 376 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceMOTD, " 376 ",
|
||||||
|
"RPL_ENDOFMOTD",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── AWAY (set, check via DM, clear) ───────────────
|
||||||
|
|
||||||
|
alice.send("AWAY :gone fishing")
|
||||||
|
|
||||||
|
aliceAway := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 306 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceAway, " 306 ",
|
||||||
|
"RPL_NOWAWAY",
|
||||||
|
)
|
||||||
|
|
||||||
|
// bob DMs alice — should get RPL_AWAY.
|
||||||
|
bob.send("PRIVMSG alice :are you there?")
|
||||||
|
|
||||||
|
bobAwayReply := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 301 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobAwayReply, " 301 ",
|
||||||
|
"RPL_AWAY for bob when messaging alice",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, bobAwayReply, "gone fishing",
|
||||||
|
"away message relayed",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clear away.
|
||||||
|
alice.send("AWAY")
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 305 ")
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── PASS (set password post-registration) ─────────
|
||||||
|
|
||||||
|
alice.send("PASS :mypassword123")
|
||||||
|
|
||||||
|
alicePass := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "Password set")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, alicePass, "Password set",
|
||||||
|
"password set confirmation",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── MODE -t/+t topic lock toggle ──────────────────
|
||||||
|
|
||||||
|
alice.send("MODE #integration -t")
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "-t")
|
||||||
|
})
|
||||||
|
bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "-t")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Now bob should be able to set topic.
|
||||||
|
bob.send("TOPIC #integration :bob sets topic now")
|
||||||
|
|
||||||
|
bobTopicOK := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "bob sets topic now")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobTopicOK, "bob sets topic now",
|
||||||
|
"bob can set topic after -t",
|
||||||
|
)
|
||||||
|
|
||||||
|
// alice sees the topic change.
|
||||||
|
aliceTopicRelay := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "bob sets topic now")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceTopicRelay, "bob sets topic now",
|
||||||
|
"alice sees bob's topic after -t",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Restore +t.
|
||||||
|
alice.send("MODE #integration +t")
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "+t")
|
||||||
|
})
|
||||||
|
bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "+t")
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── DM to nonexistent nick ────────────────────────
|
||||||
|
|
||||||
|
alice.send("PRIVMSG nobody123 :hello")
|
||||||
|
|
||||||
|
aliceNoSuch := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 401 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceNoSuch, " 401 ",
|
||||||
|
"ERR_NOSUCHNICK",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── PART with reason ──────────────────────────────
|
||||||
|
|
||||||
|
bob.send("PART #integration :bye for now")
|
||||||
|
|
||||||
|
bobPart := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "PART")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobPart, "PART",
|
||||||
|
"bob sees PART echo",
|
||||||
|
)
|
||||||
|
|
||||||
|
// alice sees bob PART via relay.
|
||||||
|
alicePart := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "PART") &&
|
||||||
|
strings.Contains(l, "bob")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, alicePart, "bob",
|
||||||
|
"alice sees bob's PART",
|
||||||
|
)
|
||||||
|
assertContains(
|
||||||
|
t, alicePart, "bye for now",
|
||||||
|
"PART reason is relayed",
|
||||||
|
)
|
||||||
|
|
||||||
|
// bob rejoins for remaining tests.
|
||||||
|
bob.joinAndDrain("#integration")
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "JOIN") &&
|
||||||
|
strings.Contains(l, "bob")
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── PART non-existent channel ─────────────────────
|
||||||
|
|
||||||
|
bob.send("PART #nonexistent")
|
||||||
|
|
||||||
|
bobPartFail := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 403 ") ||
|
||||||
|
strings.Contains(l, " 442 ")
|
||||||
|
})
|
||||||
|
|
||||||
|
foundPartErr := false
|
||||||
|
for _, line := range bobPartFail {
|
||||||
|
if strings.Contains(line, " 403 ") ||
|
||||||
|
strings.Contains(line, " 442 ") {
|
||||||
|
foundPartErr = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundPartErr {
|
||||||
|
t.Error(
|
||||||
|
"expected ERR_NOSUCHCHANNEL or " +
|
||||||
|
"ERR_NOTONCHANNEL",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── User MODE query ───────────────────────────────
|
||||||
|
|
||||||
|
alice.send("MODE alice")
|
||||||
|
|
||||||
|
aliceUMode := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 221 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceUMode, " 221 ",
|
||||||
|
"RPL_UMODEIS",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── Multiple channel operation ────────────────────
|
||||||
|
|
||||||
|
alice.send("JOIN #second")
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 366 ")
|
||||||
|
})
|
||||||
|
|
||||||
|
bob.send("JOIN #second")
|
||||||
|
bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 366 ")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Drain alice seeing bob join.
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "JOIN") &&
|
||||||
|
strings.Contains(l, "bob")
|
||||||
|
})
|
||||||
|
|
||||||
|
alice.send("PRIVMSG #second :cross-channel test")
|
||||||
|
|
||||||
|
bobCross := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "cross-channel test")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobCross, "cross-channel test",
|
||||||
|
"bob receives message in #second",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clean up #second.
|
||||||
|
alice.send("PART #second")
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "PART")
|
||||||
|
})
|
||||||
|
bob.send("PART #second")
|
||||||
|
bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "PART")
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── QUIT ──────────────────────────────────────────
|
||||||
|
|
||||||
|
bob.send("QUIT :integration test done")
|
||||||
|
|
||||||
|
bobQuit := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "ERROR")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobQuit, "integration test done",
|
||||||
|
"QUIT reason echoed",
|
||||||
|
)
|
||||||
|
|
||||||
|
// alice should see bob's QUIT via relay.
|
||||||
|
aliceQuit := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "QUIT") &&
|
||||||
|
strings.Contains(l, "bob")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceQuit, "bob",
|
||||||
|
"alice sees bob's QUIT",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIntegrationModeSecret tests +s (secret) channel
|
||||||
|
// mode — verifies that +s can be set and the mode is
|
||||||
|
// reflected in MODE queries.
|
||||||
|
func TestIntegrationModeSecret(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newTestEnv(t)
|
||||||
|
|
||||||
|
alice := env.dial(t)
|
||||||
|
alice.register("alice")
|
||||||
|
|
||||||
|
alice.joinAndDrain("#secretroom")
|
||||||
|
|
||||||
|
// Set +s.
|
||||||
|
alice.send("MODE #secretroom +s")
|
||||||
|
aliceLines := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "+s")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceLines, "+s",
|
||||||
|
"alice sees MODE +s confirmation",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify mode is reflected in query.
|
||||||
|
alice.send("MODE #secretroom")
|
||||||
|
modeLines := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 324 ")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, modeLines, "s",
|
||||||
|
"channel mode includes s",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIntegrationModeModerated tests +m (moderated) mode
|
||||||
|
// — non-voiced users cannot send.
|
||||||
|
func TestIntegrationModeModerated(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newTestEnv(t)
|
||||||
|
|
||||||
|
alice := env.dial(t)
|
||||||
|
alice.register("alice")
|
||||||
|
|
||||||
|
bob := env.dial(t)
|
||||||
|
bob.register("bob")
|
||||||
|
|
||||||
|
alice.joinAndDrain("#modtest")
|
||||||
|
bob.joinAndDrain("#modtest")
|
||||||
|
|
||||||
|
// Drain alice's view of bob's join.
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "JOIN") &&
|
||||||
|
strings.Contains(l, "bob")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set +m.
|
||||||
|
alice.send("MODE #modtest +m")
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "+m")
|
||||||
|
})
|
||||||
|
bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "+m")
|
||||||
|
})
|
||||||
|
|
||||||
|
// bob should get an error trying to send.
|
||||||
|
bob.send("PRIVMSG #modtest :should fail")
|
||||||
|
bobLines := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, " 404 ") ||
|
||||||
|
strings.Contains(l, " 482 ")
|
||||||
|
})
|
||||||
|
|
||||||
|
foundModErr := false
|
||||||
|
for _, line := range bobLines {
|
||||||
|
if strings.Contains(line, " 404 ") ||
|
||||||
|
strings.Contains(line, " 482 ") {
|
||||||
|
foundModErr = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundModErr {
|
||||||
|
t.Error(
|
||||||
|
"non-voiced user should not be able to send " +
|
||||||
|
"in +m channel",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grant +v to bob, then he should be able to send.
|
||||||
|
alice.send("MODE #modtest +v bob")
|
||||||
|
alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "+v")
|
||||||
|
})
|
||||||
|
bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "+v")
|
||||||
|
})
|
||||||
|
|
||||||
|
bob.send("PRIVMSG #modtest :voiced message")
|
||||||
|
aliceLines := alice.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "voiced message")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, aliceLines, "voiced message",
|
||||||
|
"alice receives voiced bob's message",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIntegrationThirdClientObserver verifies that a third
|
||||||
|
// client observing the same channel receives messages from
|
||||||
|
// the other two.
|
||||||
|
func TestIntegrationThirdClientObserver(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newTestEnv(t)
|
||||||
|
|
||||||
|
alice := env.dial(t)
|
||||||
|
alice.register("alice")
|
||||||
|
|
||||||
|
bob := env.dial(t)
|
||||||
|
bob.register("bob")
|
||||||
|
|
||||||
|
carol := env.dial(t)
|
||||||
|
carol.register("carol")
|
||||||
|
|
||||||
|
alice.joinAndDrain("#trio")
|
||||||
|
bob.joinAndDrain("#trio")
|
||||||
|
carol.joinAndDrain("#trio")
|
||||||
|
|
||||||
|
// Drain join notifications.
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// alice sends; both bob and carol should receive.
|
||||||
|
alice.send("PRIVMSG #trio :hello trio")
|
||||||
|
|
||||||
|
bobLines := bob.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "hello trio")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, bobLines, "hello trio",
|
||||||
|
"bob receives trio message",
|
||||||
|
)
|
||||||
|
|
||||||
|
carolLines := carol.readUntil(func(l string) bool {
|
||||||
|
return strings.Contains(l, "hello trio")
|
||||||
|
})
|
||||||
|
assertContains(
|
||||||
|
t, carolLines, "hello trio",
|
||||||
|
"carol receives trio message",
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user