Files
chat/internal/db/queries_test.go
clawbot a57a73e94e
All checks were successful
check / check (push) Successful in 2m19s
fix: address all PR #10 review findings
Security:
- Add channel membership check before PRIVMSG (prevents non-members from sending)
- Add membership check on history endpoint (channels require membership, DMs scoped to own nick)
- Enforce MaxBytesReader on all POST request bodies
- Fix rand.Read error being silently ignored in token generation

Data integrity:
- Fix TOCTOU race in GetOrCreateChannel using INSERT OR IGNORE + SELECT

Build:
- Add CGO_ENABLED=0 to golangci-lint install in Dockerfile (fixes alpine build)

Linting:
- Strict .golangci.yml: only wsl disabled (deprecated in v2)
- Re-enable exhaustruct, depguard, godot, wrapcheck, varnamelen
- Fix linters-settings -> linters.settings for v2 config format
- Fix ALL lint findings in actual code (no linter config weakening)
- Wrap all external package errors (wrapcheck)
- Fill struct fields or add targeted nolint:exhaustruct where appropriate
- Rename short variables (ts->timestamp, n->bufIndex, etc.)
- Add depguard deny policy for io/ioutil and math/rand
- Exclude G704 (SSRF) in gosec config (CLI client takes user-configured URLs)

Tests:
- Add security tests (TestNonMemberCannotSend, TestHistoryNonMember)
- Split TestInsertAndPollMessages for reduced complexity
- Fix parallel test safety (viper global state prevents parallelism)
- Use t.Context() instead of context.Background() in tests

Docker build verified passing locally.
2026-02-26 21:21:49 -08:00

570 lines
9.6 KiB
Go

