Re-vendor the canonical files from sneak/prompts at dd4027b (closes #112)
check / check (push) Failing after 7s

The shared files are fetched from sneak/prompts dd4027b, with this
repository's own entries after the shared content in .gitignore,
.editorconfig and .dockerignore.

Lint and tests are Dockerfile phases (golangci-lint v2.14.0, Debian Go
1.24.13) that the build stage depends on, and the Makefile targets call
the script/ entrypoints. make fmt also formats Markdown with prettier.

Fixes for the new lint findings: the auth cookie is always Secure, an
IRC connection's relay goroutine stops when the connection closes, and
repeated strings are constants.

Model: opus-5-5
This commit is contained in:
2026-10-06 08:35:46 +00:00
parent 9a46421902
commit 60c0d4b5e3
45 changed files with 2753 additions and 1420 deletions
-1
View File
@@ -1,4 +1,3 @@
// Package db provides database access and migration management.
package db
import (
+22 -12
View File
@@ -1150,7 +1150,8 @@ func scanMessages(
code, _ := strconv.Atoi(msg.Command)
msg.Code = code
if mt, err := irc.FromInt(code); err == nil {
mt, lookupErr := irc.FromInt(code)
if lookupErr == nil {
msg.Command = mt.Name()
}
}
@@ -1373,7 +1374,9 @@ func (database *Database) GetStaleOrphanSessions(
for rows.Next() {
var stale StaleSession
if err := rows.Scan(&stale.ID, &stale.Nick); err != nil {
err = rows.Scan(&stale.ID, &stale.Nick)
if err != nil {
return nil, fmt.Errorf(
"scan stale session: %w", err,
)
@@ -1382,7 +1385,8 @@ func (database *Database) GetStaleOrphanSessions(
result = append(result, stale)
}
if err := rows.Err(); err != nil {
err = rows.Err()
if err != nil {
return nil, fmt.Errorf(
"iterate stale sessions: %w", err,
)
@@ -1905,9 +1909,10 @@ func (database *Database) ListChannelBans(
for rows.Next() {
var ban BanInfo
if scanErr := rows.Scan(
scanErr := rows.Scan(
&ban.Mask, &ban.SetBy, &ban.CreatedAt,
); scanErr != nil {
)
if scanErr != nil {
return nil, fmt.Errorf(
"scan channel ban: %w", scanErr,
)
@@ -1916,7 +1921,8 @@ func (database *Database) ListChannelBans(
bans = append(bans, ban)
}
if rowErr := rows.Err(); rowErr != nil {
rowErr := rows.Err()
if rowErr != nil {
return nil, fmt.Errorf(
"iterate channel bans: %w", rowErr,
)
@@ -2247,11 +2253,12 @@ func (database *Database) ListAllChannelsWithCountsFiltered(
for rows.Next() {
var chanInfo ChannelInfoFull
if scanErr := rows.Scan(
scanErr := rows.Scan(
&chanInfo.Name,
&chanInfo.MemberCount,
&chanInfo.Topic,
); scanErr != nil {
)
if scanErr != nil {
return nil, fmt.Errorf(
"scan channel: %w", scanErr,
)
@@ -2260,7 +2267,8 @@ func (database *Database) ListAllChannelsWithCountsFiltered(
channels = append(channels, chanInfo)
}
if rowErr := rows.Err(); rowErr != nil {
rowErr := rows.Err()
if rowErr != nil {
return nil, fmt.Errorf(
"iterate channels: %w", rowErr,
)
@@ -2308,11 +2316,12 @@ func (database *Database) GetSessionChannelsFiltered(
for rows.Next() {
var chanInfo ChannelInfo
if scanErr := rows.Scan(
scanErr := rows.Scan(
&chanInfo.ID,
&chanInfo.Name,
&chanInfo.Topic,
); scanErr != nil {
)
if scanErr != nil {
return nil, fmt.Errorf(
"scan channel: %w", scanErr,
)
@@ -2321,7 +2330,8 @@ func (database *Database) GetSessionChannelsFiltered(
channels = append(channels, chanInfo)
}
if rowErr := rows.Err(); rowErr != nil {
rowErr := rows.Err()
if rowErr != nil {
return nil, fmt.Errorf(
"iterate channels: %w", rowErr,
)
+9 -6
View File
@@ -987,11 +987,12 @@ func TestGetOperCount(t *testing.T) {
sid2, _, _, err := database.CreateSession(
ctx, "user2", "", "", "",
)
_ = sid2
if err != nil {
t.Fatal(err)
}
_ = sid2
// Initially zero opers.
count, err := database.GetOperCount(ctx)
if err != nil {
@@ -1023,23 +1024,25 @@ func TestGetOperCount(t *testing.T) {
func TestWildcardMatch(t *testing.T) {
t.Parallel()
const hostmask = "nick!user@host"
tests := []struct {
pattern string
input string
match bool
}{
{"*!*@*", "nick!user@host", true},
{"*!*@*", hostmask, true},
{"*!*@*.example.com", "nick!user@foo.example.com", true},
{"*!*@*.example.com", "nick!user@other.net", false},
{"badnick!*@*", "badnick!user@host", true},
{"badnick!*@*", "goodnick!user@host", false},
{"nick!user@host", "nick!user@host", true},
{"nick!user@host", "nick!user@other", false},
{hostmask, hostmask, true},
{hostmask, "nick!user@other", false},
{"*", "anything", true},
{"?ick!*@*", "nick!user@host", true},
{"?ick!*@*", hostmask, true},
{"?ick!*@*", "nn!user@host", false},
// Case-insensitive.
{"Nick!*@*", "nick!user@host", true},
{"Nick!*@*", hostmask, true},
}
for _, tc := range tests {
+52 -60
View File
@@ -125,21 +125,18 @@ func (hdlr *Handlers) authSession(
}
// setAuthCookie sets the authentication cookie on the
// response.
// response. It is always Secure: the server runs behind a
// TLS-terminating reverse proxy.
func (hdlr *Handlers) setAuthCookie(
writer http.ResponseWriter,
request *http.Request,
token string,
) {
secure := request.TLS != nil ||
request.Header.Get("X-Forwarded-Proto") == "https"
http.SetCookie(writer, &http.Cookie{ //nolint:exhaustruct // optional fields
Name: authCookieName,
Value: token,
Path: "/",
HttpOnly: true,
Secure: secure,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
}
@@ -148,17 +145,13 @@ func (hdlr *Handlers) setAuthCookie(
// the client.
func (hdlr *Handlers) clearAuthCookie(
writer http.ResponseWriter,
request *http.Request,
) {
secure := request.TLS != nil ||
request.Header.Get("X-Forwarded-Proto") == "https"
http.SetCookie(writer, &http.Cookie{ //nolint:exhaustruct // optional fields
Name: authCookieName,
Value: "",
Path: "/",
HttpOnly: true,
Secure: secure,
Secure: true,
SameSite: http.SameSiteStrictMode,
MaxAge: -1,
})
@@ -172,7 +165,7 @@ func (hdlr *Handlers) requireAuth(
hdlr.authSession(request)
if err != nil {
hdlr.respondJSON(writer, request, map[string]any{
"error": "not registered",
errorKey: "not registered",
"numeric": irc.ErrNotRegistered,
}, http.StatusUnauthorized)
@@ -285,11 +278,11 @@ func (hdlr *Handlers) executeCreateSession(
hdlr.deliverMOTD(request, clientID, sessionID, nick)
hdlr.setAuthCookie(writer, request, token)
hdlr.setAuthCookie(writer, token)
hdlr.respondJSON(writer, request, map[string]any{
"id": sessionID,
"nick": nick,
"id": sessionID,
nickKey: nick,
}, http.StatusCreated)
}
@@ -643,7 +636,7 @@ func (hdlr *Handlers) HandleState() http.HandlerFunc {
hdlr.respondJSON(writer, request, map[string]any{
"id": sessionID,
"nick": nick,
nickKey: nick,
"channels": channels,
}, http.StatusOK)
}
@@ -1090,7 +1083,7 @@ func (hdlr *Handlers) dispatchQueryCommand(
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "error"},
map[string]string{statusKey: statusError},
http.StatusOK)
}
}
@@ -1112,7 +1105,7 @@ func (hdlr *Handlers) handlePrivmsg(
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "error"},
map[string]string{statusKey: statusError},
http.StatusOK)
return
@@ -1127,7 +1120,7 @@ func (hdlr *Handlers) handlePrivmsg(
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "error"},
map[string]string{statusKey: statusError},
http.StatusOK)
return
@@ -1169,7 +1162,7 @@ func (hdlr *Handlers) respondIRCError(
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "error"},
map[string]string{statusKey: statusError},
http.StatusOK)
}
@@ -1257,7 +1250,7 @@ func (hdlr *Handlers) handleChannelMsg(
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"id": uuid, "status": "sent"},
map[string]string{"id": uuid, statusKey: "sent"},
http.StatusOK)
}
@@ -1447,7 +1440,7 @@ func (hdlr *Handlers) handleDirectMsg(
hdlr.respondJSON(writer, request,
map[string]string{
"id": result.UUID, "status": "sent",
"id": result.UUID, statusKey: "sent",
},
http.StatusOK)
}
@@ -1522,7 +1515,7 @@ func (hdlr *Handlers) executeJoin(
hdlr.respondJSON(writer, request,
map[string]string{
"status": "joined",
statusKey: "joined",
"channel": channel,
},
http.StatusOK)
@@ -1679,6 +1672,7 @@ func (hdlr *Handlers) handlePart(
// Extract reason from body for the service call.
reason := ""
if body != nil {
var lines []string
if json.Unmarshal(body, &lines) == nil &&
@@ -1700,7 +1694,7 @@ func (hdlr *Handlers) handlePart(
hdlr.respondJSON(writer, request,
map[string]string{
"status": "parted",
statusKey: "parted",
"channel": channel,
},
http.StatusOK)
@@ -1739,7 +1733,7 @@ func (hdlr *Handlers) handleNick(
if newNick == nick {
hdlr.respondJSON(writer, request,
map[string]string{
"status": "ok", "nick": newNick,
statusKey: "ok", nickKey: newNick,
},
http.StatusOK)
@@ -1770,7 +1764,7 @@ func (hdlr *Handlers) executeNickChange(
hdlr.respondJSON(writer, request,
map[string]string{
"status": "ok", "nick": newNick,
statusKey: "ok", nickKey: newNick,
},
http.StatusOK)
}
@@ -1831,7 +1825,7 @@ func (hdlr *Handlers) handleTopic(
hdlr.respondJSON(writer, request,
map[string]string{
"status": "ok", "topic": topic,
statusKey: "ok", "topic": topic,
},
http.StatusOK)
}
@@ -1885,7 +1879,7 @@ func (hdlr *Handlers) dispatchInfoCommand(
_ = target
_ = bodyLines
okResp := map[string]string{"status": "ok"}
okResp := map[string]string{statusKey: "ok"}
switch command {
case irc.CmdMotd:
@@ -1916,6 +1910,7 @@ func (hdlr *Handlers) handleQuit(
body json.RawMessage,
) {
reason := "Client quit"
if body != nil {
var lines []string
if json.Unmarshal(body, &lines) == nil &&
@@ -1928,10 +1923,10 @@ func (hdlr *Handlers) handleQuit(
request.Context(), sessionID, nick, reason,
)
hdlr.clearAuthCookie(writer, request)
hdlr.clearAuthCookie(writer)
hdlr.respondJSON(writer, request,
map[string]string{"status": "quit"},
map[string]string{statusKey: "quit"},
http.StatusOK)
}
@@ -1963,7 +1958,7 @@ func (hdlr *Handlers) handleMode(
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
return
@@ -2064,7 +2059,7 @@ func (hdlr *Handlers) queryChannelMode(
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -2241,7 +2236,7 @@ func (hdlr *Handlers) applyParameterizedMode(
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "error"},
map[string]string{statusKey: statusError},
http.StatusOK)
}
}
@@ -2307,7 +2302,7 @@ func (hdlr *Handlers) applyUserMode(
)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -2353,7 +2348,7 @@ func (hdlr *Handlers) setChannelFlag(
)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -2437,7 +2432,7 @@ func (hdlr *Handlers) setHashcashMode(
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -2486,7 +2481,7 @@ func (hdlr *Handlers) clearHashcashMode(
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -2591,7 +2586,7 @@ func (hdlr *Handlers) executeBanChange(
}
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -2645,7 +2640,7 @@ func (hdlr *Handlers) listBans(
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -2709,7 +2704,7 @@ func (hdlr *Handlers) setChannelKeyMode(
}
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -2758,7 +2753,7 @@ func (hdlr *Handlers) clearChannelKeyMode(
}
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -2833,7 +2828,7 @@ func (hdlr *Handlers) setChannelLimitMode(
}
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -2883,7 +2878,7 @@ func (hdlr *Handlers) clearChannelLimitMode(
}
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -3050,7 +3045,7 @@ func (hdlr *Handlers) executeInvite(
}
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -3119,7 +3114,7 @@ func (hdlr *Handlers) handleNames(
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -3172,7 +3167,7 @@ func (hdlr *Handlers) handleList(
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -3262,7 +3257,7 @@ func (hdlr *Handlers) executeWhois(
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -3287,7 +3282,7 @@ func (hdlr *Handlers) whoisNotFound(
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -3454,7 +3449,7 @@ func (hdlr *Handlers) handleWho(
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
return
@@ -3496,7 +3491,7 @@ func (hdlr *Handlers) handleWho(
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -3512,7 +3507,7 @@ func (hdlr *Handlers) handleLusers(
)
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -3703,10 +3698,10 @@ func (hdlr *Handlers) HandleLogout() http.HandlerFunc {
)
}
hdlr.clearAuthCookie(writer, request)
hdlr.clearAuthCookie(writer)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
}
@@ -3809,7 +3804,7 @@ func (hdlr *Handlers) handleOper(
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -3857,7 +3852,7 @@ func (hdlr *Handlers) handleAway(
hdlr.broker.Notify(sessionID)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -3919,7 +3914,7 @@ func (hdlr *Handlers) handleKick(
}
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
@@ -3943,10 +3938,7 @@ func (hdlr *Handlers) deliverWhoisIdle(
return
}
idleSeconds := int64(time.Since(lastSeen).Seconds())
if idleSeconds < 0 {
idleSeconds = 0
}
idleSeconds := max(int64(time.Since(lastSeen).Seconds()), 0)
signonUnix := strconv.FormatInt(
createdAt.Unix(), 10,
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -117,11 +117,11 @@ func (hdlr *Handlers) executeLogin(
request, clientID, sessionID, nick,
)
hdlr.setAuthCookie(writer, request, token)
hdlr.setAuthCookie(writer, token)
hdlr.respondJSON(writer, request, map[string]any{
"id": sessionID,
"nick": nick,
"id": sessionID,
nickKey: nick,
}, http.StatusOK)
}
@@ -177,6 +177,6 @@ func (hdlr *Handlers) handlePass(
}
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
map[string]string{statusKey: "ok"},
http.StatusOK)
}
+9 -1
View File
@@ -24,6 +24,14 @@ import (
var errUnauthorized = errors.New("unauthorized")
// Field names and values used in many JSON responses.
const (
statusKey = "status"
statusError = "error"
errorKey = "error"
nickKey = "nick"
)
// Params defines the dependencies for creating Handlers.
type Params struct {
fx.In
@@ -137,7 +145,7 @@ func (hdlr *Handlers) respondError(
) {
hdlr.respondJSON(
writer, request,
map[string]string{"error": msg},
map[string]string{errorKey: msg},
status,
)
}
+7 -6
View File
@@ -100,9 +100,10 @@ func (v *Validator) Validate(
dateStr := parts[2]
resource := parts[3]
if err := v.validateHeader(
err := v.validateHeader(
version, bitsStr, resource, requiredBits,
); err != nil {
)
if err != nil {
return err
}
@@ -111,13 +112,13 @@ func (v *Validator) Validate(
return err
}
if err := validateTime(stampTime); err != nil {
err = validateTime(stampTime)
if err != nil {
return err
}
if err := validateProof(
stamp, requiredBits,
); err != nil {
err = validateProof(stamp, requiredBits)
if err != nil {
return err
}
+8 -15
View File
@@ -159,9 +159,7 @@ func (c *Conn) handleJoin(
return
}
channels := strings.Split(msg.Params[0], ",")
for _, chanName := range channels {
for chanName := range strings.SplitSeq(msg.Params[0], ",") {
chanName = strings.TrimSpace(chanName)
if !strings.HasPrefix(chanName, "#") {
@@ -305,9 +303,7 @@ func (c *Conn) handlePart(
reason = msg.Params[1]
}
channels := strings.Split(msg.Params[0], ",")
for _, ch := range channels {
for ch := range strings.SplitSeq(msg.Params[0], ",") {
ch = strings.TrimSpace(ch)
c.partChannel(ctx, ch, reason)
}
@@ -619,8 +615,8 @@ func (c *Conn) applyChannelModes(
) {
adding := true
argIdx := 0
applied := ""
appliedArgs := ""
var applied, appliedArgs strings.Builder
for _, modeChar := range modeStr {
var res modeResult
@@ -672,16 +668,13 @@ func (c *Conn) applyChannelModes(
argIdx += res.consumed
if !res.skip {
applied += res.applied
appliedArgs += res.appliedArgs
applied.WriteString(res.applied)
appliedArgs.WriteString(res.appliedArgs)
}
}
if applied != "" {
modeReply := applied
if appliedArgs != "" {
modeReply += appliedArgs
}
if applied.Len() > 0 {
modeReply := applied.String() + appliedArgs.String()
c.send(FormatMessage(
c.hostmask(), "MODE", channel, modeReply,
+4 -3
View File
@@ -62,7 +62,6 @@ type Conn struct {
lastQueueID int64
closed bool
cancel context.CancelFunc
}
func newConn(
@@ -151,8 +150,10 @@ func resolveHost(ctx context.Context, addr string) string {
}
// serve is the main loop for a single IRC client connection.
// Cancelling ctx when it returns stops the relay goroutine.
func (c *Conn) serve(ctx context.Context) {
ctx, c.cancel = context.WithCancel(ctx)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
defer c.cleanup(ctx)
scanner := bufio.NewScanner(c.conn)
@@ -481,7 +482,7 @@ func (c *Conn) deliverMOTD() {
"- %s Message of the Day -", c.serverSfx,
))
for _, line := range strings.Split(motd, "\n") {
for line := range strings.SplitSeq(motd, "\n") {
c.sendNumeric(irc.RplMotd, "- "+line)
}
+4
View File
@@ -282,6 +282,7 @@ func TestIntegrationTwoClients(t *testing.T) {
// Both nicks should appear in the name list.
foundBothNames := false
for _, line := range aliceNames {
if strings.Contains(line, " 353 ") &&
strings.Contains(line, "alice") &&
@@ -671,6 +672,7 @@ func TestIntegrationTwoClients(t *testing.T) {
})
foundPartErr := false
for _, line := range bobPartFail {
if strings.Contains(line, " 403 ") ||
strings.Contains(line, " 442 ") {
@@ -833,6 +835,7 @@ func TestIntegrationModeModerated(t *testing.T) {
})
foundModErr := false
for _, line := range bobLines {
if strings.Contains(line, " 404 ") ||
strings.Contains(line, " 482 ") {
@@ -859,6 +862,7 @@ func TestIntegrationModeModerated(t *testing.T) {
})
bob.send("PRIVMSG #modtest :voiced message")
aliceLines := alice.readUntil(func(l string) bool {
return strings.Contains(l, "voiced message")
})
+40 -28
View File
@@ -4,12 +4,18 @@ import (
"testing"
"sneak.berlin/go/neoirc/internal/ircserver"
"sneak.berlin/go/neoirc/pkg/irc"
)
//nolint:funlen // table-driven test
func TestParseMessage(t *testing.T) {
t.Parallel()
const (
nick = "alice"
channel = "#general"
)
tests := []struct {
name string
input string
@@ -24,10 +30,10 @@ func TestParseMessage(t *testing.T) {
},
{
name: "simple command",
input: "PING",
input: irc.CmdPing,
want: &ircserver.Message{
Prefix: "",
Command: "PING",
Command: irc.CmdPing,
Params: nil,
},
wantNil: false,
@@ -38,7 +44,7 @@ func TestParseMessage(t *testing.T) {
want: &ircserver.Message{
Prefix: "",
Command: "NICK",
Params: []string{"alice"},
Params: []string{nick},
},
wantNil: false,
},
@@ -57,8 +63,8 @@ func TestParseMessage(t *testing.T) {
input: "PRIVMSG #general :hello world",
want: &ircserver.Message{
Prefix: "",
Command: "PRIVMSG",
Params: []string{"#general", "hello world"},
Command: irc.CmdPrivmsg,
Params: []string{channel, "hello world"},
},
wantNil: false,
},
@@ -68,7 +74,7 @@ func TestParseMessage(t *testing.T) {
want: &ircserver.Message{
Prefix: "server.example.com",
Command: "001",
Params: []string{"alice", "Welcome to IRC"},
Params: []string{nick, "Welcome to IRC"},
},
wantNil: false,
},
@@ -79,7 +85,7 @@ func TestParseMessage(t *testing.T) {
Prefix: "",
Command: "USER",
Params: []string{
"alice", "0", "*", "Alice Smith",
nick, "0", "*", "Alice Smith",
},
},
wantNil: false,
@@ -90,7 +96,7 @@ func TestParseMessage(t *testing.T) {
want: &ircserver.Message{
Prefix: "",
Command: "JOIN",
Params: []string{"#general"},
Params: []string{channel},
},
wantNil: false,
},
@@ -99,17 +105,17 @@ func TestParseMessage(t *testing.T) {
input: "QUIT :leaving now",
want: &ircserver.Message{
Prefix: "",
Command: "QUIT",
Command: irc.CmdQuit,
Params: []string{"leaving now"},
},
wantNil: false,
},
{
name: "quit without reason",
input: "QUIT",
input: irc.CmdQuit,
want: &ircserver.Message{
Prefix: "",
Command: "QUIT",
Command: irc.CmdQuit,
Params: nil,
},
wantNil: false,
@@ -120,7 +126,7 @@ func TestParseMessage(t *testing.T) {
want: &ircserver.Message{
Prefix: "",
Command: "MODE",
Params: []string{"#general"},
Params: []string{channel},
},
wantNil: false,
},
@@ -131,7 +137,7 @@ func TestParseMessage(t *testing.T) {
Prefix: "",
Command: "KICK",
Params: []string{
"#general", "bob", "misbehaving",
channel, "bob", "misbehaving",
},
},
wantNil: false,
@@ -141,8 +147,8 @@ func TestParseMessage(t *testing.T) {
input: "PRIVMSG #general :",
want: &ircserver.Message{
Prefix: "",
Command: "PRIVMSG",
Params: []string{"#general", ""},
Command: irc.CmdPrivmsg,
Params: []string{channel, ""},
},
wantNil: false,
},
@@ -161,7 +167,7 @@ func TestParseMessage(t *testing.T) {
input: "PING :irc.example.com",
want: &ircserver.Message{
Prefix: "",
Command: "PING",
Command: irc.CmdPing,
Params: []string{"irc.example.com"},
},
wantNil: false,
@@ -173,7 +179,7 @@ func TestParseMessage(t *testing.T) {
Prefix: "",
Command: "TOPIC",
Params: []string{
"#general",
channel,
"Welcome to the channel!",
},
},
@@ -237,6 +243,12 @@ func TestParseMessage(t *testing.T) {
func TestFormatMessage(t *testing.T) {
t.Parallel()
const (
nick = "alice"
channel = "#general"
serverName = "server"
)
tests := []struct {
name string
prefix string
@@ -247,35 +259,35 @@ func TestFormatMessage(t *testing.T) {
{
name: "simple command",
prefix: "",
command: "PING",
command: irc.CmdPing,
params: nil,
want: "PING",
want: irc.CmdPing,
},
{
name: "with prefix",
prefix: "server",
prefix: serverName,
command: "PONG",
params: []string{"server"},
params: []string{serverName},
want: ":server PONG server",
},
{
name: "privmsg with trailing",
prefix: "alice!alice@host",
command: "PRIVMSG",
params: []string{"#general", "hello world"},
command: irc.CmdPrivmsg,
params: []string{channel, "hello world"},
want: ":alice!alice@host PRIVMSG #general :hello world",
},
{
name: "numeric reply",
prefix: "server",
prefix: serverName,
command: "001",
params: []string{"alice", "Welcome to IRC"},
params: []string{nick, "Welcome to IRC"},
want: ":server 001 alice :Welcome to IRC",
},
{
name: "empty trailing",
prefix: "server",
command: "PRIVMSG",
prefix: serverName,
command: irc.CmdPrivmsg,
params: []string{"#chan", ""},
want: ":server PRIVMSG #chan :",
},
@@ -302,7 +314,7 @@ func TestParseFormatRoundTrip(t *testing.T) {
// parameter either contains a space (gets ':' prefix
// on format) or is a non-trailing single token.
lines := []string{
"PING",
irc.CmdPing,
"NICK alice",
"PRIVMSG #general :hello world",
"JOIN #general",
+6 -4
View File
@@ -81,22 +81,24 @@ func New(
// start begins listening for TCP connections.
//
//nolint:contextcheck // long-lived server ctx, not the short Fx one
func (s *Server) start(_ context.Context, addr string) error {
ln, err := net.Listen("tcp", addr)
func (s *Server) start(ctx context.Context, addr string) error {
var listenConfig net.ListenConfig
ln, err := listenConfig.Listen(ctx, "tcp", addr)
if err != nil {
return fmt.Errorf("irc listen: %w", err)
}
s.listener = ln
ctx, cancel := context.WithCancel(context.Background())
serverCtx, cancel := context.WithCancel(context.Background())
s.cancel = cancel
s.log.Info(
"irc server listening", "addr", addr,
)
go s.acceptLoop(ctx)
go s.acceptLoop(serverCtx)
return nil
}
+8 -3
View File
@@ -71,7 +71,9 @@ func newTestEnv(t *testing.T) *testEnv {
MOTD: "Welcome to test IRC",
}
listener, err := net.Listen("tcp", "127.0.0.1:0")
var listenConfig net.ListenConfig
listener, err := listenConfig.Listen(t.Context(), "tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
@@ -116,10 +118,12 @@ func newTestEnv(t *testing.T) *testEnv {
func (env *testEnv) dial(t *testing.T) *testClient {
t.Helper()
conn, err := net.DialTimeout(
dialer := net.Dialer{Timeout: testTimeout}
conn, err := dialer.DialContext(
t.Context(),
"tcp",
env.srv.Listener().Addr().String(),
testTimeout,
)
if err != nil {
t.Fatalf("dial: %v", err)
@@ -323,6 +327,7 @@ func TestPrivmsgBetweenClients(t *testing.T) {
bob.joinAndDrain("#chat")
alice.send("PRIVMSG #chat :hello bob!")
lines := bob.sendAndExpect("PING :sync", "hello bob!")
assertContains(t, lines, "hello bob!", "channel PRIVMSG")
}
+3 -1
View File
@@ -78,7 +78,9 @@ func New(
srv.enableSentry()
srv.SetupRoutes()
go srv.serve() //nolint:contextcheck
// The start hook's context ends when the hook
// returns; serving must outlive it.
go srv.serve() //nolint:contextcheck,gosec // G118
return nil
},
+29 -28
View File
@@ -19,6 +19,12 @@ import (
"sneak.berlin/go/neoirc/pkg/irc"
)
// Error texts that several commands reply with.
const (
msgNoSuchChannel = "No such channel"
msgNotChannelOp = "You're not channel operator"
)
// Params defines the dependencies for creating a Service.
type Params struct {
fx.In
@@ -142,7 +148,7 @@ func (s *Service) SendChannelMessage(
return 0, "", &IRCError{
irc.ErrNoSuchChannel,
[]string{channel},
"No such channel",
msgNoSuchChannel,
}
}
@@ -256,10 +262,11 @@ func (s *Service) JoinChannel(
isCreator := countErr == nil && memberCount == 0
if !isCreator {
if joinErr := checkJoinRestrictions(
joinErr := checkJoinRestrictions(
ctx, s.db, chID, sessionID,
channel, suppliedKey, memberCount,
); joinErr != nil {
)
if joinErr != nil {
return nil, joinErr
}
}
@@ -306,7 +313,7 @@ func (s *Service) PartChannel(
return &IRCError{
irc.ErrNoSuchChannel,
[]string{channel},
"No such channel",
msgNoSuchChannel,
}
}
@@ -348,7 +355,7 @@ func (s *Service) SetTopic(
return &IRCError{
irc.ErrNoSuchChannel,
[]string{channel},
"No such channel",
msgNoSuchChannel,
}
}
@@ -372,14 +379,13 @@ func (s *Service) SetTopic(
return &IRCError{
irc.ErrChanOpPrivsNeeded,
[]string{channel},
"You're not channel operator",
msgNotChannelOp,
}
}
}
if setErr := s.db.SetTopic(
ctx, channel, topic,
); setErr != nil {
setErr := s.db.SetTopic(ctx, channel, topic)
if setErr != nil {
return fmt.Errorf("set topic: %w", setErr)
}
@@ -409,7 +415,7 @@ func (s *Service) KickUser(
return &IRCError{
irc.ErrNoSuchChannel,
[]string{channel},
"No such channel",
msgNoSuchChannel,
}
}
@@ -420,7 +426,7 @@ func (s *Service) KickUser(
return &IRCError{
irc.ErrChanOpPrivsNeeded,
[]string{channel},
"You're not channel operator",
msgNotChannelOp,
}
}
@@ -609,7 +615,7 @@ func (s *Service) ValidateChannelOp(
return 0, &IRCError{
irc.ErrNoSuchChannel,
[]string{channel},
"No such channel",
msgNoSuchChannel,
}
}
@@ -620,7 +626,7 @@ func (s *Service) ValidateChannelOp(
return 0, &IRCError{
irc.ErrChanOpPrivsNeeded,
[]string{channel},
"You're not channel operator",
msgNotChannelOp,
}
}
@@ -682,33 +688,28 @@ func (s *Service) SetChannelFlag(
) error {
switch flag {
case 'm':
if err := s.db.SetChannelModerated(
ctx, chID, setting,
); err != nil {
err := s.db.SetChannelModerated(ctx, chID, setting)
if err != nil {
return fmt.Errorf("set moderated: %w", err)
}
case 't':
if err := s.db.SetChannelTopicLocked(
ctx, chID, setting,
); err != nil {
err := s.db.SetChannelTopicLocked(ctx, chID, setting)
if err != nil {
return fmt.Errorf("set topic locked: %w", err)
}
case 'i':
if err := s.db.SetChannelInviteOnly(
ctx, chID, setting,
); err != nil {
err := s.db.SetChannelInviteOnly(ctx, chID, setting)
if err != nil {
return fmt.Errorf("set invite only: %w", err)
}
case 's':
if err := s.db.SetChannelSecret(
ctx, chID, setting,
); err != nil {
err := s.db.SetChannelSecret(ctx, chID, setting)
if err != nil {
return fmt.Errorf("set secret: %w", err)
}
case 'n':
if err := s.db.SetChannelNoExternal(
ctx, chID, setting,
); err != nil {
err := s.db.SetChannelNoExternal(ctx, chID, setting)
if err != nil {
return fmt.Errorf(
"set no external: %w", err,
)