All checks were successful
check / check (push) Successful in 57s
Adds integration_test.go with four test functions that exercise all major IRC features using real TCP connections: TestIntegrationTwoClients: sequential two-client test covering NICK/USER registration, JOIN, PRIVMSG (channel + DM), NOTICE (channel + DM), TOPIC (set/get/lock/unlock), MODE (query, +m, +v, -t/+t), NAMES, LIST, WHO, WHOIS (with channels), LUSERS, NICK change (with relay), duplicate NICK, KICK (with relay + reason), KICK non-op error, PING/PONG, unknown command, MOTD, AWAY (set/clear/RPL_AWAY on DM), PASS post-registration, PART (with reason + relay), PART non-existent channel, user MODE query, multi-channel messaging, and QUIT (with relay). TestIntegrationModeSecret: verifies +s mode can be set and is reflected in MODE queries. TestIntegrationModeModerated: verifies +m blocks non-voiced users and +v enables sending in moderated channels. TestIntegrationThirdClientObserver: verifies three-client channel message fanout. closes #97
914 lines
22 KiB
Go
914 lines
22 KiB
Go
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",
|
|
)
|
|
}
|