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 is contained in:
2026-11-03 10:23:41 +00:00
parent c12668226f
commit e44080fcbb
12 changed files with 199 additions and 276 deletions
+1 -8
View File
@@ -10,13 +10,6 @@ import (
"golang.org/x/crypto/bcrypt" "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( var errNoPassword = errors.New(
"account has no password set", "account has no password set",
) )
@@ -29,7 +22,7 @@ func (database *Database) SetPassword(
password string, password string,
) error { ) error {
hash, err := bcrypt.GenerateFromPassword( hash, err := bcrypt.GenerateFromPassword(
[]byte(password), bcryptCost, []byte(password), bcrypt.DefaultCost,
) )
if err != nil { if err != nil {
return fmt.Errorf("hash password: %w", err) return fmt.Errorf("hash password: %w", err)
-5
View File
@@ -85,11 +85,6 @@ func New(
return database, nil 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 { func (database *Database) connect(ctx context.Context) error {
dbURL := database.params.Config.DBURL dbURL := database.params.Config.DBURL
if dbURL == "" { if dbURL == "" {
+76
View File
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"testing" "testing"
"time"
"sneak.berlin/go/neoirc/internal/dbtest" "sneak.berlin/go/neoirc/internal/dbtest"
) )
@@ -67,3 +68,78 @@ func (database *Database) ExecForTest(
return nil 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 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. // SetSessionOper sets the is_oper flag on a session.
func (database *Database) SetSessionOper( func (database *Database) SetSessionOper(
ctx context.Context, ctx context.Context,
@@ -433,54 +410,6 @@ func (database *Database) GetChannelByName(
return channelID, nil 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. // Errors that CreateOrJoinChannel refuses a join with.
var ( var (
ErrBannedFromChannel = errors.New("banned from channel") ErrBannedFromChannel = errors.New("banned from channel")
@@ -2460,28 +2389,6 @@ func (database *Database) AddChannelInvite(
return nil 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) --- // --- Tier 2: Secret (+s) ---
// IsChannelSecret checks if a channel has +s mode. // IsChannelSecret checks if a channel has +s mode.
+3 -16
View File
@@ -125,7 +125,7 @@ func TestCreateSessionStoresIP(t *testing.T) {
database := setupTestDB(t) database := setupTestDB(t)
ctx := t.Context() ctx := t.Context()
sessionID, clientID, _, err := database.CreateSession( sessionID, _, _, err := database.CreateSession(
ctx, "ipuser", "ident", "host.example.com", ctx, "ipuser", "ident", "host.example.com",
"192.168.1.42", "192.168.1.42",
) )
@@ -147,8 +147,8 @@ func TestCreateSessionStoresIP(t *testing.T) {
) )
} }
clientInfo, err := database.GetClientHostInfo( clientInfo, err := database.GetLatestClientForSession(
ctx, clientID, ctx, sessionID,
) )
if err != nil { if err != nil {
t.Fatal(err) 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) { func TestGetSessionHostInfoNotFound(t *testing.T) {
t.Parallel() 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)
}
+22 -2
View File
@@ -1,5 +1,5 @@
// Package dbtest gives each test a database of its own. Only tests // Package dbtest gives each test a database of its own and runs raw
// may import it. // SQL on it. Only tests may import it.
package dbtest package dbtest
import ( import (
@@ -37,3 +37,23 @@ func NewURL(tb testing.TB) string {
return dbURL return dbURL
} }
// Exec runs one raw SQL statement on the database at dbURL, over a
// connection of its own. A test uses it for what the methods of
// db.Database cannot do, such as creating a trigger that makes a
// write fail.
func Exec(tb testing.TB, dbURL, statement string) {
tb.Helper()
pool, err := sql.Open("sqlite", dbURL)
if err != nil {
tb.Fatalf("open test database: %v", err)
}
defer func() { _ = pool.Close() }()
_, err = pool.ExecContext(tb.Context(), statement)
if err != nil {
tb.Fatalf("run %q: %v", statement, err)
}
}
+42 -44
View File
@@ -47,22 +47,25 @@ import (
const templateDBURL = "file:template?mode=memory&cache=shared" const templateDBURL = "file:template?mode=memory&cache=shared"
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
db.SetBcryptCost(bcrypt.MinCost) template := fx.New(
fx.NopLogger,
fx.Supply(
&globals.Globals{},
&config.Config{DBURL: templateDBURL},
),
fx.Provide(logger.New, newTestDB),
// fx makes the database only if something asks for it.
fx.Invoke(func(*db.Database) {}),
)
template, err := sql.Open("sqlite", templateDBURL) err := template.Start(context.Background())
if err != nil {
panic(err)
}
err = db.NewTestDatabaseFromConn(template).
RunMigrations(context.Background())
if err != nil { if err != nil {
panic(err) panic(err)
} }
code := m.Run() code := m.Run()
_ = template.Close() _ = template.Stop(context.Background())
os.Exit(code) os.Exit(code)
} }
@@ -137,6 +140,7 @@ type testServer struct {
t *testing.T t *testing.T
fxApp *fxtest.App fxApp *fxtest.App
broker *broker.Broker broker *broker.Broker
dbURL string
database *db.Database database *db.Database
handlers *handlers.Handlers handlers *handlers.Handlers
} }
@@ -231,6 +235,7 @@ func newTestServerWith(
t: t, t: t,
fxApp: app, fxApp: app,
broker: brk, broker: brk,
dbURL: dbURL,
database: database, database: database,
handlers: hdlr, handlers: hdlr,
} }
@@ -858,10 +863,7 @@ func TestRequestBodyLimit(t *testing.T) {
tserver := newTestServer(t) tserver := newTestServer(t)
token := tserver.createSession(nick) token := tserver.createSession(nick)
tserver.sendCommand(token, map[string]any{ tserver.setPassword(nick)
commandKey: irc.CmdPass,
bodyKey: []string{testPassword},
})
bytesToLimit := maxMessageSize - len(endpoint.value) bytesToLimit := maxMessageSize - len(endpoint.value)
@@ -1763,9 +1765,7 @@ func checkQuitRace(
firstToken := tserver.createSession(nick) firstToken := tserver.createSession(nick)
observerToken := tserver.createSession("raceobserver") observerToken := tserver.createSession("raceobserver")
tserver.sendCommand(firstToken, map[string]any{ tserver.setPassword(nick)
commandKey: irc.CmdPass, bodyKey: []string{testPassword},
})
tokens := []string{firstToken, tserver.login(nick)} tokens := []string{firstToken, tserver.login(nick)}
@@ -1815,9 +1815,7 @@ func TestLogoutOfOneClientKeepsSession(t *testing.T) {
firstToken := tserver.createSession(nick) firstToken := tserver.createSession(nick)
observerToken := tserver.createSession("logoutobserver") observerToken := tserver.createSession("logoutobserver")
tserver.sendCommand(firstToken, map[string]any{ tserver.setPassword(nick)
commandKey: irc.CmdPass, bodyKey: []string{testPassword},
})
secondToken := tserver.login(nick) secondToken := tserver.login(nick)
@@ -1887,15 +1885,12 @@ func TestFailedLogoutChangesNothing(t *testing.T) {
_, lastID := tserver.pollMessages(observerToken, 0) _, lastID := tserver.pollMessages(observerToken, 0)
_, err := tserver.database.GetDB().ExecContext(t.Context(), dbtest.Exec(t, tserver.dbURL,
`CREATE TRIGGER keep_sessions `CREATE TRIGGER keep_sessions
BEFORE DELETE ON sessions BEFORE DELETE ON sessions
BEGIN BEGIN
SELECT RAISE(ABORT, 'sessions are kept'); SELECT RAISE(ABORT, 'sessions are kept');
END`) END`)
if err != nil {
t.Fatal(err)
}
resp, err := doRequestAuth( resp, err := doRequestAuth(
t, http.MethodPost, tserver.url(apiLogout), token, nil, t, http.MethodPost, tserver.url(apiLogout), token, nil,
@@ -2129,6 +2124,24 @@ func (tserver *testServer) longPoll(
return result.Messages, nil 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 // login logs in to the session nick with testPassword and
// returns the new client's token. // returns the new client's token.
func (tserver *testServer) login(nick string) string { func (tserver *testServer) login(nick string) string {
@@ -3736,13 +3749,8 @@ func TestLoginValid(t *testing.T) {
tserver := newTestServer(t) tserver := newTestServer(t)
// Create session and set password via PASS command. tserver.createSession("loginuser")
token := tserver.createSession("loginuser") tserver.setPassword("loginuser")
tserver.sendCommand(token, map[string]any{
commandKey: irc.CmdPass,
bodyKey: []string{testPassword},
})
// Login with nick + password. // Login with nick + password.
loginBody, err := json.Marshal(map[string]string{ loginBody, err := json.Marshal(map[string]string{
@@ -3806,13 +3814,8 @@ func TestLoginWrongPassword(t *testing.T) {
tserver := newTestServer(t) tserver := newTestServer(t)
// Create session and set password via PASS command. tserver.createSession("wrongpwuser")
token := tserver.createSession("wrongpwuser") tserver.setPassword("wrongpwuser")
tserver.sendCommand(token, map[string]any{
commandKey: irc.CmdPass,
bodyKey: []string{"correctpass1"},
})
postJSONExpectStatus( postJSONExpectStatus(
t, tserver, "/api/v1/login", t, tserver, "/api/v1/login",
@@ -4283,13 +4286,8 @@ func TestLoginRateLimitAllowsNormalUse(t *testing.T) {
tserver := newTestServer(t) tserver := newTestServer(t)
// Create session and set password via PASS command. tserver.createSession("normaluser")
token := tserver.createSession("normaluser") tserver.setPassword("normaluser")
tserver.sendCommand(token, map[string]any{
commandKey: irc.CmdPass,
bodyKey: []string{testPassword},
})
// A single login should succeed without rate limiting. // A single login should succeed without rate limiting.
loginBody, err := json.Marshal(map[string]string{ loginBody, err := json.Marshal(map[string]string{
+17 -4
View File
@@ -8,6 +8,8 @@ import (
"testing" "testing"
"time" "time"
"golang.org/x/crypto/bcrypt"
"sneak.berlin/go/neoirc/internal/dbtest"
"sneak.berlin/go/neoirc/internal/ircserver" "sneak.berlin/go/neoirc/internal/ircserver"
"sneak.berlin/go/neoirc/internal/service" "sneak.berlin/go/neoirc/internal/service"
) )
@@ -583,8 +585,10 @@ func ownSessionCopies(
// aliceWithHTTPClient connects alice to the IRC listener and // aliceWithHTTPClient connects alice to the IRC listener and
// has her join #chan, which bob is on too, then adds an HTTP API // has her join #chan, which bob is on too, then adds an HTTP API
// client to her session with the password her connection set, // client to her session with a password, as POST /api/v1/login
// as POST /api/v1/login does. It returns her connection, bob's // 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. // connection, her session and her HTTP API client.
func aliceWithHTTPClient( func aliceWithHTTPClient(
t *testing.T, env *testEnv, t *testing.T, env *testEnv,
@@ -594,15 +598,24 @@ func aliceWithHTTPClient(
const password = "alicepass" const password = "alicepass"
alice := env.dial(t) alice := env.dial(t)
alice.send("PASS " + password)
alice.register("alice") alice.register("alice")
// Registration stores the password before the JOIN runs.
alice.joinAndDrain("#chan") alice.joinAndDrain("#chan")
bob := env.dial(t) bob := env.dial(t)
bob.register("bob") bob.register("bob")
bob.joinAndDrain("#chan") 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( sessionID, httpClientID, _, err := env.database.LoginUser(
t.Context(), "alice", password, "127.0.0.1", "localhost", t.Context(), "alice", password, "127.0.0.1", "localhost",
) )
+33 -49
View File
@@ -32,14 +32,9 @@ import (
const testTimeout = 5 * time.Second const testTimeout = 5 * time.Second
func TestMain(m *testing.M) {
db.SetBcryptCost(4)
os.Exit(m.Run())
}
// testEnv holds the shared test infrastructure. // testEnv holds the shared test infrastructure.
type testEnv struct { type testEnv struct {
dbURL string
database *db.Database database *db.Database
brk *broker.Broker brk *broker.Broker
cfg *config.Config cfg *config.Config
@@ -110,34 +105,27 @@ func newTestEnvWithResolver(
) *testEnv { ) *testEnv {
t.Helper() t.Helper()
conn, err := sql.Open("sqlite", dbtest.NewURL(t)) dbURL := dbtest.NewURL(t)
if err != nil { lifecycle := fxtest.NewLifecycle(t)
t.Fatalf("open db: %v", err)
}
t.Cleanup(func() { dbLog, err := logger.New(lifecycle, logger.Params{
err := conn.Close() Globals: &globals.Globals{},
if err != nil {
t.Logf("close db: %v", err)
}
}) })
conn.SetMaxOpenConns(1)
_, err = conn.ExecContext(
t.Context(), "PRAGMA foreign_keys = ON",
)
if err != nil { if err != nil {
t.Fatalf("pragma: %v", err) t.Fatalf("new logger: %v", err)
} }
database := db.NewTestDatabaseFromConn(conn) database, err := db.New(lifecycle, db.Params{
Logger: dbLog,
err = database.RunMigrations(t.Context()) Config: &config.Config{DBURL: dbURL},
})
if err != nil { if err != nil {
t.Fatalf("migrate: %v", err) t.Fatalf("new database: %v", err)
} }
lifecycle.RequireStart()
t.Cleanup(lifecycle.RequireStop)
brk := broker.New() brk := broker.New()
log := slog.New(slog.NewTextHandler( log := slog.New(slog.NewTextHandler(
@@ -161,6 +149,7 @@ func newTestEnvWithResolver(
t.Cleanup(srv.Stop) t.Cleanup(srv.Stop)
return &testEnv{ return &testEnv{
dbURL: dbURL,
database: database, database: database,
brk: brk, brk: brk,
cfg: cfg, cfg: cfg,
@@ -929,12 +918,7 @@ func quitSeenByPeers(t *testing.T, quit string) (string, []string) {
t.Fatalf("create peer session: %v", err) t.Fatalf("create peer session: %v", err)
} }
channelID, err := env.database.GetChannelByName(ctx, "#bye") _, _, err = env.database.CreateOrJoinChannel(ctx, "#bye", apiPeerID, "")
if err != nil {
t.Fatalf("get channel: %v", err)
}
err = env.database.JoinChannel(ctx, channelID, apiPeerID)
if err != nil { if err != nil {
t.Fatalf("join peer: %v", err) t.Fatalf("join peer: %v", err)
} }
@@ -1005,12 +989,9 @@ func TestStopEndsSessions(t *testing.T) {
t.Fatalf("create peer session: %v", err) t.Fatalf("create peer session: %v", err)
} }
channelID, err := env.database.GetChannelByName(ctx, "#shut") channelID, _, err := env.database.CreateOrJoinChannel(
if err != nil { ctx, "#shut", peerID, "",
t.Fatalf("get channel: %v", err) )
}
err = env.database.JoinChannel(ctx, channelID, peerID)
if err != nil { if err != nil {
t.Fatalf("join peer: %v", err) t.Fatalf("join peer: %v", err)
} }
@@ -1068,11 +1049,16 @@ func TestStopReturnsWhileCleanupIsStuck(t *testing.T) {
client.register("stuck") client.register("stuck")
// The test database has one connection. While the test // While a connection of the test's own holds a write
// holds it, the cleanup's first query waits for it. // transaction, the cleanup's first write waits for it to end.
held, err := env.database.GetDB().Conn(t.Context()) holder, err := sql.Open("sqlite", env.dbURL)
if err != nil { if err != nil {
t.Fatalf("take the database connection: %v", err) t.Fatalf("open a second database connection: %v", err)
}
_, err = holder.ExecContext(t.Context(), "BEGIN IMMEDIATE")
if err != nil {
t.Fatalf("begin a write transaction: %v", err)
} }
stopped := make(chan struct{}) stopped := make(chan struct{})
@@ -1088,10 +1074,11 @@ func TestStopReturnsWhileCleanupIsStuck(t *testing.T) {
t.Error("Stop did not return while a cleanup was stuck") t.Error("Stop did not return while a cleanup was stuck")
} }
// Lets the cleanup finish before the database is closed. // Closing ends the transaction, which lets the cleanup finish
err = held.Close() // before the database is closed.
err = holder.Close()
if err != nil { if err != nil {
t.Errorf("release the database connection: %v", err) t.Errorf("close the second database connection: %v", err)
} }
} }
@@ -1370,14 +1357,11 @@ func TestPassPostRegistrationNotStored(t *testing.T) {
client.register("failpass") client.register("failpass")
_, err := env.database.GetDB().ExecContext(t.Context(), dbtest.Exec(t, env.dbURL,
`CREATE TRIGGER refuse_password `CREATE TRIGGER refuse_password
BEFORE UPDATE OF password_hash ON sessions BEFORE UPDATE OF password_hash ON sessions
BEGIN SELECT RAISE(ABORT, 'refused'); END`, BEGIN SELECT RAISE(ABORT, 'refused'); END`,
) )
if err != nil {
t.Fatalf("create trigger: %v", err)
}
client.send("PASS :mypassword123") client.send("PASS :mypassword123")
+5 -16
View File
@@ -9,7 +9,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"os"
"slices" "slices"
"strings" "strings"
"sync" "sync"
@@ -18,7 +17,6 @@ import (
"go.uber.org/fx" "go.uber.org/fx"
"go.uber.org/fx/fxtest" "go.uber.org/fx/fxtest"
"golang.org/x/crypto/bcrypt"
"sneak.berlin/go/neoirc/internal/broker" "sneak.berlin/go/neoirc/internal/broker"
"sneak.berlin/go/neoirc/internal/config" "sneak.berlin/go/neoirc/internal/config"
"sneak.berlin/go/neoirc/internal/db" "sneak.berlin/go/neoirc/internal/db"
@@ -30,14 +28,10 @@ import (
"sneak.berlin/go/neoirc/pkg/irc" "sneak.berlin/go/neoirc/pkg/irc"
) )
func TestMain(m *testing.M) {
db.SetBcryptCost(bcrypt.MinCost)
os.Exit(m.Run())
}
// testEnv holds all dependencies for a service test. // testEnv holds all dependencies for a service test.
type testEnv struct { type testEnv struct {
svc *service.Service svc *service.Service
dbURL string
db *db.Database db *db.Database
broker *broker.Broker broker *broker.Broker
app *fxtest.App app *fxtest.App
@@ -111,6 +105,7 @@ func newTestEnv(t *testing.T) *testEnv {
return &testEnv{ return &testEnv{
svc: svc, svc: svc,
dbURL: dbURL,
db: database, db: database,
broker: brk, broker: brk,
app: app, app: app,
@@ -940,19 +935,16 @@ func TestSendDirectMessageTargetQuits(t *testing.T) {
sid := createSession(ctx, t, env.db, "alice") sid := createSession(ctx, t, env.db, "alice")
createSession(ctx, t, env.db, "bob") createSession(ctx, t, env.db, "bob")
_, err := env.db.GetDB().ExecContext(ctx, dbtest.Exec(t, env.dbURL,
`CREATE TRIGGER delete_recipient `CREATE TRIGGER delete_recipient
BEFORE INSERT ON messages BEFORE INSERT ON messages
BEGIN BEGIN
DELETE FROM sessions WHERE id = NEW.to_session_id; DELETE FROM sessions WHERE id = NEW.to_session_id;
END`) END`)
if err != nil {
t.Fatalf("create trigger: %v", err)
}
body, _ := json.Marshal([]string{"hi"}) //nolint:errchkjson body, _ := json.Marshal([]string{"hi"}) //nolint:errchkjson
_, err = env.svc.SendDirectMessage( _, err := env.svc.SendDirectMessage(
ctx, sid, 0, "alice", irc.CmdPrivmsg, "bob", body, nil, ctx, sid, 0, "alice", irc.CmdPrivmsg, "bob", body, nil,
) )
@@ -978,15 +970,12 @@ func TestSendChannelMessageChannelDeleted(t *testing.T) {
t.Fatalf("join alice: %v", err) t.Fatalf("join alice: %v", err)
} }
_, err = env.db.GetDB().ExecContext(ctx, dbtest.Exec(t, env.dbURL,
`CREATE TRIGGER delete_recipient `CREATE TRIGGER delete_recipient
BEFORE INSERT ON messages BEFORE INSERT ON messages
BEGIN BEGIN
DELETE FROM channels WHERE id = NEW.to_channel_id; DELETE FROM channels WHERE id = NEW.to_channel_id;
END`) END`)
if err != nil {
t.Fatalf("create trigger: %v", err)
}
body, _ := json.Marshal([]string{"hi"}) //nolint:errchkjson body, _ := json.Marshal([]string{"hi"}) //nolint:errchkjson