feat: add username/hostname support with IRC hostmask format (#82)
All checks were successful
check / check (push) Successful in 6s
All checks were successful
check / check (push) Successful in 6s
## Summary Adds username and hostname support to sessions, enabling standard IRC hostmask format (`nick!user@host`) for WHOIS, WHO, and future `+b` ban matching. closes #81 ## Changes ### Schema (`001_initial.sql`) - Added `username TEXT NOT NULL DEFAULT ''` and `hostname TEXT NOT NULL DEFAULT ''` columns to the `sessions` table ### Database layer (`internal/db/`) - `CreateSession` now accepts `username` and `hostname` parameters; username defaults to nick if empty - `RegisterUser` now accepts `username` and `hostname` parameters - New `SessionHostInfo` type and `GetSessionHostInfo` query to retrieve username/hostname for a session - `MemberInfo` now includes `Username` and `Hostname` fields - `ChannelMembers` query updated to return username/hostname - New `FormatHostmask(nick, username, hostname)` helper that produces `nick!user@host` format - New `Hostmask()` method on `MemberInfo` ### Handler layer (`internal/handlers/`) - Session creation (`POST /api/v1/session`) accepts optional `username` field; resolves hostname via reverse DNS of connecting client IP (respects `X-Forwarded-For` and `X-Real-IP` headers) - Registration (`POST /api/v1/register`) accepts optional `username` field with the same hostname resolution - Username validation regex: `^[a-zA-Z0-9_\-\[\]\\^{}|` + "\`" + `]{1,32}$` - WHOIS (`311 RPL_WHOISUSER`) now returns the real username and hostname instead of nick/servername - WHO (`352 RPL_WHOREPLY`) now returns the real username and hostname instead of nick/servername - Extracted `validateHashcash` and `resolveUsername` helpers to keep functions under the linter's `funlen` limit - Extracted `executeRegister` helper for the same reason - Reverse DNS uses `(*net.Resolver).LookupAddr` with a 3-second timeout context ### Tests - `TestCreateSessionWithUserHost` — verifies username/hostname are stored and retrievable - `TestCreateSessionDefaultUsername` — verifies empty username defaults to nick - `TestGetSessionHostInfoNotFound` — verifies error on nonexistent session - `TestFormatHostmask` — verifies `nick!user@host` formatting - `TestFormatHostmaskDefaults` — verifies fallback when username/hostname empty - `TestMemberInfoHostmask` — verifies `Hostmask()` method on `MemberInfo` - `TestChannelMembersIncludeUserHost` — verifies `ChannelMembers` returns username/hostname - `TestRegisterUserWithUserHost` — verifies registration stores username/hostname - `TestRegisterUserDefaultUsername` — verifies registration defaults username to nick - `TestWhoisShowsHostInfo` — integration test verifying WHOIS returns the correct username - `TestWhoShowsHostInfo` — integration test verifying WHO returns the correct username - `TestSessionUsernameDefault` — integration test verifying default username in WHOIS - All existing tests updated for new `CreateSession`/`RegisterUser` signatures ### README - New "Hostmask" section documenting the `nick!user@host` format - Updated session creation and registration API docs with the new `username` field - Updated WHOIS/WHO numeric examples to show real username/hostname - Updated sessions schema table with new columns ## Docker build `docker build .` passes cleanly (lint, format, tests, build). Co-authored-by: user <user@Mac.lan guest wan> Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de> Co-authored-by: clawbot <clawbot@eeqj.de> Reviewed-on: #82 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
This commit was merged in pull request #82.
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -30,8 +31,14 @@ import (
|
||||
"git.eeqj.de/sneak/neoirc/internal/stats"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/fx/fxtest"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
db.SetBcryptCost(bcrypt.MinCost)
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
const (
|
||||
commandKey = "command"
|
||||
bodyKey = "body"
|
||||
@@ -102,7 +109,6 @@ func newTestServer(
|
||||
)
|
||||
|
||||
app.RequireStart()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
httpSrv := httptest.NewServer(srv)
|
||||
|
||||
@@ -2131,6 +2137,249 @@ func TestSessionStillWorks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// findNumericWithParams returns the first message matching
|
||||
// the given numeric code. Returns nil if not found.
|
||||
func findNumericWithParams(
|
||||
msgs []map[string]any,
|
||||
numeric string,
|
||||
) map[string]any {
|
||||
want, _ := strconv.Atoi(numeric)
|
||||
|
||||
for _, msg := range msgs {
|
||||
code, ok := msg["code"].(float64)
|
||||
if ok && int(code) == want {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getNumericParams extracts the params array from a
|
||||
// numeric message as a string slice.
|
||||
func getNumericParams(
|
||||
msg map[string]any,
|
||||
) []string {
|
||||
raw, exists := msg["params"]
|
||||
if !exists || raw == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
arr, isArr := raw.([]any)
|
||||
if !isArr {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(arr))
|
||||
|
||||
for _, val := range arr {
|
||||
str, isString := val.(string)
|
||||
if isString {
|
||||
result = append(result, str)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func TestWhoisShowsHostInfo(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSessionWithUsername(
|
||||
"whoisuser", "myident",
|
||||
)
|
||||
|
||||
queryToken := tserver.createSession("querier")
|
||||
|
||||
_, lastID := tserver.pollMessages(queryToken, 0)
|
||||
|
||||
tserver.sendCommand(queryToken, map[string]any{
|
||||
commandKey: "WHOIS",
|
||||
toKey: "whoisuser",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(queryToken, lastID)
|
||||
|
||||
whoisMsg := findNumericWithParams(msgs, "311")
|
||||
if whoisMsg == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_WHOISUSER (311), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
params := getNumericParams(whoisMsg)
|
||||
|
||||
if len(params) < 2 {
|
||||
t.Fatalf(
|
||||
"expected at least 2 params, got %v",
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
||||
if params[1] != "myident" {
|
||||
t.Fatalf(
|
||||
"expected username myident, got %s",
|
||||
params[1],
|
||||
)
|
||||
}
|
||||
|
||||
_ = token
|
||||
}
|
||||
|
||||
// createSessionWithUsername creates a session with a
|
||||
// specific username and returns the token.
|
||||
func (tserver *testServer) createSessionWithUsername(
|
||||
nick, username string,
|
||||
) string {
|
||||
tserver.t.Helper()
|
||||
|
||||
body, err := json.Marshal(map[string]string{
|
||||
"nick": nick,
|
||||
"username": username,
|
||||
})
|
||||
if err != nil {
|
||||
tserver.t.Fatalf("marshal session: %v", err)
|
||||
}
|
||||
|
||||
resp, err := doRequest(
|
||||
tserver.t,
|
||||
http.MethodPost,
|
||||
tserver.url(apiSession),
|
||||
bytes.NewReader(body),
|
||||
)
|
||||
if err != nil {
|
||||
tserver.t.Fatalf("create session: %v", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
tserver.t.Fatalf(
|
||||
"create session: status %d: %s",
|
||||
resp.StatusCode, respBody,
|
||||
)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
_ = json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
return result.Token
|
||||
}
|
||||
|
||||
func TestWhoShowsHostInfo(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
whoToken := tserver.createSessionWithUsername(
|
||||
"whouser", "whoident",
|
||||
)
|
||||
|
||||
tserver.sendCommand(whoToken, map[string]any{
|
||||
commandKey: joinCmd, toKey: "#whotest",
|
||||
})
|
||||
|
||||
queryToken := tserver.createSession("whoquerier")
|
||||
|
||||
tserver.sendCommand(queryToken, map[string]any{
|
||||
commandKey: joinCmd, toKey: "#whotest",
|
||||
})
|
||||
|
||||
_, lastID := tserver.pollMessages(queryToken, 0)
|
||||
|
||||
tserver.sendCommand(queryToken, map[string]any{
|
||||
commandKey: "WHO",
|
||||
toKey: "#whotest",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(queryToken, lastID)
|
||||
|
||||
assertWhoReplyUsername(t, msgs, "whouser", "whoident")
|
||||
}
|
||||
|
||||
func assertWhoReplyUsername(
|
||||
t *testing.T,
|
||||
msgs []map[string]any,
|
||||
targetNick, expectedUsername string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
for _, msg := range msgs {
|
||||
code, isCode := msg["code"].(float64)
|
||||
if !isCode || int(code) != 352 {
|
||||
continue
|
||||
}
|
||||
|
||||
params := getNumericParams(msg)
|
||||
if len(params) < 5 || params[4] != targetNick {
|
||||
continue
|
||||
}
|
||||
|
||||
if params[1] != expectedUsername {
|
||||
t.Fatalf(
|
||||
"expected username %s in WHO, got %s",
|
||||
expectedUsername, params[1],
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
t.Fatalf(
|
||||
"expected RPL_WHOREPLY (352) for %s, msgs: %v",
|
||||
targetNick, msgs,
|
||||
)
|
||||
}
|
||||
|
||||
func TestSessionUsernameDefault(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
// Create session without specifying username.
|
||||
token := tserver.createSession("defaultusr")
|
||||
|
||||
queryToken := tserver.createSession("querier2")
|
||||
|
||||
_, lastID := tserver.pollMessages(queryToken, 0)
|
||||
|
||||
// WHOIS should show the nick as the username.
|
||||
tserver.sendCommand(queryToken, map[string]any{
|
||||
commandKey: "WHOIS",
|
||||
toKey: "defaultusr",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(queryToken, lastID)
|
||||
|
||||
whoisMsg := findNumericWithParams(msgs, "311")
|
||||
if whoisMsg == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_WHOISUSER (311), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
params := getNumericParams(whoisMsg)
|
||||
|
||||
if len(params) < 2 {
|
||||
t.Fatalf(
|
||||
"expected at least 2 params, got %v",
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
||||
// Username defaults to nick.
|
||||
if params[1] != "defaultusr" {
|
||||
t.Fatalf(
|
||||
"expected default username defaultusr, got %s",
|
||||
params[1],
|
||||
)
|
||||
}
|
||||
|
||||
_ = token
|
||||
}
|
||||
|
||||
func TestNickBroadcastToChannels(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
aliceToken := tserver.createSession("nick_a")
|
||||
@@ -2552,3 +2801,444 @@ func TestChannelHashcashMissingBitsArg(t *testing.T) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamesShowsHostmask(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
queryToken, lastID := setupChannelWithIdentMember(
|
||||
tserver, "namesmember", "nmident",
|
||||
"namesquery", "#namestest",
|
||||
)
|
||||
|
||||
// Issue an explicit NAMES command.
|
||||
tserver.sendCommand(queryToken, map[string]any{
|
||||
commandKey: "NAMES",
|
||||
toKey: "#namestest",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(queryToken, lastID)
|
||||
|
||||
assertNamesHostmask(
|
||||
t, msgs, "namesmember", "nmident",
|
||||
)
|
||||
}
|
||||
|
||||
func TestNamesOnJoinShowsHostmask(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
|
||||
// First user joins to populate the channel.
|
||||
firstToken := tserver.createSessionWithUsername(
|
||||
"joinmem", "jmident",
|
||||
)
|
||||
|
||||
tserver.sendCommand(firstToken, map[string]any{
|
||||
commandKey: joinCmd, toKey: "#joinnamestest",
|
||||
})
|
||||
|
||||
// Second user joins; the JOIN triggers
|
||||
// deliverNamesNumerics which should include
|
||||
// hostmask data.
|
||||
joinerToken := tserver.createSession("joiner")
|
||||
|
||||
tserver.sendCommand(joinerToken, map[string]any{
|
||||
commandKey: joinCmd, toKey: "#joinnamestest",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(joinerToken, 0)
|
||||
|
||||
assertNamesHostmask(
|
||||
t, msgs, "joinmem", "jmident",
|
||||
)
|
||||
}
|
||||
|
||||
// setupChannelWithIdentMember creates a member session
|
||||
// with username, joins a channel, then creates a querier
|
||||
// and joins the same channel. Returns the querier token
|
||||
// and last message ID.
|
||||
func setupChannelWithIdentMember(
|
||||
tserver *testServer,
|
||||
memberNick, memberUsername,
|
||||
querierNick, channel string,
|
||||
) (string, int64) {
|
||||
tserver.t.Helper()
|
||||
|
||||
memberToken := tserver.createSessionWithUsername(
|
||||
memberNick, memberUsername,
|
||||
)
|
||||
|
||||
tserver.sendCommand(memberToken, map[string]any{
|
||||
commandKey: joinCmd, toKey: channel,
|
||||
})
|
||||
|
||||
queryToken := tserver.createSession(querierNick)
|
||||
|
||||
tserver.sendCommand(queryToken, map[string]any{
|
||||
commandKey: joinCmd, toKey: channel,
|
||||
})
|
||||
|
||||
_, lastID := tserver.pollMessages(queryToken, 0)
|
||||
|
||||
return queryToken, lastID
|
||||
}
|
||||
|
||||
// assertNamesHostmask verifies that a RPL_NAMREPLY (353)
|
||||
// message contains the expected nick with hostmask format
|
||||
// (nick!user@host).
|
||||
func assertNamesHostmask(
|
||||
t *testing.T,
|
||||
msgs []map[string]any,
|
||||
targetNick, expectedUsername string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
for _, msg := range msgs {
|
||||
code, ok := msg["code"].(float64)
|
||||
if !ok || int(code) != 353 {
|
||||
continue
|
||||
}
|
||||
|
||||
raw, exists := msg["body"]
|
||||
if !exists || raw == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
arr, isArr := raw.([]any)
|
||||
if !isArr || len(arr) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
bodyStr, isStr := arr[0].(string)
|
||||
if !isStr {
|
||||
continue
|
||||
}
|
||||
|
||||
// Look for the target nick's hostmask entry.
|
||||
expected := targetNick + "!" +
|
||||
expectedUsername + "@"
|
||||
|
||||
if !strings.Contains(bodyStr, expected) {
|
||||
t.Fatalf(
|
||||
"expected NAMES body to contain %q, "+
|
||||
"got %q",
|
||||
expected, bodyStr,
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
t.Fatalf(
|
||||
"expected RPL_NAMREPLY (353) with hostmask "+
|
||||
"for %s, msgs: %v",
|
||||
targetNick, msgs,
|
||||
)
|
||||
}
|
||||
|
||||
const testOperName = "admin"
|
||||
const testOperPassword = "secretpass"
|
||||
|
||||
// newTestServerWithOper creates a test server with oper
|
||||
// credentials configured (admin / secretpass).
|
||||
func newTestServerWithOper(
|
||||
t *testing.T,
|
||||
) *testServer {
|
||||
t.Helper()
|
||||
|
||||
dbPath := filepath.Join(
|
||||
t.TempDir(), "test.db",
|
||||
)
|
||||
|
||||
dbURL := "file:" + dbPath +
|
||||
"?_journal_mode=WAL&_busy_timeout=5000"
|
||||
|
||||
var srv *server.Server
|
||||
|
||||
app := fxtest.New(t,
|
||||
fx.Provide(
|
||||
newTestGlobals,
|
||||
logger.New,
|
||||
func(
|
||||
lifecycle fx.Lifecycle,
|
||||
globs *globals.Globals,
|
||||
log *logger.Logger,
|
||||
) (*config.Config, error) {
|
||||
cfg, err := config.New(
|
||||
lifecycle, config.Params{ //nolint:exhaustruct
|
||||
Globals: globs, Logger: log,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"test config: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
cfg.DBURL = dbURL
|
||||
cfg.Port = 0
|
||||
cfg.HashcashBits = 0
|
||||
cfg.OperName = testOperName
|
||||
cfg.OperPassword = testOperPassword
|
||||
|
||||
return cfg, nil
|
||||
},
|
||||
newTestDB,
|
||||
stats.New,
|
||||
newTestHealthcheck,
|
||||
newTestMiddleware,
|
||||
newTestHandlers,
|
||||
newTestServerFx,
|
||||
),
|
||||
fx.Populate(&srv),
|
||||
)
|
||||
|
||||
app.RequireStart()
|
||||
|
||||
httpSrv := httptest.NewServer(srv)
|
||||
|
||||
t.Cleanup(func() {
|
||||
httpSrv.Close()
|
||||
app.RequireStop()
|
||||
})
|
||||
|
||||
return &testServer{
|
||||
httpServer: httpSrv,
|
||||
t: t,
|
||||
fxApp: app,
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperCommandSuccess(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
token := tserver.createSession("operuser")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Send OPER command.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 381 RPL_YOUREOPER.
|
||||
if !findNumeric(msgs, "381") {
|
||||
t.Fatalf(
|
||||
"expected RPL_YOUREOPER (381), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperCommandFailure(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
token := tserver.createSession("badoper")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Send OPER with wrong password.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, "wrongpass"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 491 ERR_NOOPERHOST.
|
||||
if !findNumeric(msgs, "491") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NOOPERHOST (491), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperCommandNeedMoreParams(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
token := tserver.createSession("shortoper")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Send OPER with only one parameter.
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Expect 461 ERR_NEEDMOREPARAMS.
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperWhoisShowsClientInfo(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
// Create a target user.
|
||||
_ = tserver.createSession("target")
|
||||
|
||||
// Create an oper user.
|
||||
operToken := tserver.createSession("theoper")
|
||||
_, lastID := tserver.pollMessages(operToken, 0)
|
||||
|
||||
// Authenticate as oper.
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
var msgs []map[string]any
|
||||
|
||||
msgs, lastID = tserver.pollMessages(operToken, lastID)
|
||||
|
||||
if !findNumeric(msgs, "381") {
|
||||
t.Fatalf(
|
||||
"expected RPL_YOUREOPER (381), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
// Now WHOIS the target.
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "WHOIS",
|
||||
toKey: "target",
|
||||
})
|
||||
|
||||
msgs, _ = tserver.pollMessages(operToken, lastID)
|
||||
|
||||
// Expect 338 RPL_WHOISACTUALLY with client IP.
|
||||
whoisActually := findNumericWithParams(msgs, "338")
|
||||
if whoisActually == nil {
|
||||
t.Fatalf(
|
||||
"expected RPL_WHOISACTUALLY (338) for "+
|
||||
"oper WHOIS, got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
params := getNumericParams(whoisActually)
|
||||
if len(params) < 2 {
|
||||
t.Fatalf(
|
||||
"expected at least 2 params in 338, "+
|
||||
"got %v",
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
||||
// First param should be the target nick.
|
||||
if params[0] != "target" {
|
||||
t.Fatalf(
|
||||
"expected first param 'target', got %s",
|
||||
params[0],
|
||||
)
|
||||
}
|
||||
|
||||
// Second param should be a non-empty IP.
|
||||
if params[1] == "" {
|
||||
t.Fatal("expected non-empty IP in 338 params")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonOperWhoisHidesClientInfo(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
// Create a target user.
|
||||
_ = tserver.createSession("hidden")
|
||||
|
||||
// Create a regular (non-oper) user.
|
||||
regToken := tserver.createSession("regular")
|
||||
_, lastID := tserver.pollMessages(regToken, 0)
|
||||
|
||||
// WHOIS the target without oper status.
|
||||
tserver.sendCommand(regToken, map[string]any{
|
||||
commandKey: "WHOIS",
|
||||
toKey: "hidden",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(regToken, lastID)
|
||||
|
||||
// Should NOT see 338 RPL_WHOISACTUALLY.
|
||||
if findNumeric(msgs, "338") {
|
||||
t.Fatalf(
|
||||
"non-oper should not see "+
|
||||
"RPL_WHOISACTUALLY (338), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
// But should see 311 RPL_WHOISUSER (normal WHOIS).
|
||||
if !findNumeric(msgs, "311") {
|
||||
t.Fatalf(
|
||||
"expected RPL_WHOISUSER (311), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoisShowsOperatorStatus(t *testing.T) {
|
||||
tserver := newTestServerWithOper(t)
|
||||
|
||||
// Create oper user and authenticate.
|
||||
operToken := tserver.createSession("iamoper")
|
||||
_, lastID := tserver.pollMessages(operToken, 0)
|
||||
|
||||
tserver.sendCommand(operToken, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, testOperPassword},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(operToken, lastID)
|
||||
|
||||
if !findNumeric(msgs, "381") {
|
||||
t.Fatalf("expected 381, got %v", msgs)
|
||||
}
|
||||
|
||||
// Another user does WHOIS on the oper.
|
||||
queryToken := tserver.createSession("asker")
|
||||
_, queryLastID := tserver.pollMessages(queryToken, 0)
|
||||
|
||||
tserver.sendCommand(queryToken, map[string]any{
|
||||
commandKey: "WHOIS",
|
||||
toKey: "iamoper",
|
||||
})
|
||||
|
||||
msgs, _ = tserver.pollMessages(queryToken, queryLastID)
|
||||
|
||||
// Should see 313 RPL_WHOISOPERATOR.
|
||||
if !findNumeric(msgs, "313") {
|
||||
t.Fatalf(
|
||||
"expected RPL_WHOISOPERATOR (313) in "+
|
||||
"WHOIS of oper, got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperNoOlineConfigured(t *testing.T) {
|
||||
// Standard test server has no oper configured.
|
||||
tserver := newTestServer(t)
|
||||
|
||||
token := tserver.createSession("nooline")
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "OPER",
|
||||
bodyKey: []string{testOperName, "password"},
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
// Should get 491 since no o-line is configured.
|
||||
if !findNumeric(msgs, "491") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NOOPERHOST (491) when no "+
|
||||
"o-line configured, got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user