Files
OwnCord/Server/service/seed_test.go
T
Claude 5f1d6fc287 refactor(server): remove the store abstraction layer (D3)
Deletes Server/store (SQLiteStore, MemStore, the composed Store
interface) and collapses to a single sqlc-backed db package, executing
the prior audit's P4 "single data layer" direction (finding #6).

SQLiteStore was a pure pass-through to *db.DB, so consumers now depend
on narrow interfaces that *db.DB satisfies directly:

  - service.Store   (service/datastore.go, renamed from store/store.go)
  - ws.EventStore   (ws/eventstore.go)
  - plugin.PluginStore (plugin/pluginstore.go)

The event- and plugin-KV methods that lived in the store's SQLite
implementation move into the db package (db/event_queries.go,
db/plugin_queries.go), keeping their raw-SQL form.

Tests: the MemStore-based unit tests now run against a real in-memory
SQLite db opened per-test with migrations applied, via package-local
seed helpers. Fault-injection tests embed a real *db.DB and override the
single method under test, preserving error-path coverage. Full server
suite and sqlc-verify are green.

Docs: audit finding #6 and A-2026-07-06 marked resolved; decisions D3
updated; architecture server.md / data-model.md diagrams and prose
updated to the api -> service -> db layering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 16:33:58 +00:00

182 lines
5.2 KiB
Go

package service
import (
"strconv"
"testing"
"github.com/owncord/server/db"
)
// This file provides real in-memory SQLite test helpers for the service
// package. D3 removed the store abstraction (and its MemStore fake); service
// tests now run against a real *db.DB opened at ":memory:" with all migrations
// applied. The seed* helpers insert rows directly with explicit IDs so tests
// keep the fixed identifiers (user 1, channel 10, …) they relied on under the
// old fake. Migration 001 pre-seeds the four default roles, so seedRole upserts
// by id to let a test redefine a role's permission bits.
// newTestDB opens an in-memory database with the full migration set applied and
// registers cleanup. Each test gets an isolated database.
func newTestDB(t *testing.T) *db.DB {
t.Helper()
database, err := db.Open(":memory:")
if err != nil {
t.Fatalf("db.Open: %v", err)
}
t.Cleanup(func() { _ = database.Close() })
if err := db.Migrate(database); err != nil {
t.Fatalf("db.Migrate: %v", err)
}
return database
}
// seedRole upserts a role by id (overriding a default role's bits when the id
// collides with one of the migration-seeded defaults).
func seedRole(t *testing.T, database *db.DB, r *db.Role) {
t.Helper()
_, err := database.Exec(
`INSERT INTO roles (id, name, color, permissions, position, is_default)
VALUES (?, ?, ?, ?, ?, 0)
ON CONFLICT(id) DO UPDATE SET
name=excluded.name,
color=excluded.color,
permissions=excluded.permissions,
position=excluded.position`,
r.ID, r.Name, r.Color, r.Permissions, r.Position,
)
if err != nil {
t.Fatalf("seedRole(%d): %v", r.ID, err)
}
}
// seedUserRole assigns a role to a user, creating a minimal user row when one
// does not already exist. It preserves any identity fields set by an earlier
// seedUser call and only writes role_id.
func seedUserRole(t *testing.T, database *db.DB, userID, roleID int64) {
t.Helper()
_, err := database.Exec(
`INSERT INTO users (id, username, password, role_id)
VALUES (?, ?, '', ?)
ON CONFLICT(id) DO UPDATE SET role_id=excluded.role_id`,
userID, seedUsername(userID), roleID,
)
if err != nil {
t.Fatalf("seedUserRole(%d,%d): %v", userID, roleID, err)
}
}
// seedUser upserts a user's identity fields by id without clobbering a role_id
// assigned by a prior seedUserRole call.
func seedUser(t *testing.T, database *db.DB, u *db.User) {
t.Helper()
username := u.Username
if username == "" {
username = seedUsername(u.ID)
}
status := u.Status
if status == "" {
status = "offline"
}
var banReason any
if u.BanReason != nil {
banReason = *u.BanReason
}
var avatar any
if u.Avatar != nil {
avatar = *u.Avatar
}
banned := 0
if u.Banned {
banned = 1
}
_, err := database.Exec(
`INSERT INTO users (id, username, password, avatar, status, banned, ban_reason)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
username=excluded.username,
password=excluded.password,
avatar=excluded.avatar,
status=excluded.status,
banned=excluded.banned,
ban_reason=excluded.ban_reason`,
u.ID, username, u.PasswordHash, avatar, status, banned, banReason,
)
if err != nil {
t.Fatalf("seedUser(%d): %v", u.ID, err)
}
}
// seedChannel upserts a channel by id.
func seedChannel(t *testing.T, database *db.DB, ch *db.Channel) {
t.Helper()
ctype := ch.Type
if ctype == "" {
ctype = "text"
}
_, err := database.Exec(
`INSERT INTO channels (id, name, type, category, topic, position)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name=excluded.name,
type=excluded.type,
category=excluded.category,
topic=excluded.topic,
position=excluded.position`,
ch.ID, ch.Name, ctype, nullStr(ch.Category), nullStr(ch.Topic), ch.Position,
)
if err != nil {
t.Fatalf("seedChannel(%d): %v", ch.ID, err)
}
}
// seedChannelOverride sets a per-channel permission override for a role.
func seedChannelOverride(t *testing.T, database *db.DB, roleID, channelID, allow, deny int64) {
t.Helper()
_, err := database.Exec(
`INSERT INTO channel_overrides (channel_id, role_id, allow, deny)
VALUES (?, ?, ?, ?)
ON CONFLICT(channel_id, role_id) DO UPDATE SET
allow=excluded.allow,
deny=excluded.deny`,
channelID, roleID, allow, deny,
)
if err != nil {
t.Fatalf("seedChannelOverride(role=%d,chan=%d): %v", roleID, channelID, err)
}
}
// seedDMParticipant adds a user as a participant of a DM channel.
func seedDMParticipant(t *testing.T, database *db.DB, channelID, userID int64) {
t.Helper()
_, err := database.Exec(
`INSERT OR IGNORE INTO dm_participants (channel_id, user_id) VALUES (?, ?)`,
channelID, userID,
)
if err != nil {
t.Fatalf("seedDMParticipant(chan=%d,user=%d): %v", channelID, userID, err)
}
}
// seedBlock records that blockerID has blocked blockedID.
func seedBlock(t *testing.T, database *db.DB, blockerID, blockedID int64) {
t.Helper()
_, err := database.Exec(
`INSERT OR IGNORE INTO user_blocks (blocker_id, blocked_id) VALUES (?, ?)`,
blockerID, blockedID,
)
if err != nil {
t.Fatalf("seedBlock(%d,%d): %v", blockerID, blockedID, err)
}
}
func seedUsername(id int64) string {
return "seeduser" + strconv.FormatInt(id, 10)
}
func nullStr(s string) any {
if s == "" {
return nil
}
return s
}