check / check (push) Canceled after 0s
The IRC listener's JOIN now passes the channel key on, INVITE records the invite, LIST and WHOIS hide +s channels from non-members with the queries the HTTP API uses, and PRIVMSG to a +H channel gets 404. The INVITE checks and the channel hashcash check move from the HTTP handlers into the service, so both transports run the same code. An IRC client cannot attach a stamp, so its PRIVMSG to a +H channel always fails that check. The unfiltered channel list query, now unused, is removed. Model: opus-5-5
1522 lines
36 KiB
Go
1522 lines
36 KiB
Go
package ircserver_test
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"sneak.berlin/go/neoirc/internal/config"
|
||
)
|
||
|
||
// 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",
|
||
)
|
||
}
|
||
|
||
// ── Tier 3 Utility Command Integration Tests ──────────
|
||
|
||
// TestIntegrationUserhost verifies the USERHOST command
|
||
// returns user@host info for connected nicks.
|
||
func TestIntegrationUserhost(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
|
||
bob := env.dial(t)
|
||
bob.register("bob")
|
||
|
||
bob.send("AWAY :lunch")
|
||
bob.readUntil(func(l string) bool {
|
||
return strings.Contains(l, " 306 ")
|
||
})
|
||
|
||
for _, step := range []struct {
|
||
line, want string
|
||
}{
|
||
{"USERHOST alice", " 302 alice alice=+alice@"},
|
||
{"USERHOST alice bob", " :alice=+alice@"},
|
||
{"USERHOST alice bob", " bob=-bob@"},
|
||
{"USERHOST nobody", " 302 alice :"},
|
||
{"USERHOST", " 461 alice USERHOST :"},
|
||
} {
|
||
alice.send(step.line)
|
||
|
||
reply := alice.readUntil(func(l string) bool {
|
||
return strings.Contains(l, " 302 ") ||
|
||
strings.Contains(l, " 461 ")
|
||
})
|
||
assertContains(t, reply, step.want, step.line)
|
||
}
|
||
}
|
||
|
||
// TestIntegrationVersion verifies the VERSION command
|
||
// returns the server version string.
|
||
func TestIntegrationVersion(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
|
||
alice.send("VERSION")
|
||
|
||
aliceReply := alice.readUntil(func(l string) bool {
|
||
return strings.Contains(l, " 351 ")
|
||
})
|
||
assertContains(
|
||
t, aliceReply, " 351 ",
|
||
"RPL_VERSION",
|
||
)
|
||
assertContains(
|
||
t, aliceReply, "neoirc",
|
||
"VERSION reply contains server name",
|
||
)
|
||
}
|
||
|
||
// TestIntegrationAdmin verifies the ADMIN command returns
|
||
// server admin info (256–259 numerics).
|
||
func TestIntegrationAdmin(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
|
||
alice.send("ADMIN")
|
||
|
||
aliceReply := alice.readUntil(func(l string) bool {
|
||
return strings.Contains(l, " 259 ")
|
||
})
|
||
assertContains(
|
||
t, aliceReply, " 256 ",
|
||
"RPL_ADMINME",
|
||
)
|
||
assertContains(
|
||
t, aliceReply, " 257 ",
|
||
"RPL_ADMINLOC1",
|
||
)
|
||
assertContains(
|
||
t, aliceReply, " 258 ",
|
||
"RPL_ADMINLOC2",
|
||
)
|
||
assertContains(
|
||
t, aliceReply, " 259 ",
|
||
"RPL_ADMINEMAIL",
|
||
)
|
||
}
|
||
|
||
// TestIntegrationInfo verifies the INFO command returns
|
||
// server information (371/374 numerics).
|
||
func TestIntegrationInfo(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
|
||
alice.send("INFO")
|
||
|
||
aliceReply := alice.readUntil(func(l string) bool {
|
||
return strings.Contains(l, " 374 ")
|
||
})
|
||
assertContains(
|
||
t, aliceReply, " 371 ",
|
||
"RPL_INFO",
|
||
)
|
||
assertContains(
|
||
t, aliceReply, " 374 ",
|
||
"RPL_ENDOFINFO",
|
||
)
|
||
assertContains(
|
||
t, aliceReply, "neoirc",
|
||
"INFO reply mentions server name",
|
||
)
|
||
}
|
||
|
||
// TestIntegrationTime verifies the TIME command returns
|
||
// the server time (391 numeric).
|
||
func TestIntegrationTime(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
|
||
alice.send("TIME")
|
||
|
||
aliceReply := alice.readUntil(func(l string) bool {
|
||
return strings.Contains(l, " 391 ")
|
||
})
|
||
assertContains(
|
||
t, aliceReply, " 391 ",
|
||
"RPL_TIME",
|
||
)
|
||
assertContains(
|
||
t, aliceReply, testServerName,
|
||
"TIME reply includes server name",
|
||
)
|
||
}
|
||
|
||
// becomeOper sends OPER with newTestEnvWithOper's
|
||
// credentials and waits for RPL_YOUREOPER.
|
||
func (tc *testClient) becomeOper() {
|
||
tc.t.Helper()
|
||
|
||
tc.send("OPER testoper testpass")
|
||
tc.readUntil(func(l string) bool {
|
||
return strings.Contains(l, " 381 ")
|
||
})
|
||
}
|
||
|
||
// TestIntegrationKillRefused covers the KILL errors: not an
|
||
// operator, no such nick, and killing yourself.
|
||
func TestIntegrationKillRefused(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnvWithOper(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
|
||
bob := env.dial(t)
|
||
bob.register("bob")
|
||
|
||
for _, step := range []struct {
|
||
line, numeric string
|
||
}{
|
||
{"KILL bob :nope", " 481 "},
|
||
{"OPER testoper testpass", " 381 "},
|
||
{"KILL nobody123 :gone", " 401 "},
|
||
{"KILL alice :me", " 483 "},
|
||
} {
|
||
alice.send(step.line)
|
||
|
||
reply := alice.readUntil(func(l string) bool {
|
||
return strings.Contains(l, step.numeric)
|
||
})
|
||
assertContains(t, reply, step.numeric, step.line)
|
||
}
|
||
}
|
||
|
||
// TestIntegrationKill checks that the victim of a KILL is
|
||
// told why, is disconnected, and is gone from its channels.
|
||
func TestIntegrationKill(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnvWithOper(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
|
||
bob := env.dial(t)
|
||
bob.register("bob")
|
||
|
||
alice.joinAndDrain("#killtest")
|
||
bob.joinAndDrain("#killtest")
|
||
|
||
// Drain alice's view of bob's join.
|
||
alice.readUntil(func(l string) bool {
|
||
return strings.Contains(l, "JOIN") &&
|
||
strings.Contains(l, "bob")
|
||
})
|
||
|
||
alice.becomeOper()
|
||
alice.send("KILL bob :bad behavior")
|
||
|
||
bobLines := bob.readUntilClosed()
|
||
assertContains(
|
||
t, bobLines, "KILL",
|
||
"victim receives KILL before disconnect",
|
||
)
|
||
assertContains(
|
||
t, bobLines, "ERROR :Closing Link",
|
||
"victim receives ERROR before disconnect",
|
||
)
|
||
assertContains(
|
||
t, bobLines, "bad behavior",
|
||
"KILL reason delivered to victim",
|
||
)
|
||
|
||
// alice should see bob's QUIT relay.
|
||
aliceSeesQuit := alice.readUntil(func(l string) bool {
|
||
return strings.Contains(l, "QUIT") &&
|
||
strings.Contains(l, "bob")
|
||
})
|
||
assertContains(
|
||
t, aliceSeesQuit, "Killed",
|
||
"KILL reason in QUIT message",
|
||
)
|
||
|
||
// bob must be gone from the channel member list.
|
||
alice.send("NAMES #killtest")
|
||
|
||
aliceNames := alice.readUntil(func(l string) bool {
|
||
return strings.Contains(l, " 366 ")
|
||
})
|
||
assertContains(
|
||
t, aliceNames, "alice",
|
||
"alice still in NAMES after killing bob",
|
||
)
|
||
assertNotContains(
|
||
t, aliceNames, "bob",
|
||
"killed user must not appear in NAMES",
|
||
)
|
||
|
||
// ...nor from WHO.
|
||
alice.send("WHO #killtest")
|
||
|
||
aliceWho := alice.readUntil(func(l string) bool {
|
||
return strings.Contains(l, " 315 ")
|
||
})
|
||
assertNotContains(
|
||
t, aliceWho, "bob",
|
||
"killed user must not appear in WHO",
|
||
)
|
||
}
|
||
|
||
// TestIntegrationWallops verifies the WALLOPS command:
|
||
// oper can broadcast to +w users.
|
||
func TestIntegrationWallops(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnvWithOper(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
|
||
bob := env.dial(t)
|
||
bob.register("bob")
|
||
|
||
// Non-oper WALLOPS should fail.
|
||
alice.send("WALLOPS :test broadcast")
|
||
|
||
aliceWallopsFail := alice.readUntil(func(l string) bool {
|
||
return strings.Contains(l, " 481 ")
|
||
})
|
||
assertContains(
|
||
t, aliceWallopsFail, " 481 ",
|
||
"ERR_NOPRIVILEGES for non-oper WALLOPS",
|
||
)
|
||
|
||
alice.becomeOper()
|
||
|
||
// bob sets +w to receive wallops.
|
||
bob.send("MODE bob +w")
|
||
bob.readUntil(func(l string) bool {
|
||
return strings.Contains(l, " 221 ")
|
||
})
|
||
|
||
// alice sends WALLOPS.
|
||
alice.send("WALLOPS :important announcement")
|
||
|
||
// bob (who has +w) should receive it.
|
||
bobWallops := bob.readUntil(func(l string) bool {
|
||
return strings.Contains(
|
||
l, "important announcement",
|
||
)
|
||
})
|
||
assertContains(
|
||
t, bobWallops, "important announcement",
|
||
"bob receives WALLOPS message",
|
||
)
|
||
assertContains(
|
||
t, bobWallops, "WALLOPS",
|
||
"message is WALLOPS command",
|
||
)
|
||
}
|
||
|
||
// TestIntegrationUserMode covers MODE for a nick: changes
|
||
// and queries of your own modes in any letter case, refusal
|
||
// for another nick, and a rejected mode string changing
|
||
// nothing.
|
||
func TestIntegrationUserMode(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
|
||
bob := env.dial(t)
|
||
bob.register("bob")
|
||
|
||
const (
|
||
notYours = " 502 alice :Can't change mode for other users"
|
||
unknown = " 501 alice :Unknown MODE flag"
|
||
)
|
||
|
||
for _, step := range []struct {
|
||
line, want string
|
||
}{
|
||
{"MODE ALICE +w", " 221 alice +w"},
|
||
{"MODE bob", notYours},
|
||
{"MODE bob -w", notYours},
|
||
{"MODE alice xw", unknown},
|
||
{"MODE alice -w+z", unknown},
|
||
{"MODE alice", " 221 alice +w"},
|
||
{"MODE alice +w-w", " 221 alice +"},
|
||
} {
|
||
alice.send(step.line)
|
||
|
||
reply := alice.readUntil(func(l string) bool {
|
||
return strings.Contains(l, " 221 ") ||
|
||
strings.Contains(l, " 501 ") ||
|
||
strings.Contains(l, " 502 ")
|
||
})
|
||
|
||
last := reply[len(reply)-1]
|
||
if !strings.HasSuffix(last, step.want) {
|
||
t.Errorf("%s: want %q, got %q", step.line, step.want, last)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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",
|
||
)
|
||
}
|
||
|
||
// TestIntegrationModeKey checks that JOIN takes the key of
|
||
// a +k channel as its second parameter, and that with
|
||
// several channels each key goes to the channel in the
|
||
// same position.
|
||
func TestIntegrationModeKey(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
alice.joinAndDrain("#keyed")
|
||
alice.joinAndDrain("#vault")
|
||
|
||
// The IRC listener cannot set +k, so the test sets the
|
||
// keys directly.
|
||
for channel, key := range map[string]string{
|
||
"#keyed": "sesame",
|
||
"#vault": "barley",
|
||
} {
|
||
chID, err := env.database.GetChannelByName(
|
||
t.Context(), channel,
|
||
)
|
||
if err != nil {
|
||
t.Fatalf("get channel %s: %v", channel, err)
|
||
}
|
||
|
||
err = env.database.SetChannelKey(t.Context(), chID, key)
|
||
if err != nil {
|
||
t.Fatalf("set key of %s: %v", channel, err)
|
||
}
|
||
}
|
||
|
||
bob := env.dial(t)
|
||
bob.register("bob")
|
||
|
||
bob.sendAndExpect("JOIN #keyed", " 475 ")
|
||
bob.sendAndExpect("JOIN #keyed wrong", " 475 ")
|
||
|
||
lines := bob.sendAndExpect("JOIN #keyed sesame", " 366 ")
|
||
assertContains(
|
||
t, lines, "JOIN #keyed", "bob joins with the key",
|
||
)
|
||
|
||
carol := env.dial(t)
|
||
carol.register("carol")
|
||
|
||
lines = carol.sendAndExpect(
|
||
"JOIN #keyed,#vault sesame,barley", " 366 carol #vault ",
|
||
)
|
||
assertContains(t, lines, "JOIN #keyed", "carol joins #keyed")
|
||
assertContains(t, lines, "JOIN #vault", "carol joins #vault")
|
||
assertNotContains(t, lines, " 475 ", "carol's JOIN")
|
||
}
|
||
|
||
// TestIntegrationModeInviteOnly checks that INVITE lets a
|
||
// user into a +i channel.
|
||
func TestIntegrationModeInviteOnly(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
alice.joinAndDrain("#invonly")
|
||
alice.sendAndExpect("MODE #invonly +i", "+i")
|
||
|
||
bob := env.dial(t)
|
||
bob.register("bob")
|
||
|
||
bob.sendAndExpect("JOIN #invonly", " 473 ")
|
||
|
||
alice.sendAndExpect("INVITE bob #invonly", " 341 ")
|
||
|
||
lines := bob.sendAndExpect("JOIN #invonly", " 366 ")
|
||
assertContains(
|
||
t, lines, "JOIN #invonly", "bob joins once invited",
|
||
)
|
||
}
|
||
|
||
// TestIntegrationModeInviteOnlyNeedsOperator checks that on
|
||
// a +i channel an INVITE from a member who is not a channel
|
||
// operator gets 482 and does not let the invited user in.
|
||
func TestIntegrationModeInviteOnlyNeedsOperator(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
alice.joinAndDrain("#invonly")
|
||
|
||
// Bob joins before +i is set, so he is a member but
|
||
// not an operator.
|
||
bob := env.dial(t)
|
||
bob.register("bob")
|
||
bob.joinAndDrain("#invonly")
|
||
|
||
alice.sendAndExpect("MODE #invonly +i", "+i")
|
||
|
||
carol := env.dial(t)
|
||
carol.register("carol")
|
||
|
||
lines := bob.sendAndExpect("INVITE carol #invonly", " 482 ")
|
||
assertContains(
|
||
t, lines, " 482 bob #invonly ", "bob's INVITE refused",
|
||
)
|
||
|
||
carol.sendAndExpect("JOIN #invonly", " 473 ")
|
||
}
|
||
|
||
// TestIntegrationInviteUserOnChannel checks that inviting a
|
||
// user who is already on the channel gets 443.
|
||
func TestIntegrationInviteUserOnChannel(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
alice.joinAndDrain("#room")
|
||
|
||
bob := env.dial(t)
|
||
bob.register("bob")
|
||
bob.joinAndDrain("#room")
|
||
|
||
lines := alice.sendAndExpect("INVITE bob #room", " 443 ")
|
||
assertContains(
|
||
t, lines, " 443 alice bob #room ", "alice's INVITE refused",
|
||
)
|
||
}
|
||
|
||
// TestIntegrationModeSecretList checks that LIST shows a +s
|
||
// channel only to its members.
|
||
func TestIntegrationModeSecretList(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
alice.joinAndDrain("#hidden")
|
||
alice.joinAndDrain("#visible")
|
||
alice.sendAndExpect("MODE #hidden +s", "+s")
|
||
|
||
bob := env.dial(t)
|
||
bob.register("bob")
|
||
|
||
bobList := bob.sendAndExpect("LIST", " 323 ")
|
||
assertContains(
|
||
t, bobList, " 322 bob #visible ", "bob's LIST",
|
||
)
|
||
assertNotContains(t, bobList, "#hidden", "bob's LIST")
|
||
|
||
aliceList := alice.sendAndExpect("LIST", " 323 ")
|
||
assertContains(
|
||
t, aliceList, " 322 alice #hidden ", "alice's LIST",
|
||
)
|
||
}
|
||
|
||
// TestIntegrationModeSecretWhois checks that WHOIS names a
|
||
// +s channel only to users on it.
|
||
func TestIntegrationModeSecretWhois(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
alice.joinAndDrain("#hidden")
|
||
alice.joinAndDrain("#visible")
|
||
alice.sendAndExpect("MODE #hidden +s", "+s")
|
||
|
||
bob := env.dial(t)
|
||
bob.register("bob")
|
||
|
||
bobWhois := bob.sendAndExpect("WHOIS alice", " 318 ")
|
||
assertContains(t, bobWhois, "@#visible", "bob's WHOIS")
|
||
assertNotContains(t, bobWhois, "#hidden", "bob's WHOIS")
|
||
|
||
carol := env.dial(t)
|
||
carol.register("carol")
|
||
carol.joinAndDrain("#hidden")
|
||
|
||
carolWhois := carol.sendAndExpect("WHOIS alice", " 318 ")
|
||
assertContains(t, carolWhois, "@#hidden", "carol's WHOIS")
|
||
}
|
||
|
||
// TestIntegrationModeHashcash checks that PRIVMSG to a +H
|
||
// channel is refused, since an IRC client cannot attach a
|
||
// hashcash stamp.
|
||
func TestIntegrationModeHashcash(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
alice.joinAndDrain("#stamped")
|
||
alice.sendAndExpect("MODE #stamped +H 2", "+H")
|
||
|
||
lines := alice.sendAndExpect(
|
||
"PRIVMSG #stamped :hello", " 404 ",
|
||
)
|
||
assertContains(
|
||
t, lines, " 404 alice #stamped :Channel requires hashcash",
|
||
"PRIVMSG refused",
|
||
)
|
||
}
|
||
|
||
// 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",
|
||
)
|
||
}
|
||
|
||
// TestIntegrationDefaultServerNameFallback checks that with
|
||
// SERVER_NAME unset, as it is by default, VERSION, ADMIN and
|
||
// TIME name the server "neoirc".
|
||
func TestIntegrationDefaultServerNameFallback(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnvWithConfig(t, &config.Config{})
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
|
||
for _, step := range []struct {
|
||
line, lastNumeric, want string
|
||
}{
|
||
{"VERSION", " 351 ", " 351 alice neoirc-test. neoirc "},
|
||
{"ADMIN", " 259 ", " 256 alice neoirc "},
|
||
{"TIME", " 391 ", " 391 alice neoirc "},
|
||
} {
|
||
alice.send(step.line)
|
||
|
||
reply := alice.readUntil(func(l string) bool {
|
||
return strings.Contains(l, step.lastNumeric)
|
||
})
|
||
assertContains(t, reply, step.want, step.line)
|
||
}
|
||
}
|