// Tests for Tier 3 utility IRC commands: USERHOST, // VERSION, ADMIN, INFO, TIME, KILL, WALLOPS. // //nolint:paralleltest package handlers_test import ( "strings" "testing" ) // --- USERHOST --- func TestUserhostSingleNick(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("alice") _, lastID := tserver.pollMessages(token, 0) tserver.sendCommand(token, map[string]any{ commandKey: "USERHOST", bodyKey: []string{"alice"}, }) msgs, _ := tserver.pollMessages(token, lastID) // Expect 302 RPL_USERHOST. msg := findNumericWithParams(msgs, "302") if msg == nil { t.Fatalf( "expected RPL_USERHOST (302), got %v", msgs, ) } // Body should contain "alice" with the // nick=+user@host format. body := getNumericBody(msg) if !strings.Contains(body, "alice") { t.Fatalf( "expected body to contain 'alice', got %q", body, ) } // '+' means not away. if !strings.Contains(body, "=+") { t.Fatalf( "expected not-away prefix '=+', got %q", body, ) } } func TestUserhostMultipleNicks(t *testing.T) { tserver := newTestServer(t) token1 := tserver.createSession("bob") token2 := tserver.createSession("carol") _ = token2 _, lastID := tserver.pollMessages(token1, 0) tserver.sendCommand(token1, map[string]any{ commandKey: "USERHOST", bodyKey: []string{"bob", "carol"}, }) msgs, _ := tserver.pollMessages(token1, lastID) msg := findNumericWithParams(msgs, "302") if msg == nil { t.Fatalf( "expected RPL_USERHOST (302), got %v", msgs, ) } body := getNumericBody(msg) if !strings.Contains(body, "bob") { t.Fatalf( "expected body to contain 'bob', got %q", body, ) } if !strings.Contains(body, "carol") { t.Fatalf( "expected body to contain 'carol', got %q", body, ) } } func TestUserhostNonexistentNick(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("dave") _, lastID := tserver.pollMessages(token, 0) tserver.sendCommand(token, map[string]any{ commandKey: "USERHOST", bodyKey: []string{"nobody"}, }) msgs, _ := tserver.pollMessages(token, lastID) // Should still get 302 but with empty body. msg := findNumericWithParams(msgs, "302") if msg == nil { t.Fatalf( "expected RPL_USERHOST (302), got %v", msgs, ) } } func TestUserhostNoParams(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("eve") _, lastID := tserver.pollMessages(token, 0) tserver.sendCommand(token, map[string]any{ commandKey: "USERHOST", }) msgs, _ := tserver.pollMessages(token, lastID) // Expect 461 ERR_NEEDMOREPARAMS. if !findNumeric(msgs, "461") { t.Fatalf( "expected ERR_NEEDMOREPARAMS (461), got %v", msgs, ) } } func TestUserhostShowsOper(t *testing.T) { tserver := newTestServerWithOper(t) token := tserver.createSession("opernick") _, lastID := tserver.pollMessages(token, 0) // Authenticate as oper. tserver.sendCommand(token, map[string]any{ commandKey: "OPER", bodyKey: []string{testOperName, testOperPassword}, }) _, lastID = tserver.pollMessages(token, lastID) // USERHOST should show '*' for oper. tserver.sendCommand(token, map[string]any{ commandKey: "USERHOST", bodyKey: []string{"opernick"}, }) msgs, _ := tserver.pollMessages(token, lastID) msg := findNumericWithParams(msgs, "302") if msg == nil { t.Fatalf( "expected RPL_USERHOST (302), got %v", msgs, ) } body := getNumericBody(msg) if !strings.Contains(body, "opernick*=") { t.Fatalf( "expected oper '*' in reply, got %q", body, ) } } func TestUserhostShowsAway(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("awaynick") _, lastID := tserver.pollMessages(token, 0) // Set away. tserver.sendCommand(token, map[string]any{ commandKey: "AWAY", bodyKey: []string{"gone fishing"}, }) _, lastID = tserver.pollMessages(token, lastID) // USERHOST should show '-' for away. tserver.sendCommand(token, map[string]any{ commandKey: "USERHOST", bodyKey: []string{"awaynick"}, }) msgs, _ := tserver.pollMessages(token, lastID) msg := findNumericWithParams(msgs, "302") if msg == nil { t.Fatalf( "expected RPL_USERHOST (302), got %v", msgs, ) } body := getNumericBody(msg) if !strings.Contains(body, "=-") { t.Fatalf( "expected away prefix '=-' in reply, got %q", body, ) } } // --- VERSION --- func TestVersion(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("frank") _, lastID := tserver.pollMessages(token, 0) tserver.sendCommand(token, map[string]any{ commandKey: "VERSION", }) msgs, _ := tserver.pollMessages(token, lastID) // Expect 351 RPL_VERSION. msg := findNumericWithParams(msgs, "351") if msg == nil { t.Fatalf( "expected RPL_VERSION (351), got %v", msgs, ) } params := getNumericParams(msg) if len(params) == 0 { t.Fatal("expected VERSION params, got none") } // First param should contain version string. if !strings.Contains(params[0], "test") { t.Fatalf( "expected version to contain 'test', got %q", params[0], ) } } // --- ADMIN --- func TestAdmin(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("grace") _, lastID := tserver.pollMessages(token, 0) tserver.sendCommand(token, map[string]any{ commandKey: "ADMIN", }) msgs, _ := tserver.pollMessages(token, lastID) // Expect 256 RPL_ADMINME. if !findNumeric(msgs, "256") { t.Fatalf( "expected RPL_ADMINME (256), got %v", msgs, ) } // Expect 257 RPL_ADMINLOC1. if !findNumeric(msgs, "257") { t.Fatalf( "expected RPL_ADMINLOC1 (257), got %v", msgs, ) } // Expect 258 RPL_ADMINLOC2. if !findNumeric(msgs, "258") { t.Fatalf( "expected RPL_ADMINLOC2 (258), got %v", msgs, ) } // Expect 259 RPL_ADMINEMAIL. if !findNumeric(msgs, "259") { t.Fatalf( "expected RPL_ADMINEMAIL (259), got %v", msgs, ) } } // --- INFO --- func TestInfo(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("hank") _, lastID := tserver.pollMessages(token, 0) tserver.sendCommand(token, map[string]any{ commandKey: "INFO", }) msgs, _ := tserver.pollMessages(token, lastID) // Expect 371 RPL_INFO (at least one). if !findNumeric(msgs, "371") { t.Fatalf( "expected RPL_INFO (371), got %v", msgs, ) } // Expect 374 RPL_ENDOFINFO. if !findNumeric(msgs, "374") { t.Fatalf( "expected RPL_ENDOFINFO (374), got %v", msgs, ) } } // --- TIME --- func TestTime(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("iris") _, lastID := tserver.pollMessages(token, 0) tserver.sendCommand(token, map[string]any{ commandKey: "TIME", }) msgs, _ := tserver.pollMessages(token, lastID) // Expect 391 RPL_TIME. msg := findNumericWithParams(msgs, "391") if msg == nil { t.Fatalf( "expected RPL_TIME (391), got %v", msgs, ) } } // --- KILL --- func TestKillSuccess(t *testing.T) { tserver := newTestServerWithOper(t) // Create the victim first. victimToken := tserver.createSession("victim") _ = victimToken // Create oper user. operToken := tserver.createSession("killer") _, lastID := tserver.pollMessages(operToken, 0) // Authenticate as oper. tserver.sendCommand(operToken, map[string]any{ commandKey: "OPER", bodyKey: []string{testOperName, testOperPassword}, }) _, lastID = tserver.pollMessages(operToken, lastID) // Kill the victim. status, result := tserver.sendCommand( operToken, map[string]any{ commandKey: "KILL", bodyKey: []string{"victim", "go away"}, }, ) if status != 200 { t.Fatalf("expected 200, got %d: %v", status, result) } resultStatus, _ := result[statusKey].(string) if resultStatus != "ok" { t.Fatalf( "expected status ok, got %v", result, ) } // Verify the victim's session is gone by trying // to WHOIS them. tserver.sendCommand(operToken, map[string]any{ commandKey: "WHOIS", toKey: "victim", }) msgs, _ := tserver.pollMessages(operToken, lastID) // Should get 401 ERR_NOSUCHNICK. if !findNumeric(msgs, "401") { t.Fatalf( "expected victim to be gone (401), got %v", msgs, ) } } func TestKillNotOper(t *testing.T) { tserver := newTestServer(t) _ = tserver.createSession("target") token := tserver.createSession("notoper") _, lastID := tserver.pollMessages(token, 0) tserver.sendCommand(token, map[string]any{ commandKey: "KILL", bodyKey: []string{"target", "no reason"}, }) msgs, _ := tserver.pollMessages(token, lastID) // Expect 481 ERR_NOPRIVILEGES. if !findNumeric(msgs, "481") { t.Fatalf( "expected ERR_NOPRIVILEGES (481), got %v", msgs, ) } } func TestKillNoParams(t *testing.T) { tserver := newTestServerWithOper(t) token := tserver.createSession("opertest") _, lastID := tserver.pollMessages(token, 0) tserver.sendCommand(token, map[string]any{ commandKey: "OPER", bodyKey: []string{testOperName, testOperPassword}, }) _, lastID = tserver.pollMessages(token, lastID) tserver.sendCommand(token, map[string]any{ commandKey: "KILL", }) msgs, _ := tserver.pollMessages(token, lastID) // Expect 461 ERR_NEEDMOREPARAMS. if !findNumeric(msgs, "461") { t.Fatalf( "expected ERR_NEEDMOREPARAMS (461), got %v", msgs, ) } } // sendOperKillCommand is a helper that creates an oper // session, authenticates, then sends KILL with the given // target nick, and returns the resulting messages. func sendOperKillCommand( t *testing.T, tserver *testServer, operNick, targetNick string, ) []map[string]any { t.Helper() token := tserver.createSession(operNick) _, lastID := tserver.pollMessages(token, 0) tserver.sendCommand(token, map[string]any{ commandKey: "OPER", bodyKey: []string{testOperName, testOperPassword}, }) _, lastID = tserver.pollMessages(token, lastID) tserver.sendCommand(token, map[string]any{ commandKey: "KILL", bodyKey: []string{targetNick}, }) msgs, _ := tserver.pollMessages(token, lastID) return msgs } func TestKillNonexistentUser(t *testing.T) { tserver := newTestServerWithOper(t) msgs := sendOperKillCommand( t, tserver, "opertest2", "ghost", ) // Expect 401 ERR_NOSUCHNICK. if !findNumeric(msgs, "401") { t.Fatalf( "expected ERR_NOSUCHNICK (401), got %v", msgs, ) } } func TestKillSelf(t *testing.T) { tserver := newTestServerWithOper(t) msgs := sendOperKillCommand( t, tserver, "selfkiller", "selfkiller", ) // Expect 483 ERR_CANTKILLSERVER. if !findNumeric(msgs, "483") { t.Fatalf( "expected ERR_CANTKILLSERVER (483), got %v", msgs, ) } } func TestKillBroadcastsQuit(t *testing.T) { tserver := newTestServerWithOper(t) // Create victim and join a channel. victimToken := tserver.createSession("vuser") tserver.sendCommand(victimToken, map[string]any{ commandKey: joinCmd, toKey: "#killtest", }) // Create observer and join same channel. observerToken := tserver.createSession("observer") tserver.sendCommand(observerToken, map[string]any{ commandKey: joinCmd, toKey: "#killtest", }) _, lastObs := tserver.pollMessages(observerToken, 0) // Create oper. operToken := tserver.createSession("theoper2") tserver.pollMessages(operToken, 0) tserver.sendCommand(operToken, map[string]any{ commandKey: "OPER", bodyKey: []string{testOperName, testOperPassword}, }) tserver.pollMessages(operToken, 0) // Kill the victim. tserver.sendCommand(operToken, map[string]any{ commandKey: "KILL", bodyKey: []string{"vuser", "testing kill"}, }) // Observer should see a QUIT message. msgs, _ := tserver.pollMessages(observerToken, lastObs) foundQuit := false for _, msg := range msgs { cmd, _ := msg["command"].(string) if cmd == "QUIT" { from, _ := msg["from"].(string) if from == "vuser" { foundQuit = true break } } } if !foundQuit { t.Fatalf( "expected QUIT from vuser, got %v", msgs, ) } } // --- WALLOPS --- func TestWallopsSuccess(t *testing.T) { tserver := newTestServerWithOper(t) // Create receiver with +w. receiverToken := tserver.createSession("receiver") tserver.sendCommand(receiverToken, map[string]any{ commandKey: "MODE", toKey: "receiver", bodyKey: []string{"+w"}, }) _, lastRecv := tserver.pollMessages(receiverToken, 0) // Create oper. operToken := tserver.createSession("walloper") tserver.pollMessages(operToken, 0) tserver.sendCommand(operToken, map[string]any{ commandKey: "OPER", bodyKey: []string{testOperName, testOperPassword}, }) tserver.pollMessages(operToken, 0) // Also set +w on oper so they receive it too. tserver.sendCommand(operToken, map[string]any{ commandKey: "MODE", toKey: "walloper", bodyKey: []string{"+w"}, }) tserver.pollMessages(operToken, 0) // Send WALLOPS. tserver.sendCommand(operToken, map[string]any{ commandKey: "WALLOPS", bodyKey: []string{"server going down"}, }) // Receiver should get the WALLOPS message. msgs, _ := tserver.pollMessages(receiverToken, lastRecv) foundWallops := false for _, msg := range msgs { cmd, _ := msg["command"].(string) if cmd == "WALLOPS" { foundWallops = true break } } if !foundWallops { t.Fatalf( "expected WALLOPS message, got %v", msgs, ) } } func TestWallopsNotOper(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("notoper2") _, lastID := tserver.pollMessages(token, 0) tserver.sendCommand(token, map[string]any{ commandKey: "WALLOPS", bodyKey: []string{"hello"}, }) msgs, _ := tserver.pollMessages(token, lastID) // Expect 481 ERR_NOPRIVILEGES. if !findNumeric(msgs, "481") { t.Fatalf( "expected ERR_NOPRIVILEGES (481), got %v", msgs, ) } } func TestWallopsNoParams(t *testing.T) { tserver := newTestServerWithOper(t) token := tserver.createSession("operempty") _, lastID := tserver.pollMessages(token, 0) tserver.sendCommand(token, map[string]any{ commandKey: "OPER", bodyKey: []string{testOperName, testOperPassword}, }) _, lastID = tserver.pollMessages(token, lastID) tserver.sendCommand(token, map[string]any{ commandKey: "WALLOPS", }) msgs, _ := tserver.pollMessages(token, lastID) // Expect 461 ERR_NEEDMOREPARAMS. if !findNumeric(msgs, "461") { t.Fatalf( "expected ERR_NEEDMOREPARAMS (461), got %v", msgs, ) } } func TestWallopsNotReceivedWithoutW(t *testing.T) { tserver := newTestServerWithOper(t) // Create receiver WITHOUT +w. receiverToken := tserver.createSession("nowallops") _, lastRecv := tserver.pollMessages(receiverToken, 0) // Create oper. operToken := tserver.createSession("walloper2") tserver.pollMessages(operToken, 0) tserver.sendCommand(operToken, map[string]any{ commandKey: "OPER", bodyKey: []string{testOperName, testOperPassword}, }) tserver.pollMessages(operToken, 0) // Send WALLOPS. tserver.sendCommand(operToken, map[string]any{ commandKey: "WALLOPS", bodyKey: []string{"secret message"}, }) // Receiver should NOT get the WALLOPS message. msgs, _ := tserver.pollMessages(receiverToken, lastRecv) for _, msg := range msgs { cmd, _ := msg["command"].(string) if cmd == "WALLOPS" { t.Fatalf( "did not expect WALLOPS for user "+ "without +w, got %v", msgs, ) } } } // --- User Mode +w --- func TestUserModeSetW(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("wmoder") _, lastID := tserver.pollMessages(token, 0) // Set +w. tserver.sendCommand(token, map[string]any{ commandKey: "MODE", toKey: "wmoder", bodyKey: []string{"+w"}, }) msgs, lastID := tserver.pollMessages(token, lastID) // Expect 221 RPL_UMODEIS with "+w". msg := findNumericWithParams(msgs, "221") if msg == nil { t.Fatalf( "expected RPL_UMODEIS (221), got %v", msgs, ) } body := getNumericBody(msg) if !strings.Contains(body, "w") { t.Fatalf( "expected mode string to contain 'w', got %q", body, ) } // Now query mode. tserver.sendCommand(token, map[string]any{ commandKey: "MODE", toKey: "wmoder", }) msgs, _ = tserver.pollMessages(token, lastID) msg = findNumericWithParams(msgs, "221") if msg == nil { t.Fatalf( "expected RPL_UMODEIS (221) on query, got %v", msgs, ) } body = getNumericBody(msg) if !strings.Contains(body, "w") { t.Fatalf( "expected mode '+w' in query, got %q", body, ) } } func TestUserModeUnsetW(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("wunsetter") _, lastID := tserver.pollMessages(token, 0) // Set +w first. tserver.sendCommand(token, map[string]any{ commandKey: "MODE", toKey: "wunsetter", bodyKey: []string{"+w"}, }) _, lastID = tserver.pollMessages(token, lastID) // Unset -w. tserver.sendCommand(token, map[string]any{ commandKey: "MODE", toKey: "wunsetter", bodyKey: []string{"-w"}, }) msgs, _ := tserver.pollMessages(token, lastID) msg := findNumericWithParams(msgs, "221") if msg == nil { t.Fatalf( "expected RPL_UMODEIS (221), got %v", msgs, ) } body := getNumericBody(msg) if strings.Contains(body, "w") { t.Fatalf( "expected 'w' to be removed, got %q", body, ) } } func TestUserModeUnknownFlag(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("badmode") _, lastID := tserver.pollMessages(token, 0) tserver.sendCommand(token, map[string]any{ commandKey: "MODE", toKey: "badmode", bodyKey: []string{"+z"}, }) msgs, _ := tserver.pollMessages(token, lastID) // Expect 501 ERR_UMODEUNKNOWNFLAG. if !findNumeric(msgs, "501") { t.Fatalf( "expected ERR_UMODEUNKNOWNFLAG (501), got %v", msgs, ) } } func TestUserModeCannotSetO(t *testing.T) { tserver := newTestServer(t) token := tserver.createSession("tryoper") _, lastID := tserver.pollMessages(token, 0) // Try to set +o via MODE (should fail). tserver.sendCommand(token, map[string]any{ commandKey: "MODE", toKey: "tryoper", bodyKey: []string{"+o"}, }) msgs, _ := tserver.pollMessages(token, lastID) // Expect 501 ERR_UMODEUNKNOWNFLAG. if !findNumeric(msgs, "501") { t.Fatalf( "expected ERR_UMODEUNKNOWNFLAG (501), got %v", msgs, ) } } func TestUserModeDeoper(t *testing.T) { tserver := newTestServerWithOper(t) token := tserver.createSession("deoper") _, lastID := tserver.pollMessages(token, 0) // Authenticate as oper. tserver.sendCommand(token, map[string]any{ commandKey: "OPER", bodyKey: []string{testOperName, testOperPassword}, }) _, lastID = tserver.pollMessages(token, lastID) // Use MODE -o to de-oper. tserver.sendCommand(token, map[string]any{ commandKey: "MODE", toKey: "deoper", bodyKey: []string{"-o"}, }) msgs, _ := tserver.pollMessages(token, lastID) msg := findNumericWithParams(msgs, "221") if msg == nil { t.Fatalf( "expected RPL_UMODEIS (221), got %v", msgs, ) } body := getNumericBody(msg) if strings.Contains(body, "o") { t.Fatalf( "expected 'o' to be removed, got %q", body, ) } } func TestUserModeCannotChangeOtherUser(t *testing.T) { tserver := newTestServer(t) _ = tserver.createSession("other") token := tserver.createSession("changer") _, lastID := tserver.pollMessages(token, 0) // Try to change another user's mode. tserver.sendCommand(token, map[string]any{ commandKey: "MODE", toKey: "other", bodyKey: []string{"+w"}, }) msgs, _ := tserver.pollMessages(token, lastID) // Expect 502 ERR_USERSDONTMATCH. if !findNumeric(msgs, "502") { t.Fatalf( "expected ERR_USERSDONTMATCH (502), got %v", msgs, ) } } // getNumericBody extracts the body text from a numeric // message. The body is stored as a JSON array; this // returns the first element. func getNumericBody(msg map[string]any) string { raw, exists := msg["body"] if !exists || raw == nil { return "" } arr, isArr := raw.([]any) if !isArr || len(arr) == 0 { return "" } str, isStr := arr[0].(string) if !isStr { return "" } return str }