check / check (push) Failing after 4s
- Tests hash passwords at bcrypt.MinCost, set through the PasswordCost field of db.Params. TestPasswordCost checks the cost with and without it. - Test IRC servers check the queue only on broker notification; one test keeps the 100 ms fallback poll. - The stop-timeout test shortens the timeout through a test-only setter. - service and handlers tests build config.Config directly instead of through config.New, so they run in parallel. config exports DefaultTrustedProxies and ParseTrustedProxies for the handlers test config. - Repeated race trials run as parallel subtests. - Fixed sleeps become waits on the broker. - A new test covers the relay's failed queue read, which only the 100 ms poll had reached. Model: opus-5-5
1556 lines
37 KiB
Go
1556 lines
37 KiB
Go
package ircserver_test
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
|
||
"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, refusal for another nick,
|
||
// even one that differs from yours only in letter case, 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 Alice", notYours},
|
||
{"MODE Alice -w", notYours},
|
||
{"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",
|
||
)
|
||
}
|
||
|
||
// TestIntegrationInviteLine checks that an IRC listener user
|
||
// invited by an IRC listener user, and then over the HTTP
|
||
// API, gets an INVITE line from the inviter for the channel
|
||
// each time.
|
||
func TestIntegrationInviteLine(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
env := newTestEnv(t)
|
||
|
||
alice := env.dial(t)
|
||
alice.register("alice")
|
||
alice.joinAndDrain("#inv")
|
||
|
||
bob := env.dial(t)
|
||
bob.register("bob")
|
||
|
||
alice.sendAndExpect("INVITE bob #inv", " 341 ")
|
||
|
||
got := bob.readLine()
|
||
want := ":alice!alice@* INVITE bob #inv"
|
||
|
||
if got != want {
|
||
t.Errorf("from the IRC listener: got %q, want %q", got, want)
|
||
}
|
||
|
||
// The HTTP API queues an INVITE with the channel as its body.
|
||
env.queueMessage(t, "bob", "INVITE", "carol", "bob", []string{"#inv"})
|
||
|
||
got = bob.readLine()
|
||
want = ":carol!carol@* INVITE bob #inv"
|
||
|
||
if got != want {
|
||
t.Errorf("over the HTTP API: got %q, want %q", got, want)
|
||
}
|
||
}
|
||
|
||
// 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")
|
||
|
||
// 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)
|
||
}
|
||
}
|