// 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) } } }