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.
This commit is contained in:
1 parent
9a85e9acc8
commit
b792ad256e
11 files changed
+2028
No files matched your search
+214
@@ -0,0 +1,214 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user