feat: add username/hostname support with IRC hostmask format
All checks were successful
check / check (push) Successful in 2m11s
All checks were successful
check / check (push) Successful in 2m11s
- Add username and hostname columns to sessions table (001_initial.sql) - Accept optional username field in session creation and registration endpoints; defaults to nick if not provided - Resolve hostname via reverse DNS of connecting client IP at session creation time (supports X-Forwarded-For and X-Real-IP headers) - Display real username and hostname in WHOIS (311 RPL_WHOISUSER) and WHO (352 RPL_WHOREPLY) responses instead of nick/servername - Add FormatHostmask helper for nick!user@host format - Add SessionHostInfo type and GetSessionHostInfo query - Include username/hostname in MemberInfo and ChannelMembers results - Extract validateHashcash and resolveUsername helpers to stay under funlen limits - Add comprehensive unit tests for all new DB functions, hostmask formatting, and integration tests for WHOIS/WHO responses - Update README with hostmask documentation, new API fields, and updated schema reference
This commit is contained in:
@@ -34,7 +34,7 @@ func TestCreateSession(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
sessionID, _, token, err := database.CreateSession(
|
||||
ctx, "alice",
|
||||
ctx, "alice", "", "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -45,7 +45,7 @@ func TestCreateSession(t *testing.T) {
|
||||
}
|
||||
|
||||
_, _, dupToken, dupErr := database.CreateSession(
|
||||
ctx, "alice",
|
||||
ctx, "alice", "", "",
|
||||
)
|
||||
if dupErr == nil {
|
||||
t.Fatal("expected error for duplicate nick")
|
||||
@@ -54,13 +54,186 @@ func TestCreateSession(t *testing.T) {
|
||||
_ = dupToken
|
||||
}
|
||||
|
||||
// assertSessionHostInfo creates a session and verifies
|
||||
// the stored username and hostname match expectations.
|
||||
func assertSessionHostInfo(
|
||||
t *testing.T,
|
||||
database *db.Database,
|
||||
nick, inputUser, inputHost,
|
||||
expectUser, expectHost string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
sessionID, _, _, err := database.CreateSession(
|
||||
t.Context(), nick, inputUser, inputHost,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
info, err := database.GetSessionHostInfo(
|
||||
t.Context(), sessionID,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if info.Username != expectUser {
|
||||
t.Fatalf(
|
||||
"expected username %s, got %s",
|
||||
expectUser, info.Username,
|
||||
)
|
||||
}
|
||||
|
||||
if info.Hostname != expectHost {
|
||||
t.Fatalf(
|
||||
"expected hostname %s, got %s",
|
||||
expectHost, info.Hostname,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSessionWithUserHost(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
database := setupTestDB(t)
|
||||
|
||||
assertSessionHostInfo(
|
||||
t, database,
|
||||
"hostuser", "myident", "example.com",
|
||||
"myident", "example.com",
|
||||
)
|
||||
}
|
||||
|
||||
func TestCreateSessionDefaultUsername(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
database := setupTestDB(t)
|
||||
|
||||
// Empty username defaults to nick.
|
||||
assertSessionHostInfo(
|
||||
t, database,
|
||||
"defaultu", "", "host.local",
|
||||
"defaultu", "host.local",
|
||||
)
|
||||
}
|
||||
|
||||
func TestGetSessionHostInfoNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
database := setupTestDB(t)
|
||||
|
||||
_, err := database.GetSessionHostInfo(
|
||||
t.Context(), 99999,
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nonexistent session")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatHostmask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := db.FormatHostmask(
|
||||
"nick", "user", "host.com",
|
||||
)
|
||||
if result != "nick!user@host.com" {
|
||||
t.Fatalf(
|
||||
"expected nick!user@host.com, got %s",
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatHostmaskDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := db.FormatHostmask("nick", "", "")
|
||||
if result != "nick!nick@*" {
|
||||
t.Fatalf(
|
||||
"expected nick!nick@*, got %s",
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemberInfoHostmask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
member := &db.MemberInfo{ //nolint:exhaustruct // test only uses hostmask fields
|
||||
Nick: "alice",
|
||||
Username: "aliceident",
|
||||
Hostname: "alice.example.com",
|
||||
}
|
||||
|
||||
hostmask := member.Hostmask()
|
||||
expected := "alice!aliceident@alice.example.com"
|
||||
|
||||
if hostmask != expected {
|
||||
t.Fatalf(
|
||||
"expected %s, got %s", expected, hostmask,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelMembersIncludeUserHost(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
database := setupTestDB(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sid, _, _, err := database.CreateSession(
|
||||
ctx, "memuser", "myuser", "myhost.net",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
chID, err := database.GetOrCreateChannel(
|
||||
ctx, "#hostchan",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = database.JoinChannel(ctx, chID, sid)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
members, err := database.ChannelMembers(ctx, chID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(members) != 1 {
|
||||
t.Fatalf(
|
||||
"expected 1 member, got %d", len(members),
|
||||
)
|
||||
}
|
||||
|
||||
if members[0].Username != "myuser" {
|
||||
t.Fatalf(
|
||||
"expected username myuser, got %s",
|
||||
members[0].Username,
|
||||
)
|
||||
}
|
||||
|
||||
if members[0].Hostname != "myhost.net" {
|
||||
t.Fatalf(
|
||||
"expected hostname myhost.net, got %s",
|
||||
members[0].Hostname,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSessionByToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
database := setupTestDB(t)
|
||||
ctx := t.Context()
|
||||
|
||||
_, _, token, err := database.CreateSession(ctx, "bob")
|
||||
_, _, token, err := database.CreateSession(ctx, "bob", "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -93,7 +266,7 @@ func TestGetSessionByNick(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
charlieID, charlieClientID, charlieToken, err :=
|
||||
database.CreateSession(ctx, "charlie")
|
||||
database.CreateSession(ctx, "charlie", "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -150,7 +323,7 @@ func TestJoinAndPart(t *testing.T) {
|
||||
database := setupTestDB(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sid, _, _, err := database.CreateSession(ctx, "user1")
|
||||
sid, _, _, err := database.CreateSession(ctx, "user1", "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -199,7 +372,7 @@ func TestDeleteChannelIfEmpty(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sid, _, _, err := database.CreateSession(ctx, "temp")
|
||||
sid, _, _, err := database.CreateSession(ctx, "temp", "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -234,7 +407,7 @@ func createSessionWithChannels(
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
sid, _, _, err := database.CreateSession(ctx, nick)
|
||||
sid, _, _, err := database.CreateSession(ctx, nick, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -317,7 +490,7 @@ func TestChangeNick(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
sid, _, token, err := database.CreateSession(
|
||||
ctx, "old",
|
||||
ctx, "old", "", "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -401,7 +574,7 @@ func TestPollMessages(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
sid, _, token, err := database.CreateSession(
|
||||
ctx, "poller",
|
||||
ctx, "poller", "", "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -508,7 +681,7 @@ func TestDeleteSession(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
sid, _, _, err := database.CreateSession(
|
||||
ctx, "deleteme",
|
||||
ctx, "deleteme", "", "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -548,12 +721,12 @@ func TestChannelMembers(t *testing.T) {
|
||||
database := setupTestDB(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sid1, _, _, err := database.CreateSession(ctx, "m1")
|
||||
sid1, _, _, err := database.CreateSession(ctx, "m1", "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sid2, _, _, err := database.CreateSession(ctx, "m2")
|
||||
sid2, _, _, err := database.CreateSession(ctx, "m2", "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -611,7 +784,7 @@ func TestEnqueueToClient(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
_, _, token, err := database.CreateSession(
|
||||
ctx, "enqclient",
|
||||
ctx, "enqclient", "", "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
Reference in New Issue
Block a user