Move the test-only helpers out of the shipped db package (closes #248)
check / check (push) Waiting to run
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 that need a password only to log in store one hashed at bcrypt.MinCost. Tests of PASS itself keep the real cost. Model: opus-5-5
This commit is contained in:
+1
-8
@@ -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)
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user