feat(server): скелет Go-сервера с healthz, readyz и api/v1/meta
- internal/config: конфигурация из env с валидацией и дефолтами (§5.1, §9.1) - internal/httpx: middleware (request id, slog-логи, recover, security-заголовки), единый формат ошибок §8.5 и лимит тела запроса - internal/database: SQLite WAL + busy_timeout + foreign_keys, один writer и пул чтения, goose-миграции embedded, PRAGMA optimize по расписанию - internal/meta: полезная нагрузка /api/v1/meta (версия, фичи, лимиты) - internal/server: маршруты, SPA-раздача собранного клиента, OpenAPI 3.1 - cmd/glchat: точка входа с graceful shutdown и JSON-логами - тесты: конфиг, миграции и идемпотентность, HTTP-эндпоинты, статика, 404/405
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpenAppliesPragmasAndMigrates(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
path := filepath.Join(t.TempDir(), "db", "glchat.db")
|
||||
|
||||
db, err := Open(ctx, Options{Path: path, ReadPool: 2, Migrate: true})
|
||||
if err != nil {
|
||||
t.Fatalf("Open() returned error: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
journal, err := db.JournalMode(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("JournalMode() returned error: %v", err)
|
||||
}
|
||||
if journal != "wal" {
|
||||
t.Errorf("journal mode = %q, want wal", journal)
|
||||
}
|
||||
|
||||
version, err := db.SchemaVersion(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("SchemaVersion() returned error: %v", err)
|
||||
}
|
||||
if version != 1 {
|
||||
t.Errorf("schema version = %d, want 1", version)
|
||||
}
|
||||
|
||||
var foreignKeys int
|
||||
if err := db.Writer.QueryRowContext(ctx, "PRAGMA foreign_keys").Scan(&foreignKeys); err != nil {
|
||||
t.Fatalf("read foreign_keys pragma: %v", err)
|
||||
}
|
||||
if foreignKeys != 1 {
|
||||
t.Errorf("foreign_keys = %d, want 1", foreignKeys)
|
||||
}
|
||||
|
||||
var baseline string
|
||||
if err := db.Reader.QueryRowContext(ctx,
|
||||
`SELECT value FROM instance_settings WHERE key = 'registration_enabled'`,
|
||||
).Scan(&baseline); err != nil {
|
||||
t.Fatalf("read instance setting: %v", err)
|
||||
}
|
||||
if baseline != "true" {
|
||||
t.Errorf("registration_enabled = %q, want true", baseline)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateIsIdempotent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
path := filepath.Join(t.TempDir(), "glchat.db")
|
||||
|
||||
db, err := Open(ctx, Options{Path: path, ReadPool: 2, Migrate: true})
|
||||
if err != nil {
|
||||
t.Fatalf("first Open() returned error: %v", err)
|
||||
}
|
||||
if err := db.Migrate(ctx); err != nil {
|
||||
t.Fatalf("second Migrate() returned error: %v", err)
|
||||
}
|
||||
|
||||
var applied int
|
||||
if err := db.Reader.QueryRowContext(ctx,
|
||||
`SELECT COUNT(*) FROM goose_db_version WHERE version_id = 1 AND is_applied = 1`,
|
||||
).Scan(&applied); err != nil {
|
||||
t.Fatalf("count applied migrations: %v", err)
|
||||
}
|
||||
if applied != 1 {
|
||||
t.Errorf("migration 1 applied %d times, want 1", applied)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatalf("Close() returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenRejectsEmptyPath(t *testing.T) {
|
||||
if _, err := Open(context.Background(), Options{}); err == nil {
|
||||
t.Fatal("Open() accepted an empty path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadyFailsOnClosedDatabase(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db, err := Open(ctx, Options{Path: filepath.Join(t.TempDir(), "glchat.db"), Migrate: true})
|
||||
if err != nil {
|
||||
t.Fatalf("Open() returned error: %v", err)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatalf("Close() returned error: %v", err)
|
||||
}
|
||||
if err := db.Ready(ctx); err == nil {
|
||||
t.Fatal("Ready() returned nil for a closed database")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user