package db_test
import (
"encoding/json"
"testing"
"git.eeqj.de/sneak/chat/internal/db"
_ "modernc.org/sqlite"
)
func setupTestDB(t *testing.T) *db.Database {
t.Helper()
database, err := db.NewTestDatabase()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
closeErr := database.Close()
if closeErr != nil {
t.Logf("close db: %v", closeErr)
}
})
return database
}
func TestCreateUser(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
id, token, err := database.CreateUser(ctx, "alice")
if err != nil {
t.Fatal(err)
}
if id == 0 || token == "" {
t.Fatal("expected valid id and token")
}
_, _, err = database.CreateUser(ctx, "alice")
if err == nil {
t.Fatal("expected error for duplicate nick")
}
}
func TestGetUserByToken(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
_, token, err := database.CreateUser(ctx, "bob")
if err != nil {
t.Fatal(err)
}
id, nick, err := database.GetUserByToken(ctx, token)
if err != nil {
t.Fatal(err)
}
if nick != "bob" || id == 0 {
t.Fatalf("expected bob, got %s", nick)
}
_, _, err = database.GetUserByToken(ctx, "badtoken")
if err == nil {
t.Fatal("expected error for bad token")
}
}
func TestGetUserByNick(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
_, _, err := database.CreateUser(ctx, "charlie")
if err != nil {
t.Fatal(err)
}
id, err := database.GetUserByNick(ctx, "charlie")
if err != nil || id == 0 {
t.Fatal("expected to find charlie")
}
_, err = database.GetUserByNick(ctx, "nobody")
if err == nil {
t.Fatal("expected error for unknown nick")
}
}
func TestChannelOperations(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
chID, err := database.GetOrCreateChannel(ctx, "#test")
if err != nil || chID == 0 {
t.Fatal("expected channel id")
}
chID2, err := database.GetOrCreateChannel(ctx, "#test")
if err != nil || chID2 != chID {
t.Fatal("expected same channel id")
}
chID3, err := database.GetChannelByName(ctx, "#test")
if err != nil || chID3 != chID {
t.Fatal("expected same channel id")
}
_, err = database.GetChannelByName(ctx, "#nope")
if err == nil {
t.Fatal("expected error for nonexistent channel")
}
}
func TestJoinAndPart(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
uid, _, err := database.CreateUser(ctx, "user1")
if err != nil {
t.Fatal(err)
}
chID, err := database.GetOrCreateChannel(ctx, "#chan")
if err != nil {
t.Fatal(err)
}
err = database.JoinChannel(ctx, chID, uid)
if err != nil {
t.Fatal(err)
}
ids, err := database.GetChannelMemberIDs(ctx, chID)
if err != nil || len(ids) != 1 || ids[0] != uid {
t.Fatal("expected user in channel")
}
err = database.JoinChannel(ctx, chID, uid)
if err != nil {
t.Fatal(err)
}
err = database.PartChannel(ctx, chID, uid)
if err != nil {
t.Fatal(err)
}
ids, _ = database.GetChannelMemberIDs(ctx, chID)
if len(ids) != 0 {
t.Fatal("expected empty channel")
}
}
func TestDeleteChannelIfEmpty(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
chID, err := database.GetOrCreateChannel(
ctx, "#empty",
)
if err != nil {
t.Fatal(err)
}
uid, _, err := database.CreateUser(ctx, "temp")
if err != nil {
t.Fatal(err)
}
err = database.JoinChannel(ctx, chID, uid)
if err != nil {
t.Fatal(err)
}
err = database.PartChannel(ctx, chID, uid)
if err != nil {
t.Fatal(err)
}
err = database.DeleteChannelIfEmpty(ctx, chID)
if err != nil {
t.Fatal(err)
}
_, err = database.GetChannelByName(ctx, "#empty")
if err == nil {
t.Fatal("expected channel to be deleted")
}
}
func createUserWithChannels(
t *testing.T,
database *db.Database,
nick, ch1Name, ch2Name string,
) (int64, int64, int64) {
t.Helper()
ctx := t.Context()
uid, _, err := database.CreateUser(ctx, nick)
if err != nil {
t.Fatal(err)
}
ch1, err := database.GetOrCreateChannel(
ctx, ch1Name,
)
if err != nil {
t.Fatal(err)
}
ch2, err := database.GetOrCreateChannel(
ctx, ch2Name,
)
if err != nil {
t.Fatal(err)
}
err = database.JoinChannel(ctx, ch1, uid)
if err != nil {
t.Fatal(err)
}
err = database.JoinChannel(ctx, ch2, uid)
if err != nil {
t.Fatal(err)
}
return uid, ch1, ch2
}
func TestListChannels(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
uid, _, _ := createUserWithChannels(
t, database, "lister", "#a", "#b",
)
channels, err := database.ListChannels(
t.Context(), uid,
)
if err != nil || len(channels) != 2 {
t.Fatalf(
"expected 2 channels, got %d",
len(channels),
)
}
}
func TestListAllChannels(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
_, err := database.GetOrCreateChannel(ctx, "#x")
if err != nil {
t.Fatal(err)
}
_, err = database.GetOrCreateChannel(ctx, "#y")
if err != nil {
t.Fatal(err)
}
channels, err := database.ListAllChannels(ctx)
if err != nil || len(channels) < 2 {
t.Fatalf(
"expected >= 2 channels, got %d",
len(channels),
)
}
}
func TestChangeNick(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
uid, token, err := database.CreateUser(ctx, "old")
if err != nil {
t.Fatal(err)
}
err = database.ChangeNick(ctx, uid, "new")
if err != nil {
t.Fatal(err)
}
_, nick, err := database.GetUserByToken(ctx, token)
if err != nil {
t.Fatal(err)
}
if nick != "new" {
t.Fatalf("expected new, got %s", nick)
}
}
func TestSetTopic(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
_, err := database.GetOrCreateChannel(
ctx, "#topictest",
)
if err != nil {
t.Fatal(err)
}
err = database.SetTopic(ctx, "#topictest", "Hello")
if err != nil {
t.Fatal(err)
}
channels, err := database.ListAllChannels(ctx)
if err != nil {
t.Fatal(err)
}
for _, ch := range channels {
if ch.Name == "#topictest" &&
ch.Topic != "Hello" {
t.Fatalf(
"expected topic Hello, got %s",
ch.Topic,
)
}
}
}
func TestInsertMessage(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
body := json.RawMessage(`["hello"]`)
dbID, msgUUID, err := database.InsertMessage(
ctx, "PRIVMSG", "poller", "#test", body, nil,
)
if err != nil {
t.Fatal(err)
}
if dbID == 0 || msgUUID == "" {
t.Fatal("expected valid id and uuid")
}
}
func TestPollMessages(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
uid, _, err := database.CreateUser(ctx, "poller")
if err != nil {
t.Fatal(err)
}
body := json.RawMessage(`["hello"]`)
dbID, _, err := database.InsertMessage(
ctx, "PRIVMSG", "poller", "#test", body, nil,
)
if err != nil {
t.Fatal(err)
}
err = database.EnqueueMessage(ctx, uid, dbID)
if err != nil {
t.Fatal(err)
}
const batchSize = 10
msgs, lastQID, err := database.PollMessages(
ctx, uid, 0, batchSize,
)
if err != nil {
t.Fatal(err)
}
if len(msgs) != 1 {
t.Fatalf(
"expected 1 message, got %d", len(msgs),
)
}
if msgs[0].Command != "PRIVMSG" {
t.Fatalf(
"expected PRIVMSG, got %s", msgs[0].Command,
)
}
if lastQID == 0 {
t.Fatal("expected nonzero lastQID")
}
msgs, _, _ = database.PollMessages(
ctx, uid, lastQID, batchSize,
)
if len(msgs) != 0 {
t.Fatalf(
"expected 0 messages, got %d", len(msgs),
)
}
}
func TestGetHistory(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
const msgCount = 10
for range msgCount {
_, _, err := database.InsertMessage(
ctx, "PRIVMSG", "user", "#hist",
json.RawMessage(`["msg"]`), nil,
)
if err != nil {
t.Fatal(err)
}
}
const histLimit = 5
msgs, err := database.GetHistory(
ctx, "#hist", 0, histLimit,
)
if err != nil {
t.Fatal(err)
}
if len(msgs) != histLimit {
t.Fatalf("expected %d, got %d",
histLimit, len(msgs))
}
if msgs[0].DBID > msgs[histLimit-1].DBID {
t.Fatal("expected ascending order")
}
}
func TestDeleteUser(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
uid, _, err := database.CreateUser(ctx, "deleteme")
if err != nil {
t.Fatal(err)
}
chID, err := database.GetOrCreateChannel(
ctx, "#delchan",
)
if err != nil {
t.Fatal(err)
}
err = database.JoinChannel(ctx, chID, uid)
if err != nil {
t.Fatal(err)
}
err = database.DeleteUser(ctx, uid)
if err != nil {
t.Fatal(err)
}
_, err = database.GetUserByNick(ctx, "deleteme")
if err == nil {
t.Fatal("user should be deleted")
}
ids, _ := database.GetChannelMemberIDs(ctx, chID)
if len(ids) != 0 {
t.Fatal("expected no members after deletion")
}
}
func TestChannelMembers(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
ctx := t.Context()
uid1, _, err := database.CreateUser(ctx, "m1")
if err != nil {
t.Fatal(err)
}
uid2, _, err := database.CreateUser(ctx, "m2")
if err != nil {
t.Fatal(err)
}
chID, err := database.GetOrCreateChannel(
ctx, "#members",
)
if err != nil {
t.Fatal(err)
}
err = database.JoinChannel(ctx, chID, uid1)
if err != nil {
t.Fatal(err)
}
err = database.JoinChannel(ctx, chID, uid2)
if err != nil {
t.Fatal(err)
}
members, err := database.ChannelMembers(ctx, chID)
if err != nil || len(members) != 2 {
t.Fatalf(
"expected 2 members, got %d",
len(members),
)
}
}
func TestGetAllChannelMembershipsForUser(t *testing.T) {
t.Parallel()
database := setupTestDB(t)
uid, _, _ := createUserWithChannels(
t, database, "multi", "#m1", "#m2",
)
channels, err :=
database.GetAllChannelMembershipsForUser(
t.Context(), uid,
)
if err != nil || len(channels) != 2 {
t.Fatalf(
"expected 2 channels, got %d",
len(channels),
)
}
}