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.
203 lines
6.1 KiB
Go
203 lines
6.1 KiB
Go
// 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)
|
||
}
|
||
}
|
||
}
|