Files
dpsapi/logger_test.go
T
bryanpedini b792ad256e feat(api): add authenticated containers http api
GET /api/v1/containers behind bearer token auth, JSON errors for 401/404/405/500, one audit row per authenticated request, graceful shutdown; unit and e2e tests for auth, routing, auditing, request logging and server lifecycle.
2026-10-06 10:52:25 +02:00

215 lines
6.7 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 (
"context"
"log/slog"
"math"
"regexp"
"strings"
"testing"
"time"
)
func TestLevelOrdering(t *testing.T) {
t.Parallel()
ordered := []slog.Level{LevelTrace, slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError}
for i := 1; i < len(ordered); i++ {
if !(ordered[i-1] < ordered[i]) {
t.Fatalf("level %v must be below %v", ordered[i-1], ordered[i])
}
}
if LevelTrace != slog.Level(-8) {
t.Fatalf("LevelTrace = %d", LevelTrace)
}
}
func TestParseLogLevel(t *testing.T) {
t.Parallel()
for name, want := range map[string]slog.Level{
"ERROR": slog.LevelError, "WARN": slog.LevelWarn, "INFO": slog.LevelInfo,
"DEBUG": slog.LevelDebug, "TRACE": LevelTrace, "trace": LevelTrace, "Debug": slog.LevelDebug,
} {
got, err := ParseLogLevel(name)
if err != nil || got != want {
t.Errorf("ParseLogLevel(%q) = %v, %v", name, got, err)
}
}
for _, name := range []string{"", "WARNING", "TRACEE", "TRAC", "ınfo"} {
if _, err := ParseLogLevel(name); err == nil {
t.Errorf("ParseLogLevel(%q) should fail", name)
}
}
}
func TestLevelName(t *testing.T) {
t.Parallel()
for level, want := range map[slog.Level]string{
slog.LevelError: "ERROR", slog.LevelWarn: "WARN", slog.LevelInfo: "INFO",
slog.LevelDebug: "DEBUG", LevelTrace: "TRACE", slog.Level(2): "INFO+2", slog.Level(-12): "DEBUG-8",
} {
if got := LevelName(level); got != want {
t.Errorf("LevelName(%d) = %q, want %q", level, got, want)
}
}
}
// emitAll writes one record per supported level
func emitAll(log *slog.Logger) {
ctx := context.Background()
trace(ctx, log, "trace-record")
log.Debug("debug-record")
log.Info("info-record")
log.Warn("warn-record")
log.Error("error-record")
}
func TestLoggerFiltering(t *testing.T) {
t.Parallel()
all := []string{"trace", "debug", "info", "warn", "error"}
levels := map[slog.Level]int{ // index of the lowest emitted record
LevelTrace: 0, slog.LevelDebug: 1, slog.LevelInfo: 2, slog.LevelWarn: 3, slog.LevelError: 4,
}
for level, lowest := range levels {
log, buf := newTestLogger(level)
emitAll(log)
out := buf.String()
for i, name := range all {
present := strings.Contains(out, "msg="+name+"-record")
if present != (i >= lowest) {
t.Errorf("level %s: record %s present=%v", LevelName(level), name, present)
}
if present && !strings.Contains(out, "level="+strings.ToUpper(name)+" msg="+name+"-record") {
t.Errorf("level %s: record %s has wrong level label:\n%s", LevelName(level), name, out)
}
}
}
}
func TestLoggerFormat(t *testing.T) {
t.Parallel()
log, buf := newTestLogger(LevelTrace)
trace(context.Background(), log, "hello", "k", "v")
line := strings.TrimSuffix(buf.String(), "\n")
re := regexp.MustCompile(`^time=\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z level=TRACE msg=hello k=v$`)
if !re.MatchString(line) {
t.Fatalf("unexpected line %q", line)
}
}
func TestLoggerEmitsRequestMessageAtInfo(t *testing.T) {
t.Parallel()
log, buf := newTestLogger(slog.LevelInfo)
s := NewWebServer(log, &fakeLookup{}, &fakeAudit{}, &fakeDocker{})
rec := doRequest(s, "GET", "/nope", nil)
if rec.Code != 404 {
t.Fatalf("status %d", rec.Code)
}
if !strings.Contains(buf.String(), `level=INFO msg="this request has been made"`) {
t.Fatalf("missing request message:\n%s", buf.String())
}
if strings.Contains(buf.String(), "level=DEBUG") || strings.Contains(buf.String(), "level=TRACE") {
t.Fatalf("lower-priority records leaked:\n%s", buf.String())
}
}
func TestTraceFuncEmitsEntryAndExit(t *testing.T) {
t.Parallel()
log, buf := newTestLogger(LevelTrace)
func() {
defer traceFunc(context.Background(), log, "Example")()
log.Debug("inside")
}()
lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
if len(lines) != 3 ||
!strings.Contains(lines[0], `level=TRACE msg="function entry" func=Example`) ||
!strings.Contains(lines[1], "msg=inside") ||
!strings.Contains(lines[2], `level=TRACE msg="function exit" func=Example`) {
t.Fatalf("unexpected output:\n%s", buf.String())
}
// Nothing is emitted above TRACE
quiet, qbuf := newTestLogger(slog.LevelDebug)
traceFunc(context.Background(), quiet, "Example")()
if qbuf.String() != "" {
t.Fatalf("trace emitted at DEBUG: %q", qbuf.String())
}
}
func TestLoggerUnusualFieldsDoNotPanic(t *testing.T) {
t.Parallel()
log, buf := newTestLogger(LevelTrace)
var nilPtr *int
log.Info("")
// Malformed argument lists are passed as slices on purpose (vet rejects them as literals)
log.Info("x", []any{""}...)
log.Info("x", "", "")
log.Info("x", []any{"odd"}...)
log.Info("x", []any{42, "non-string key"}...)
log.Info("x", "nil", nil, "nilptr", nilPtr)
log.Info("x", "level", "spoofed", "time", "not-a-time", "msg", "dup")
log.Info("x", "level", slog.LevelWarn)
log.Info("x", "nan", math.NaN(), "inf", math.Inf(1))
log.Info("x", "ctl", "a\nb\x00c\x1b[31m", "bad", string([]byte{0xff, 0xfe}))
log.Info("x", "unicode", "日本語 🚀", "long", strings.Repeat("y", 100000))
log.Info("x", slog.Group("g", "level", "inner"), slog.Any("map", map[string]any{"a": []int{1}}))
log.WithGroup("grp").Info("x", "level", "grouped", "time", time.Unix(0, 0))
log.Log(context.Background(), slog.Level(-100), "very low")
log.Log(context.Background(), slog.Level(100), "very high")
out := buf.String()
// 15 calls, of which the level -100 record is below TRACE and filtered
if c := strings.Count(out, "\n"); c != 14 {
t.Fatalf("expected 14 records, got %d:\n%s", c, out)
}
// Control characters are escaped, so one record is one line
if strings.Contains(out, "a\nb") {
t.Fatalf("raw newline leaked into log output")
}
if strings.Contains(out, "very low") || !strings.Contains(out, "level=ERROR+92") {
t.Fatalf("out-of-range levels not rendered:\n%s", out)
}
if !strings.Contains(out, "level=WARN") {
t.Fatalf("user level attr of type slog.Level not rendered by name")
}
}
func TestTokenID(t *testing.T) {
t.Parallel()
// sha256("abc") = ba7816bf...
if got := tokenID("abc"); got != "ba7816bf" {
t.Fatalf("tokenID(abc) = %q", got)
}
if tokenID("abc") != tokenID("abc") || tokenID("abc") == tokenID("abd") {
t.Fatal("tokenID must be stable and distinguish inputs")
}
if got := tokenID(""); len(got) != 8 {
t.Fatalf("tokenID('') = %q", got)
}
}
func TestASCIIEqualFold(t *testing.T) {
t.Parallel()
cases := []struct {
a, b string
want bool
}{
{"Bearer", "bearer", true},
{"BEARER", "bearer", true},
{"bearer", "bearer", true},
{"bearer", "bearer ", false},
{"bearerx", "bearer", false},
{"bearer", "bearex", false},
{"", "", true},
{"ı", "i", false},
{"[", "{", false},
{"@", "`", false},
}
for _, c := range cases {
if got := asciiEqualFold(c.a, c.b); got != c.want {
t.Errorf("asciiEqualFold(%q, %q) = %v", c.a, c.b, got)
}
}
}