fetch_ml/tests/unit/logging/logging_test.go
Jeremie Fraeys ea15af1833 Fix multi-user authentication and clean up debug code
- Fix YAML tags in auth config struct (json -> yaml)
- Update CLI configs to use pre-hashed API keys
- Remove double hashing in WebSocket client
- Fix port mapping (9102 -> 9103) in CLI commands
- Update permission keys to use jobs:read, jobs:create, etc.
- Clean up all debug logging from CLI and server
- All user roles now authenticate correctly:
  * Admin: Can queue jobs and see all jobs
  * Researcher: Can queue jobs and see own jobs
  * Analyst: Can see status (read-only access)

Multi-user authentication is now fully functional.
2025-12-06 12:35:32 -05:00

182 lines
4.5 KiB
Go

package tests
import (
"context"
"io"
"log/slog"
"os"
"os/exec"
"strings"
"testing"
"github.com/jfraeys/fetch_ml/internal/logging"
)
type recordingHandler struct {
base []slog.Attr
last []slog.Attr
}
func TestLoggerFatalExits(t *testing.T) {
if os.Getenv("LOG_FATAL_TEST") == "1" {
logger := logging.NewLogger(slog.LevelInfo, false)
logger.Fatal("fatal message")
return
}
//nolint:gosec // G204: Subprocess launched with potential tainted input - this is a test
cmd := exec.CommandContext(context.Background(), os.Args[0], "-test.run", t.Name())
cmd.Env = append(os.Environ(), "LOG_FATAL_TEST=1")
if err := cmd.Run(); err == nil {
t.Fatalf("expected Fatal to exit with non-nil error")
}
}
func TestNewLoggerHonorsJSONFormatEnv(t *testing.T) {
t.Setenv("LOG_FORMAT", "json")
origStderr := os.Stderr
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create pipe: %v", err)
}
os.Stderr = w
defer func() {
_ = w.Close()
_ = r.Close()
os.Stderr = origStderr
}()
logger := logging.NewLogger(slog.LevelInfo, false)
logger.Info("hello", "key", "value")
_ = w.Close()
data, readErr := io.ReadAll(r)
if readErr != nil {
t.Fatalf("failed to read logger output: %v", readErr)
}
output := string(data)
if !strings.Contains(output, "\"msg\":\"hello\"") || !strings.Contains(output, "\"key\":\"value\"") {
t.Fatalf("expected json output, got %s", output)
}
}
func (h *recordingHandler) Enabled(_ context.Context, _ slog.Level) bool {
return true
}
func (h *recordingHandler) Handle(_ context.Context, r slog.Record) error {
// Reset last and include base attributes first
h.last = append([]slog.Attr{}, h.base...)
r.Attrs(func(a slog.Attr) bool {
h.last = append(h.last, a)
return true
})
return nil
}
func (h *recordingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
newBase := append([]slog.Attr{}, h.base...)
newBase = append(newBase, attrs...)
return &recordingHandler{base: newBase}
}
func (h *recordingHandler) WithGroup(_ string) slog.Handler {
return h
}
func attrsToMap(attrs []slog.Attr) map[string]any {
out := make(map[string]any, len(attrs))
for _, attr := range attrs {
out[attr.Key] = attr.Value.Any()
}
return out
}
func TestEnsureTraceAddsIDs(t *testing.T) {
ctx := context.Background()
ctx = logging.EnsureTrace(ctx)
if ctx.Value(logging.CtxTraceID) == nil {
t.Fatalf("expected trace id to be injected")
}
if ctx.Value(logging.CtxSpanID) == nil {
t.Fatalf("expected span id to be injected")
}
existingTrace := ctx.Value(logging.CtxTraceID)
ctx = logging.EnsureTrace(ctx)
if ctx.Value(logging.CtxTraceID) != existingTrace {
t.Fatalf("EnsureTrace should not overwrite existing trace id")
}
}
func TestLoggerWithContextIncludesValues(t *testing.T) {
handler := &recordingHandler{}
base := slog.New(handler)
logger := &logging.Logger{Logger: base}
ctx := context.Background()
ctx = context.WithValue(ctx, logging.CtxTraceID, "trace-123")
ctx = context.WithValue(ctx, logging.CtxSpanID, "span-456")
ctx = logging.CtxWithWorker(ctx, "worker-1")
ctx = logging.CtxWithJob(ctx, "job-a")
ctx = logging.CtxWithTask(ctx, "task-b")
child := logger.WithContext(ctx)
child.Info("hello")
rec, ok := child.Handler().(*recordingHandler)
if !ok {
t.Fatalf("expected recordingHandler, got %T", child.Handler())
}
fields := attrsToMap(rec.last)
expected := map[string]string{
"trace_id": "trace-123",
"span_id": "span-456",
"worker_id": "worker-1",
"job_name": "job-a",
"task_id": "task-b",
}
for key, want := range expected {
got, ok := fields[key]
if !ok {
t.Fatalf("expected attribute %s to be present", key)
}
if got != want {
t.Fatalf("attribute %s mismatch: want %s got %v", key, want, got)
}
}
}
func TestColorTextHandlerAddsColorAttr(t *testing.T) {
tmp, err := os.CreateTemp("", "log-output")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
t.Cleanup(func() {
defer func() { _ = tmp.Close() }()
defer func() { _ = os.Remove(tmp.Name()) }()
})
handler := logging.NewColorTextHandler(tmp, &slog.HandlerOptions{Level: slog.LevelInfo})
logger := slog.New(handler)
logger.Info("color test")
if err := tmp.Sync(); err != nil {
t.Fatalf("failed to sync temp file: %v", err)
}
data, err := os.ReadFile(tmp.Name())
if err != nil {
t.Fatalf("failed to read temp file: %v", err)
}
output := string(data)
if !strings.Contains(output, "lvl_color=\"\x1b[32mINF\x1b[0m\"") &&
!strings.Contains(output, "lvl_color=\"\\x1b[32mINF\\x1b[0m\"") {
t.Fatalf("expected info level color attribute, got: %s", output)
}
}