Files
dpsapi/config_test.go
T
bryanpedini 88d7d6d2b9 feat(core): add configuration, logging and sqlite persistence
DB_PATH, HTTP_ADDR, HTTP_PORT and LOG_LEVEL parsing with validation; slog text logger with a TRACE level and token redaction; embedded idempotent schema on CGO-free SQLite (WAL, busy timeout); token store (create, list, deactivate, authenticate) and append-only audit store with ordered, limited and windowed queries; shared unit test helpers.
2026-10-06 10:52:25 +02:00

203 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 Bryan Joshua Pedini
package main
import (
"errors"
"log/slog"
"strings"
"testing"
)
func TestLoadConfigDefaults(t *testing.T) {
t.Parallel()
cfg, err := LoadConfig(envMap(map[string]string{"DB_PATH": "/data/dpsapi.db"}))
if err != nil {
t.Fatalf("LoadConfig: %v", err)
}
want := Config{DBPath: "/data/dpsapi.db", HTTPAddr: "0.0.0.0", HTTPPort: 80, LogLevel: slog.LevelInfo}
if cfg != want {
t.Fatalf("got %+v, want %+v", cfg, want)
}
if got := cfg.ListenAddress(); got != "0.0.0.0:80" {
t.Fatalf("ListenAddress = %q", got)
}
if got := cfg.ListenNetwork(); got != "tcp4" {
t.Fatalf("ListenNetwork = %q", got)
}
}
func TestLoadConfigExplicitValues(t *testing.T) {
t.Parallel()
cfg, err := LoadConfig(envMap(map[string]string{
"DB_PATH": "rel/path.db",
"HTTP_ADDR": "::1",
"HTTP_PORT": "8080",
"LOG_LEVEL": "trace",
}))
if err != nil {
t.Fatalf("LoadConfig: %v", err)
}
want := Config{DBPath: "rel/path.db", HTTPAddr: "::1", HTTPPort: 8080, LogLevel: LevelTrace}
if cfg != want {
t.Fatalf("got %+v, want %+v", cfg, want)
}
if got := cfg.ListenAddress(); got != "[::1]:8080" {
t.Fatalf("ListenAddress = %q", got)
}
if got := cfg.ListenNetwork(); got != "tcp" {
t.Fatalf("ListenNetwork = %q", got)
}
}
func TestLoadConfigEveryLogLevel(t *testing.T) {
t.Parallel()
cases := map[string]slog.Level{
"ERROR": slog.LevelError, "error": slog.LevelError, "Error": slog.LevelError,
"WARN": slog.LevelWarn, "warn": slog.LevelWarn, "wArN": slog.LevelWarn,
"INFO": slog.LevelInfo, "info": slog.LevelInfo,
"DEBUG": slog.LevelDebug, "debug": slog.LevelDebug,
"TRACE": LevelTrace, "trace": LevelTrace, "TrAcE": LevelTrace,
}
for in, want := range cases {
cfg, err := LoadConfig(envMap(map[string]string{"DB_PATH": "x", "LOG_LEVEL": in}))
if err != nil {
t.Errorf("LOG_LEVEL=%q: %v", in, err)
continue
}
if cfg.LogLevel != want {
t.Errorf("LOG_LEVEL=%q: got %v, want %v", in, cfg.LogLevel, want)
}
}
}
func TestLoadConfigInvalidLogLevels(t *testing.T) {
t.Parallel()
for _, in := range []string{
"WARNING", "ERR", "FATAL", "VERBOSE", "0", "-4", "INFO ", " INFO", "IN FO", "INFO\n",
"ınfo", // dotless i: Unicode case folding must not apply
"DEBUG-4", "trace1", "\x00", "INFO",
} {
_, err := LoadConfig(envMap(map[string]string{"DB_PATH": "x", "LOG_LEVEL": in}))
if !errors.Is(err, errConfig) {
t.Errorf("LOG_LEVEL=%q: expected config error, got %v", in, err)
continue
}
if !strings.Contains(err.Error(), "invalid LOG_LEVEL") {
t.Errorf("LOG_LEVEL=%q: unexpected message %q", in, err)
}
}
}
func TestLoadConfigMissingDBPath(t *testing.T) {
t.Parallel()
_, err := LoadConfig(envMap(map[string]string{"HTTP_PORT": "8080"}))
if !errors.Is(err, errConfig) || err.Error() != "configuration error: DB_PATH is required" {
t.Fatalf("got %v", err)
}
}
func TestLoadConfigEmptyValues(t *testing.T) {
t.Parallel()
for _, name := range []string{"DB_PATH", "HTTP_ADDR", "HTTP_PORT", "LOG_LEVEL"} {
env := map[string]string{"DB_PATH": "x"}
env[name] = ""
_, err := LoadConfig(envMap(env))
want := "configuration error: " + name + " must not be empty"
if !errors.Is(err, errConfig) || err.Error() != want {
t.Errorf("%s empty: got %v, want %q", name, err, want)
}
}
}
func TestLoadConfigInvalidPorts(t *testing.T) {
t.Parallel()
for _, in := range []string{
"0", "-1", "-80", "+80", "65536", "70000", "99999999999999999999999", "abc", "80a", "8 0",
" 80", "80 ", "0x50", "1e3", "80.0", "80", "\t80",
} {
_, err := LoadConfig(envMap(map[string]string{"DB_PATH": "x", "HTTP_PORT": in}))
if !errors.Is(err, errConfig) || !strings.Contains(err.Error(), "invalid HTTP_PORT") {
t.Errorf("HTTP_PORT=%q: expected invalid port error, got %v", in, err)
}
}
}
func TestLoadConfigValidEdgePorts(t *testing.T) {
t.Parallel()
cases := map[string]int{"1": 1, "80": 80, "1024": 1024, "65535": 65535, "0080": 80, "00001": 1}
for in, want := range cases {
cfg, err := LoadConfig(envMap(map[string]string{"DB_PATH": "x", "HTTP_PORT": in}))
if err != nil {
t.Errorf("HTTP_PORT=%q: %v", in, err)
continue
}
if cfg.HTTPPort != want {
t.Errorf("HTTP_PORT=%q: got %d, want %d", in, cfg.HTTPPort, want)
}
}
}
func TestParsePortEmpty(t *testing.T) {
t.Parallel()
if _, err := parsePort(""); err == nil {
t.Fatal("expected error for empty port")
}
}
func TestLoadConfigAddresses(t *testing.T) {
t.Parallel()
valid := map[string]string{
"0.0.0.0": "tcp4",
"127.0.0.1": "tcp4",
"192.168.1.10": "tcp4",
"::": "tcp",
"::1": "tcp",
"fe80::1": "tcp",
"2001:db8::1": "tcp",
"::ffff:127.0.0.1": "tcp4",
}
for in, network := range valid {
cfg, err := LoadConfig(envMap(map[string]string{"DB_PATH": "x", "HTTP_ADDR": in}))
if err != nil {
t.Errorf("HTTP_ADDR=%q: %v", in, err)
continue
}
if cfg.HTTPAddr != in || cfg.ListenNetwork() != network {
t.Errorf("HTTP_ADDR=%q: got addr %q network %q", in, cfg.HTTPAddr, cfg.ListenNetwork())
}
}
for _, in := range []string{
"localhost", "example.com", "256.0.0.1", "1.2.3", "01.2.3.4", " 127.0.0.1", "127.0.0.1 ",
"127.0.0.1:80", "[::1]", "fe80::1%eth0", "*", "0", "::g",
} {
_, err := LoadConfig(envMap(map[string]string{"DB_PATH": "x", "HTTP_ADDR": in}))
if !errors.Is(err, errConfig) || !strings.Contains(err.Error(), "invalid HTTP_ADDR") {
t.Errorf("HTTP_ADDR=%q: expected invalid address error, got %v", in, err)
}
}
}
func TestLoadConfigValidationOrder(t *testing.T) {
t.Parallel()
// Every variable invalid: DB_PATH is reported first, then HTTP_ADDR, HTTP_PORT, LOG_LEVEL
env := map[string]string{"HTTP_ADDR": "bad", "HTTP_PORT": "bad", "LOG_LEVEL": "bad"}
steps := []struct {
fix func()
want string
}{
{func() {}, "DB_PATH"},
{func() { env["DB_PATH"] = "x" }, "HTTP_ADDR"},
{func() { env["HTTP_ADDR"] = "127.0.0.1" }, "HTTP_PORT"},
{func() { env["HTTP_PORT"] = "1" }, "LOG_LEVEL"},
}
for _, step := range steps {
step.fix()
_, err := LoadConfig(envMap(env))
if err == nil || !strings.Contains(err.Error(), step.want) {
t.Fatalf("expected error about %s, got %v", step.want, err)
}
}
}