3 Commits
Author SHA1 Message Date
clawbot bc8aa6bb96 Remove the unused GetSessionChannelIDs from the db package (closes #254)
check / check (push) Waiting to run
Neither the server nor any test calls this method.

Model: opus-5-5
2026-11-03 12:38:42 +01:00
clawbot bc7ef8240b Keep Go's caches out of the test stage's image (closes #256)
check / check (push) Waiting to run
The test stage ran go mod download in its own layer, and go test left
Go's build cache in another. Exporting them took about 20 seconds of
every make test, and nothing reads either one afterwards.

Removing the build cache alone saved almost nothing, because the module
cache is the larger layer. So the download now runs in the same RUN as
go test, and go clean removes both caches once the tests pass. A failing
run still reruns with -v and exits 1.

Model: opus-5-5
2026-11-03 12:20:48 +01:00
clawbot 8afd873813 Move the test-only helpers out of the shipped db package (closes #248)
check / check (push) Waiting to run
The server binary carried exported names that only tests call, including
raw SQL access and a way to weaken the password hash.

Delete testing.go, GetDB and SetBcryptCost from the shipped code, so
passwords always hash at bcrypt.DefaultCost. Move GetDB, GetOrCreateChannel,
JoinChannel and HasChannelInvite into export_test.go. Delete
GetClientHostInfo, whose remaining check GetLatestClientForSession covers.

Tests in other packages now build the database with db.New and run raw SQL
through dbtest.Exec. Handler tests and the relay tests' aliceWithHTTPClient
helper need a password only to log in, so they store one hashed at
bcrypt.MinCost. Tests of PASS itself keep the real cost.

Model: opus-5-5
2026-11-03 11:59:17 +01:00
4 changed files with 53 additions and 56 deletions
+8 -4
View File
@@ -33,14 +33,18 @@ RUN golangci-lint run --config .golangci.yml ./...
# golang:1.24.13-bookworm, 2026-10-06
FROM golang@sha256:1a6d4452c65dea36aac2e2d606b01b4a029ec90cc1ae53890540ce6173ea77ac AS test
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css web/dist/app.js
# -p 4 because test runs on a shared build host cap their parallelism.
RUN go test -p 4 -timeout 90s -race -cover ./... || \
# The modules download in this RUN rather than a layer of their own, because
# exporting a layer that holds them is slow. Nothing reads them or the build
# cache afterwards, so go clean removes both.
RUN set -e; \
go mod download; \
go test -p 4 -timeout 90s -race -cover ./... || \
{ echo "--- Rerunning with -v for details ---"; \
go test -p 4 -timeout 90s -race -v ./...; exit 1; }
go test -p 4 -timeout 90s -race -v ./...; exit 1; }; \
go clean -cache -modcache
# Build stage. Nothing is wanted from either phase above; the copies
# are what make BuildKit build them first, so this stage cannot run
-17
View File
@@ -1056,23 +1056,6 @@ func (database *Database) GetChannelMemberIDs(
return scanInt64s(rows)
}
// GetSessionChannelIDs returns channel IDs for a session.
func (database *Database) GetSessionChannelIDs(
ctx context.Context,
sessionID int64,
) ([]int64, error) {
rows, err := database.conn.QueryContext(ctx,
`SELECT channel_id FROM channel_members
WHERE session_id = ?`, sessionID)
if err != nil {
return nil, fmt.Errorf(
"get session channel ids: %w", err,
)
}
return scanInt64s(rows)
}
// InsertMessage stores a message and returns its DB ID.
// toSessionID is the session a DM or INVITE is sent to and
// toChannelID the channel a channel message is sent to;
+28 -31
View File
@@ -23,6 +23,7 @@ import (
"go.uber.org/fx"
"go.uber.org/fx/fxtest"
"golang.org/x/crypto/bcrypt"
"modernc.org/sqlite"
"sneak.berlin/go/neoirc/internal/broker"
api "sneak.berlin/go/neoirc/internal/cli/api"
@@ -862,10 +863,7 @@ func TestRequestBodyLimit(t *testing.T) {
tserver := newTestServer(t)
token := tserver.createSession(nick)
tserver.sendCommand(token, map[string]any{
commandKey: irc.CmdPass,
bodyKey: []string{testPassword},
})
tserver.setPassword(nick)
bytesToLimit := maxMessageSize - len(endpoint.value)
@@ -1767,9 +1765,7 @@ func checkQuitRace(
firstToken := tserver.createSession(nick)
observerToken := tserver.createSession("raceobserver")
tserver.sendCommand(firstToken, map[string]any{
commandKey: irc.CmdPass, bodyKey: []string{testPassword},
})
tserver.setPassword(nick)
tokens := []string{firstToken, tserver.login(nick)}
@@ -1819,9 +1815,7 @@ func TestLogoutOfOneClientKeepsSession(t *testing.T) {
firstToken := tserver.createSession(nick)
observerToken := tserver.createSession("logoutobserver")
tserver.sendCommand(firstToken, map[string]any{
commandKey: irc.CmdPass, bodyKey: []string{testPassword},
})
tserver.setPassword(nick)
secondToken := tserver.login(nick)
@@ -2130,6 +2124,24 @@ func (tserver *testServer) longPoll(
return result.Messages, nil
}
// setPassword gives the session nick the password testPassword
// without sending PASS. It hashes at bcrypt.MinCost, and a login
// compares at the cost stored in the hash, so both are fast.
func (tserver *testServer) setPassword(nick string) {
tserver.t.Helper()
hash, err := bcrypt.GenerateFromPassword(
[]byte(testPassword), bcrypt.MinCost,
)
if err != nil {
tserver.t.Fatalf("hash password: %v", err)
}
dbtest.Exec(tserver.t, tserver.dbURL,
"UPDATE sessions SET password_hash = '"+string(hash)+
"' WHERE nick = '"+nick+"'")
}
// login logs in to the session nick with testPassword and
// returns the new client's token.
func (tserver *testServer) login(nick string) string {
@@ -3737,13 +3749,8 @@ func TestLoginValid(t *testing.T) {
tserver := newTestServer(t)
// Create session and set password via PASS command.
token := tserver.createSession("loginuser")
tserver.sendCommand(token, map[string]any{
commandKey: irc.CmdPass,
bodyKey: []string{testPassword},
})
tserver.createSession("loginuser")
tserver.setPassword("loginuser")
// Login with nick + password.
loginBody, err := json.Marshal(map[string]string{
@@ -3807,13 +3814,8 @@ func TestLoginWrongPassword(t *testing.T) {
tserver := newTestServer(t)
// Create session and set password via PASS command.
token := tserver.createSession("wrongpwuser")
tserver.sendCommand(token, map[string]any{
commandKey: irc.CmdPass,
bodyKey: []string{"correctpass1"},
})
tserver.createSession("wrongpwuser")
tserver.setPassword("wrongpwuser")
postJSONExpectStatus(
t, tserver, "/api/v1/login",
@@ -4284,13 +4286,8 @@ func TestLoginRateLimitAllowsNormalUse(t *testing.T) {
tserver := newTestServer(t)
// Create session and set password via PASS command.
token := tserver.createSession("normaluser")
tserver.sendCommand(token, map[string]any{
commandKey: irc.CmdPass,
bodyKey: []string{testPassword},
})
tserver.createSession("normaluser")
tserver.setPassword("normaluser")
// A single login should succeed without rate limiting.
loginBody, err := json.Marshal(map[string]string{
+17 -4
View File
@@ -8,6 +8,8 @@ import (
"testing"
"time"
"golang.org/x/crypto/bcrypt"
"sneak.berlin/go/neoirc/internal/dbtest"
"sneak.berlin/go/neoirc/internal/ircserver"
"sneak.berlin/go/neoirc/internal/service"
)
@@ -583,8 +585,10 @@ func ownSessionCopies(
// aliceWithHTTPClient connects alice to the IRC listener and
// has her join #chan, which bob is on too, then adds an HTTP API
// client to her session with the password her connection set,
// as POST /api/v1/login does. It returns her connection, bob's
// client to her session with a password, as POST /api/v1/login
// does. It stores that password without sending PASS. It hashes
// at bcrypt.MinCost, and a login compares at the cost stored in
// the hash, so both are fast. It returns her connection, bob's
// connection, her session and her HTTP API client.
func aliceWithHTTPClient(
t *testing.T, env *testEnv,
@@ -594,15 +598,24 @@ func aliceWithHTTPClient(
const password = "alicepass"
alice := env.dial(t)
alice.send("PASS " + password)
alice.register("alice")
// Registration stores the password before the JOIN runs.
alice.joinAndDrain("#chan")
bob := env.dial(t)
bob.register("bob")
bob.joinAndDrain("#chan")
hash, err := bcrypt.GenerateFromPassword(
[]byte(password), bcrypt.MinCost,
)
if err != nil {
t.Fatalf("hash password: %v", err)
}
dbtest.Exec(t, env.dbURL,
"UPDATE sessions SET password_hash = '"+string(hash)+
"' WHERE nick = 'alice'")
sessionID, httpClientID, _, err := env.database.LoginUser(
t.Context(), "alice", password, "127.0.0.1", "localhost",
)