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
This commit was merged in pull request #255.
This commit is contained in:
2026-11-03 11:59:17 +01:00
parent c12668226f
commit 8afd873813
12 changed files with 199 additions and 276 deletions
+1 -8
View File
@@ -10,13 +10,6 @@ import (
"golang.org/x/crypto/bcrypt"
)
//nolint:gochecknoglobals // var so tests can override via SetBcryptCost
var bcryptCost = bcrypt.DefaultCost
// SetBcryptCost overrides the bcrypt cost.
// Use bcrypt.MinCost in tests to avoid slow hashing.
func SetBcryptCost(cost int) { bcryptCost = cost }
var errNoPassword = errors.New(
"account has no password set",
)
@@ -29,7 +22,7 @@ func (database *Database) SetPassword(
password string,
) error {
hash, err := bcrypt.GenerateFromPassword(
[]byte(password), bcryptCost,
[]byte(password), bcrypt.DefaultCost,
)
if err != nil {
return fmt.Errorf("hash password: %w", err)
-5
View File
@@ -85,11 +85,6 @@ func New(
return database, nil
}
// GetDB returns the underlying sql.DB connection.
func (database *Database) GetDB() *sql.DB {
return database.conn
}
func (database *Database) connect(ctx context.Context) error {
dbURL := database.params.Config.DBURL
if dbURL == "" {
+76
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"log/slog"
"testing"
"time"
"sneak.berlin/go/neoirc/internal/dbtest"
)
@@ -67,3 +68,78 @@ func (database *Database) ExecForTest(
return nil
}
// GetDB returns the underlying sql.DB connection.
func (database *Database) GetDB() *sql.DB {
return database.conn
}
// GetOrCreateChannel returns channel id, creating if needed.
// Uses INSERT OR IGNORE to avoid TOCTOU races.
func (database *Database) GetOrCreateChannel(
ctx context.Context,
name string,
) (int64, error) {
now := time.Now()
_, err := database.conn.ExecContext(ctx,
`INSERT OR IGNORE INTO channels
(name, created_at, updated_at)
VALUES (?, ?, ?)`,
name, now, now)
if err != nil {
return 0, fmt.Errorf("create channel: %w", err)
}
var channelID int64
err = database.conn.QueryRowContext(
ctx,
"SELECT id FROM channels WHERE name = ?",
name,
).Scan(&channelID)
if err != nil {
return 0, fmt.Errorf("get channel: %w", err)
}
return channelID, nil
}
// JoinChannel adds a session to a channel.
func (database *Database) JoinChannel(
ctx context.Context,
channelID, sessionID int64,
) error {
_, err := database.conn.ExecContext(ctx,
`INSERT OR IGNORE INTO channel_members
(channel_id, session_id, joined_at)
VALUES (?, ?, ?)`,
channelID, sessionID, time.Now())
if err != nil {
return fmt.Errorf("join channel: %w", err)
}
return nil
}
// HasChannelInvite checks if a session has been invited
// to a channel.
func (database *Database) HasChannelInvite(
ctx context.Context,
channelID, sessionID int64,
) (bool, error) {
var count int
err := database.conn.QueryRowContext(ctx,
`SELECT COUNT(*) FROM channel_invites
WHERE channel_id = ? AND session_id = ?`,
channelID, sessionID,
).Scan(&count)
if err != nil {
return false, fmt.Errorf(
"check invite: %w", err,
)
}
return count > 0, nil
}
-14
View File
@@ -1,14 +0,0 @@
package db_test
import (
"os"
"testing"
"golang.org/x/crypto/bcrypt"
"sneak.berlin/go/neoirc/internal/db"
)
func TestMain(m *testing.M) {
db.SetBcryptCost(bcrypt.MinCost)
os.Exit(m.Run())
}
-93
View File
@@ -320,29 +320,6 @@ type ClientHostInfo struct {
Hostname string
}
// GetClientHostInfo returns the IP and hostname for a
// client.
func (database *Database) GetClientHostInfo(
ctx context.Context,
clientID int64,
) (*ClientHostInfo, error) {
var info ClientHostInfo
err := database.conn.QueryRowContext(
ctx,
`SELECT ip, hostname
FROM clients WHERE id = ?`,
clientID,
).Scan(&info.IP, &info.Hostname)
if err != nil {
return nil, fmt.Errorf(
"get client host info: %w", err,
)
}
return &info, nil
}
// SetSessionOper sets the is_oper flag on a session.
func (database *Database) SetSessionOper(
ctx context.Context,
@@ -433,54 +410,6 @@ func (database *Database) GetChannelByName(
return channelID, nil
}
// GetOrCreateChannel returns channel id, creating if needed.
// Uses INSERT OR IGNORE to avoid TOCTOU races.
func (database *Database) GetOrCreateChannel(
ctx context.Context,
name string,
) (int64, error) {
now := time.Now()
_, err := database.conn.ExecContext(ctx,
`INSERT OR IGNORE INTO channels
(name, created_at, updated_at)
VALUES (?, ?, ?)`,
name, now, now)
if err != nil {
return 0, fmt.Errorf("create channel: %w", err)
}
var channelID int64
err = database.conn.QueryRowContext(
ctx,
"SELECT id FROM channels WHERE name = ?",
name,
).Scan(&channelID)
if err != nil {
return 0, fmt.Errorf("get channel: %w", err)
}
return channelID, nil
}
// JoinChannel adds a session to a channel.
func (database *Database) JoinChannel(
ctx context.Context,
channelID, sessionID int64,
) error {
_, err := database.conn.ExecContext(ctx,
`INSERT OR IGNORE INTO channel_members
(channel_id, session_id, joined_at)
VALUES (?, ?, ?)`,
channelID, sessionID, time.Now())
if err != nil {
return fmt.Errorf("join channel: %w", err)
}
return nil
}
// Errors that CreateOrJoinChannel refuses a join with.
var (
ErrBannedFromChannel = errors.New("banned from channel")
@@ -2460,28 +2389,6 @@ func (database *Database) AddChannelInvite(
return nil
}
// HasChannelInvite checks if a session has been invited
// to a channel.
func (database *Database) HasChannelInvite(
ctx context.Context,
channelID, sessionID int64,
) (bool, error) {
var count int
err := database.conn.QueryRowContext(ctx,
`SELECT COUNT(*) FROM channel_invites
WHERE channel_id = ? AND session_id = ?`,
channelID, sessionID,
).Scan(&count)
if err != nil {
return false, fmt.Errorf(
"check invite: %w", err,
)
}
return count > 0, nil
}
// --- Tier 2: Secret (+s) ---
// IsChannelSecret checks if a channel has +s mode.
+3 -16
View File
@@ -125,7 +125,7 @@ func TestCreateSessionStoresIP(t *testing.T) {
database := setupTestDB(t)
ctx := t.Context()
sessionID, clientID, _, err := database.CreateSession(
sessionID, _, _, err := database.CreateSession(
ctx, "ipuser", "ident", "host.example.com",
"192.168.1.42",
)
@@ -147,8 +147,8 @@ func TestCreateSessionStoresIP(t *testing.T) {
)
}
clientInfo, err := database.GetClientHostInfo(
ctx, clientID,
clientInfo, err := database.GetLatestClientForSession(
ctx, sessionID,
)
if err != nil {
t.Fatal(err)
@@ -169,19 +169,6 @@ func TestCreateSessionStoresIP(t *testing.T) {
}
}
func TestGetClientHostInfoNotFound(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
_, err := database.GetClientHostInfo(
t.Context(), 99999,
)
if err == nil {
t.Fatal("expected error for nonexistent client")
}
}
func TestGetSessionHostInfoNotFound(t *testing.T) {
t.Parallel()
-25
View File
@@ -1,25 +0,0 @@
package db
import (
"context"
"database/sql"
"log/slog"
)
// NewTestDatabaseFromConn creates a Database wrapping an
// existing *sql.DB connection. Intended for integration
// tests in other packages.
func NewTestDatabaseFromConn(conn *sql.DB) *Database {
return &Database{ //nolint:exhaustruct
conn: conn,
log: slog.Default(),
}
}
// RunMigrations applies all schema migrations. Exposed
// for integration tests in other packages.
func (database *Database) RunMigrations(
ctx context.Context,
) error {
return database.runMigrations(ctx)
}