Files
scrabble-game/backend/internal/config/config_test.go
T
Ilia Denisov eeaad62b10
Tests · Go / test (push) Successful in 11s
Tests · Integration / integration (push) Successful in 8s
Stage 1: backend foundation (Postgres, sessions, accounts, OTel)
- internal/postgres: pgx-over-database/sql pool (otelsql), embedded goose
  migrations into schema 'backend', committed go-jet code + cmd/jetgen tool.
- internal/account: durable accounts + unified telegram/email identities
  (UUIDv7 keys), find-or-create provisioning with unique-conflict handling.
- internal/session: opaque 256-bit tokens stored as a SHA-256 hash, revoke-only
  (no TTL); write-through cache gating /readyz; store + service.
- internal/telemetry: OTel tracer/meter providers (none/stdout) + request-timing
  middleware; internal/config gains Postgres + OTel env loading.
- internal/server: /api/v1 {public,user,internal,admin} skeleton + X-User-ID
  middleware; /readyz checks DB ping + cache; main wires
  telemetry -> db+migrate -> warm cache -> server.
- Tests: unit + integration (build tag 'integration', testcontainers
  postgres:17) for migrations, accounts, sessions, readyz; new integration.yaml.
- Docs: ARCHITECTURE, TESTING, PLAN refinements, root + backend READMEs.

Session/account REST handlers deferred to Stage 6 (gateway); OTLP + dashboards
to Stage 11.
2026-06-02 13:52:26 +02:00

123 lines
4.2 KiB
Go

package config
import (
"testing"
"time"
"scrabble/backend/internal/postgres"
"scrabble/backend/internal/telemetry"
)
// testDSN is a syntactically valid DSN used to satisfy the required-DSN check.
const testDSN = "postgres://u:p@localhost:5432/db?search_path=backend&sslmode=disable"
// TestLoadDefaults verifies that Load applies defaults when only the required
// DSN is set.
func TestLoadDefaults(t *testing.T) {
t.Setenv("BACKEND_HTTP_ADDR", "")
t.Setenv("BACKEND_LOG_LEVEL", "")
t.Setenv("BACKEND_POSTGRES_DSN", testDSN)
c, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if c.HTTPAddr != defaultHTTPAddr {
t.Errorf("HTTPAddr = %q, want %q", c.HTTPAddr, defaultHTTPAddr)
}
if c.LogLevel != defaultLogLevel {
t.Errorf("LogLevel = %q, want %q", c.LogLevel, defaultLogLevel)
}
if c.Postgres.DSN != testDSN {
t.Errorf("Postgres.DSN = %q, want %q", c.Postgres.DSN, testDSN)
}
if c.Postgres.MaxOpenConns != postgres.DefaultMaxOpenConns {
t.Errorf("Postgres.MaxOpenConns = %d, want %d", c.Postgres.MaxOpenConns, postgres.DefaultMaxOpenConns)
}
if c.Telemetry.ServiceName != telemetry.DefaultServiceName {
t.Errorf("Telemetry.ServiceName = %q, want %q", c.Telemetry.ServiceName, telemetry.DefaultServiceName)
}
if c.Telemetry.TracesExporter != telemetry.ExporterNone {
t.Errorf("Telemetry.TracesExporter = %q, want %q", c.Telemetry.TracesExporter, telemetry.ExporterNone)
}
}
// TestLoadOverrides verifies that environment variables override the defaults.
func TestLoadOverrides(t *testing.T) {
t.Setenv("BACKEND_POSTGRES_DSN", testDSN)
t.Setenv("BACKEND_HTTP_ADDR", "127.0.0.1:9090")
t.Setenv("BACKEND_LOG_LEVEL", "debug")
t.Setenv("BACKEND_POSTGRES_MAX_OPEN_CONNS", "7")
t.Setenv("BACKEND_POSTGRES_OPERATION_TIMEOUT", "3s")
t.Setenv("BACKEND_SERVICE_NAME", "scrabble-test")
t.Setenv("BACKEND_OTEL_TRACES_EXPORTER", "stdout")
c, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if c.HTTPAddr != "127.0.0.1:9090" {
t.Errorf("HTTPAddr = %q, want %q", c.HTTPAddr, "127.0.0.1:9090")
}
if c.LogLevel != "debug" {
t.Errorf("LogLevel = %q", c.LogLevel)
}
if c.Postgres.MaxOpenConns != 7 {
t.Errorf("Postgres.MaxOpenConns = %d, want 7", c.Postgres.MaxOpenConns)
}
if c.Postgres.OperationTimeout != 3*time.Second {
t.Errorf("Postgres.OperationTimeout = %s, want 3s", c.Postgres.OperationTimeout)
}
if c.Telemetry.ServiceName != "scrabble-test" {
t.Errorf("Telemetry.ServiceName = %q", c.Telemetry.ServiceName)
}
if c.Telemetry.TracesExporter != telemetry.ExporterStdout {
t.Errorf("Telemetry.TracesExporter = %q, want %q", c.Telemetry.TracesExporter, telemetry.ExporterStdout)
}
}
// TestLoadRejectsMissingDSN verifies that an empty DSN fails validation.
func TestLoadRejectsMissingDSN(t *testing.T) {
t.Setenv("BACKEND_POSTGRES_DSN", "")
if _, err := Load(); err == nil {
t.Fatal("Load: expected an error for a missing DSN, got nil")
}
}
// TestLoadRejectsInvalidLevel verifies that an unknown log level is rejected.
func TestLoadRejectsInvalidLevel(t *testing.T) {
t.Setenv("BACKEND_POSTGRES_DSN", testDSN)
t.Setenv("BACKEND_LOG_LEVEL", "verbose")
if _, err := Load(); err == nil {
t.Fatal("Load: expected an error for an invalid log level, got nil")
}
}
// TestLoadRejectsMalformedInt verifies that a non-numeric pool size is rejected.
func TestLoadRejectsMalformedInt(t *testing.T) {
t.Setenv("BACKEND_POSTGRES_DSN", testDSN)
t.Setenv("BACKEND_POSTGRES_MAX_OPEN_CONNS", "lots")
if _, err := Load(); err == nil {
t.Fatal("Load: expected an error for a malformed int, got nil")
}
}
// TestLoadRejectsMalformedDuration verifies that a malformed duration is rejected.
func TestLoadRejectsMalformedDuration(t *testing.T) {
t.Setenv("BACKEND_POSTGRES_DSN", testDSN)
t.Setenv("BACKEND_POSTGRES_OPERATION_TIMEOUT", "soon")
if _, err := Load(); err == nil {
t.Fatal("Load: expected an error for a malformed duration, got nil")
}
}
// TestLoadRejectsUnsupportedExporter verifies that an exporter outside the MVP
// set is rejected.
func TestLoadRejectsUnsupportedExporter(t *testing.T) {
t.Setenv("BACKEND_POSTGRES_DSN", testDSN)
t.Setenv("BACKEND_OTEL_TRACES_EXPORTER", "otlp")
if _, err := Load(); err == nil {
t.Fatal("Load: expected an error for an unsupported exporter, got nil")
}
}