Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce3a989321 | ||
|
|
cc18397ee1 |
@@ -1236,7 +1236,7 @@ IRC listener sends the usual 3-digit code. Over the HTTP API, 005 arrives named
|
||||
| `001` | RPL_WELCOME | After session creation or login, on MOTD | `{"command":"RPL_WELCOME","code":1,"to":"alice","body":["Welcome to the network, alice"]}` |
|
||||
| `002` | RPL_YOURHOST | After session creation or login, on MOTD | `{"command":"RPL_YOURHOST","code":2,"to":"alice","body":["Your host is neoirc, running version 0.1"]}` |
|
||||
| `003` | RPL_CREATED | After session creation or login, on MOTD | `{"command":"RPL_CREATED","code":3,"to":"alice","body":["This server was created 2026-02-10"]}` |
|
||||
| `004` | RPL_MYINFO | After session creation or login, on MOTD | `{"command":"RPL_MYINFO","code":4,"to":"alice","params":["neoirc","0.1","","ikmnostl"]}` |
|
||||
| `004` | RPL_MYINFO | After session creation or login, on MOTD | `{"command":"RPL_MYINFO","code":4,"to":"alice","params":["neoirc","0.1","ow","ikmnostl"]}` |
|
||||
| `005` | RPL_ISUPPORT | After session creation or login, on MOTD | `{"command":"RPL_BOUNCE","code":5,"to":"alice","params":["CHANTYPES=#","NICKLEN=32","PREFIX=(ov)@+","CHANMODES=b,k,Hl,imnst","NETWORK=neoirc","CASEMAPPING=ascii"],"body":["are supported by this server"]}` |
|
||||
| `221` | RPL_UMODEIS | In response to user MODE | `{"command":"RPL_UMODEIS","code":221,"to":"alice","body":["+"]}` |
|
||||
| `251` | RPL_LUSERCLIENT | On connect, MOTD or LUSERS | `{"command":"RPL_LUSERCLIENT","code":251,"to":"alice","body":["There are 5 users and 0 invisible on 1 servers"]}` |
|
||||
|
||||
@@ -434,45 +434,14 @@ func TestSecondConnectStopsFirstPollLoop(t *testing.T) {
|
||||
// The first server answers every request at once, so the first
|
||||
// poll loop polls it until the second /connect.
|
||||
first := newServer(t, session)
|
||||
|
||||
// The second server sends on polled for each poll and holds the
|
||||
// poll until its connection closes. It answers anything else
|
||||
// with the session.
|
||||
polled := make(chan struct{})
|
||||
second := httptest.NewServer(http.HandlerFunc(
|
||||
func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.Method != http.MethodGet ||
|
||||
request.URL.Path != "/api/v1/messages" {
|
||||
_, _ = io.WriteString(writer, session)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case polled <- struct{}{}:
|
||||
case <-request.Context().Done():
|
||||
}
|
||||
|
||||
<-request.Context().Done()
|
||||
},
|
||||
))
|
||||
// Close waits for the held polls, which end only once their
|
||||
// connections close. Cleanups run last first, so the connections
|
||||
// close before Close runs.
|
||||
t.Cleanup(second.Close)
|
||||
t.Cleanup(second.CloseClientConnections)
|
||||
second, polled := newHoldingServer(t, session)
|
||||
|
||||
app := &App{ui: NewUI(), nick: testNick}
|
||||
runUI(t, app.ui)
|
||||
|
||||
app.cmdConnect(first.URL)
|
||||
app.cmdConnect(second.URL)
|
||||
|
||||
select {
|
||||
case <-polled:
|
||||
case <-time.After(eventLoopTimeout):
|
||||
t.Fatalf("the second server got no poll within %v", eventLoopTimeout)
|
||||
}
|
||||
waitPoll(t, polled, "the second server")
|
||||
|
||||
// The second connection's poll loop waits on its held poll, so
|
||||
// another poll comes from the first loop.
|
||||
@@ -582,6 +551,63 @@ func TestSecondConnectIgnoresHeldPoll(t *testing.T) {
|
||||
app.mu.Unlock()
|
||||
}
|
||||
|
||||
// TestSecondConnectAfterFailedPoll holds a poll of the earlier
|
||||
// connection at its server while a second /connect runs, then
|
||||
// fails it. A poll loop waits pollRetry after a failed poll, then
|
||||
// polls again. The test fails if the earlier poll loop polls the
|
||||
// second server.
|
||||
func TestSecondConnectAfterFailedPoll(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const session = `{"id": 1, "nick": "erin"}`
|
||||
|
||||
first, firstPolled := newHoldingServer(t, session)
|
||||
|
||||
// The second server holds the one poll of the second
|
||||
// connection's poll loop, so any other poll comes from the
|
||||
// earlier loop.
|
||||
second, secondPolled := newHoldingServer(t, session)
|
||||
|
||||
// The test runs the earlier connection's poll loop itself, so
|
||||
// that it can wait for the loop to end.
|
||||
stop := make(chan struct{})
|
||||
app := &App{
|
||||
ui: NewUI(),
|
||||
client: api.NewClient(first.URL),
|
||||
nick: testNick,
|
||||
connected: true,
|
||||
stopPoll: stop,
|
||||
}
|
||||
runUI(t, app.ui)
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
app.pollLoop(stop)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
waitPoll(t, firstPolled, "the first server")
|
||||
app.cmdConnect(second.URL)
|
||||
waitPoll(t, secondPolled, "the second server")
|
||||
|
||||
// Closing the held poll's connection makes that poll fail.
|
||||
first.CloseClientConnections()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-secondPolled:
|
||||
t.Fatal("the earlier poll loop polled the second server")
|
||||
case <-time.After(eventLoopTimeout):
|
||||
t.Fatalf("the earlier poll loop did not end within %v", eventLoopTimeout)
|
||||
}
|
||||
|
||||
// Stop the second poll loop as /quit does, without stopping the UI.
|
||||
app.mu.Lock()
|
||||
close(app.stopPoll)
|
||||
app.mu.Unlock()
|
||||
}
|
||||
|
||||
// TestStartUp runs the client's start-up path on a simulation
|
||||
// screen. It fails unless the event loop starts and the status
|
||||
// window gets two lines: first the one that holds "Welcome to
|
||||
@@ -866,6 +892,53 @@ func newServer(t *testing.T, body string) *httptest.Server {
|
||||
return server
|
||||
}
|
||||
|
||||
// newHoldingServer starts a server that sends on the returned
|
||||
// channel for each poll and holds the poll until its connection
|
||||
// closes. It answers any other request with body.
|
||||
func newHoldingServer(
|
||||
t *testing.T, body string,
|
||||
) (*httptest.Server, <-chan struct{}) {
|
||||
t.Helper()
|
||||
|
||||
polled := make(chan struct{})
|
||||
server := httptest.NewServer(http.HandlerFunc(
|
||||
func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.Method != http.MethodGet ||
|
||||
request.URL.Path != "/api/v1/messages" {
|
||||
_, _ = io.WriteString(writer, body)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case polled <- struct{}{}:
|
||||
case <-request.Context().Done():
|
||||
}
|
||||
|
||||
<-request.Context().Done()
|
||||
},
|
||||
))
|
||||
// Close waits for the held polls, which end only once their
|
||||
// connections close. Cleanups run last first, so the connections
|
||||
// close before Close runs.
|
||||
t.Cleanup(server.Close)
|
||||
t.Cleanup(server.CloseClientConnections)
|
||||
|
||||
return server, polled
|
||||
}
|
||||
|
||||
// waitPoll fails the test unless server gets a poll, sent on
|
||||
// polled, within eventLoopTimeout.
|
||||
func waitPoll(t *testing.T, polled <-chan struct{}, server string) {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case <-polled:
|
||||
case <-time.After(eventLoopTimeout):
|
||||
t.Fatalf("%s got no poll within %v", server, eventLoopTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// runUI runs ui on a simulated screen until the test ends.
|
||||
func runUI(t *testing.T, ui *UI) {
|
||||
t.Helper()
|
||||
|
||||
@@ -412,7 +412,7 @@ func (hdlr *Handlers) deliverWelcome(
|
||||
// 004 RPL_MYINFO
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, irc.RplMyInfo, nick,
|
||||
[]string{srvName, version, "", "ikmnostl"},
|
||||
[]string{srvName, version, service.UserModes, "ikmnostl"},
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
@@ -6797,6 +6797,30 @@ func TestISUPPORT(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMyInfoListsUserModes verifies the 004 numeric lists
|
||||
// the user modes o and w as its third parameter.
|
||||
func TestMyInfoListsUserModes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("myinfo")
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, 0)
|
||||
|
||||
myInfo := findNumericWithParams(msgs, "004")
|
||||
if myInfo == nil {
|
||||
t.Fatal("expected 004 RPL_MYINFO")
|
||||
}
|
||||
|
||||
params := getNumericParams(myInfo)
|
||||
if len(params) != 4 || params[2] != "ow" {
|
||||
t.Fatalf(
|
||||
"004 params %v, want four with user modes ow third",
|
||||
params,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNonOpCannotSetModes verifies non-operators
|
||||
// cannot set +i, +s, +k, +l, +b.
|
||||
func TestNonOpCannotSetModes(t *testing.T) {
|
||||
|
||||
@@ -605,7 +605,7 @@ func (c *Conn) deliverWelcome() {
|
||||
)
|
||||
c.sendNumeric(
|
||||
irc.RplMyInfo,
|
||||
c.serverSfx, "neoirc", "", "mnst",
|
||||
c.serverSfx, "neoirc", service.UserModes, "mnst",
|
||||
)
|
||||
c.sendNumeric(
|
||||
irc.RplIsupport,
|
||||
|
||||
@@ -486,6 +486,22 @@ func TestWelcomeContainsNick(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMyInfoListsUserModes checks that RPL_MYINFO lists the
|
||||
// user modes o and w, with no empty parameter before the
|
||||
// channel modes.
|
||||
func TestMyInfoListsUserModes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
lines := env.dial(t).register("carol")
|
||||
|
||||
assertContains(
|
||||
t, lines,
|
||||
":test.irc 004 carol test.irc neoirc ow mnst",
|
||||
"RPL_MYINFO with user modes",
|
||||
)
|
||||
}
|
||||
|
||||
func TestPingPong(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -1169,6 +1169,12 @@ func (s *Service) ApplyUserMode(
|
||||
return s.QueryUserMode(ctx, sessionID)
|
||||
}
|
||||
|
||||
// UserModes lists the user modes the server supports, as
|
||||
// RPL_MYINFO sends them: o (server operator) and w
|
||||
// (receives WALLOPS). parseUserModeString must accept the
|
||||
// same letters.
|
||||
const UserModes = "ow"
|
||||
|
||||
// parseUserModeString parses a user mode string such as
|
||||
// "+w", "-o" or "+w-o" into the new value of each flag it
|
||||
// names, or nil for a flag it does not name; a later letter
|
||||
|
||||
+2
-2
@@ -144,8 +144,8 @@ Where a schema file and the HTTP API disagree, the HTTP API is right:
|
||||
with `ERR_NEEDMOREPARAMS` (`461`). A `body` of `[""]` clears the topic.
|
||||
- `003.json` describes the server's creation date. The HTTP API sends the date
|
||||
the server last started.
|
||||
- `004.json` has example mode lists. The HTTP API sends an empty string as the
|
||||
user modes and `ikmnostl` as the channel modes.
|
||||
- `004.json` has example mode lists. The HTTP API sends `ow` as the user modes
|
||||
and `ikmnostl` as the channel modes.
|
||||
- `322.json` says `LIST` gets one reply per channel. The server leaves out a
|
||||
secret channel, one with mode `+s`, unless the user is on it.
|
||||
- `332.json` says `RPL_TOPIC` answers a `TOPIC` query. The HTTP API has no such
|
||||
|
||||
Reference in New Issue
Block a user