Greenfield Go multi-tenant IPFS Pinning Service wire-compatible with the
IPFS Pinning Services API spec. Paired 1:1 with Kubo over localhost RPC,
clustered via embedded NATS JetStream, Postgres source-of-truth with
RLS-enforced tenancy, Fiber + huma v2 for the HTTP surface, Authentik
OIDC for session login with kid-rotated HS256 JWT API tokens.
Feature-complete against the 22-milestone build plan, including the
ship-it v1.0 gap items:
* admin CLIs: drain/uncordon, maintenance, mint-token, rotate-key,
prune-denylist, rebalance --dry-run, cache-stats, cluster-presences
* TTL leader election via NATS KV, fence tokens, JetStream dedup
* rebalancer (plan/apply split), reconciler, requeue sweeper
* ristretto caches with NATS-backed cross-node invalidation
(placements live-nodes + token denylist)
* maintenance watchdog for stuck cluster-pause flag
* Prometheus /metrics with CIDR ACL, HTTP/pin/scheduler/cache gauges
* rate limiting: session (10/min) + anonymous global (120/min)
* integration tests: rebalance, refcount multi-org, RLS belt
* goreleaser (tar + deb/rpm/apk + Alpine Docker) targeting Gitea
Stack: Cobra/Viper, Fiber v2 + huma v2, embedded NATS JetStream,
pgx/sqlc/golang-migrate, ristretto, TypeID, prometheus/client_golang,
testcontainers-go.
175 lines
5.1 KiB
Go
175 lines
5.1 KiB
Go
package logging
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseLevel(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want slog.Level
|
|
err bool
|
|
}{
|
|
{"debug", slog.LevelDebug, false},
|
|
{"info", slog.LevelInfo, false},
|
|
{"warn", slog.LevelWarn, false},
|
|
{"warning", slog.LevelWarn, false},
|
|
{"error", slog.LevelError, false},
|
|
{"", slog.LevelInfo, false},
|
|
{"DEBUG", slog.LevelDebug, false},
|
|
{"INFO", slog.LevelInfo, false},
|
|
{"WARN", slog.LevelWarn, false},
|
|
{"ERROR", slog.LevelError, false},
|
|
{"invalid", slog.LevelInfo, true},
|
|
{"trace", slog.LevelInfo, true},
|
|
{"fatal", slog.LevelInfo, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
got, err := parseLevel(tt.input)
|
|
if tt.err && err == nil {
|
|
t.Errorf("parseLevel(%q) expected error, got nil", tt.input)
|
|
}
|
|
if !tt.err && err != nil {
|
|
t.Errorf("parseLevel(%q) unexpected error: %v", tt.input, err)
|
|
}
|
|
if !tt.err && got != tt.want {
|
|
t.Errorf("parseLevel(%q) = %v, want %v", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// recordingHandler captures log records for testing.
|
|
type recordingHandler struct {
|
|
records []slog.Record
|
|
level slog.Level
|
|
}
|
|
|
|
func (h *recordingHandler) Enabled(_ context.Context, level slog.Level) bool {
|
|
return level >= h.level
|
|
}
|
|
|
|
func (h *recordingHandler) Handle(_ context.Context, r slog.Record) error {
|
|
h.records = append(h.records, r)
|
|
return nil
|
|
}
|
|
|
|
func (h *recordingHandler) WithAttrs(_ []slog.Attr) slog.Handler { return h }
|
|
func (h *recordingHandler) WithGroup(_ string) slog.Handler { return h }
|
|
|
|
func TestFanoutHandlerDispatchesBothHandlers(t *testing.T) {
|
|
all := &recordingHandler{level: slog.LevelDebug}
|
|
errOnly := &recordingHandler{level: slog.LevelWarn}
|
|
|
|
handler := &fanoutHandler{handlers: []slog.Handler{all, errOnly}}
|
|
logger := slog.New(handler)
|
|
|
|
logger.Debug("debug msg")
|
|
logger.Info("info msg")
|
|
logger.Warn("warn msg")
|
|
logger.Error("error msg")
|
|
|
|
// The all-levels handler should see every record.
|
|
if len(all.records) != 4 {
|
|
t.Errorf("all handler got %d records, want 4", len(all.records))
|
|
}
|
|
|
|
// The warn+ handler should see only warn and error.
|
|
if len(errOnly.records) != 2 {
|
|
t.Errorf("errOnly handler got %d records, want 2", len(errOnly.records))
|
|
}
|
|
|
|
if len(errOnly.records) >= 2 {
|
|
if errOnly.records[0].Level != slog.LevelWarn {
|
|
t.Errorf("first errOnly record level = %v, want WARN", errOnly.records[0].Level)
|
|
}
|
|
if errOnly.records[1].Level != slog.LevelError {
|
|
t.Errorf("second errOnly record level = %v, want ERROR", errOnly.records[1].Level)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFanoutHandlerEnabled(t *testing.T) {
|
|
debugHandler := &recordingHandler{level: slog.LevelDebug}
|
|
errorHandler := &recordingHandler{level: slog.LevelError}
|
|
|
|
handler := &fanoutHandler{handlers: []slog.Handler{debugHandler, errorHandler}}
|
|
|
|
// At least one child accepts debug → fanout accepts debug.
|
|
if !handler.Enabled(context.Background(), slog.LevelDebug) {
|
|
t.Error("expected Enabled(Debug) = true")
|
|
}
|
|
if !handler.Enabled(context.Background(), slog.LevelInfo) {
|
|
t.Error("expected Enabled(Info) = true")
|
|
}
|
|
if !handler.Enabled(context.Background(), slog.LevelError) {
|
|
t.Error("expected Enabled(Error) = true")
|
|
}
|
|
}
|
|
|
|
func TestFanoutHandlerEnabledNoneMatch(t *testing.T) {
|
|
errorOnly := &recordingHandler{level: slog.LevelError}
|
|
handler := &fanoutHandler{handlers: []slog.Handler{errorOnly}}
|
|
|
|
if handler.Enabled(context.Background(), slog.LevelDebug) {
|
|
t.Error("expected Enabled(Debug) = false with error-only handler")
|
|
}
|
|
}
|
|
|
|
func TestFanoutWithAttrs(t *testing.T) {
|
|
h1 := &recordingHandler{level: slog.LevelInfo}
|
|
h2 := &recordingHandler{level: slog.LevelInfo}
|
|
handler := &fanoutHandler{handlers: []slog.Handler{h1, h2}}
|
|
|
|
newHandler := handler.WithAttrs([]slog.Attr{slog.String("key", "val")})
|
|
if newHandler == nil {
|
|
t.Fatal("WithAttrs returned nil")
|
|
}
|
|
}
|
|
|
|
func TestFanoutWithGroup(t *testing.T) {
|
|
h1 := &recordingHandler{level: slog.LevelInfo}
|
|
h2 := &recordingHandler{level: slog.LevelInfo}
|
|
handler := &fanoutHandler{handlers: []slog.Handler{h1, h2}}
|
|
|
|
newHandler := handler.WithGroup("test")
|
|
if newHandler == nil {
|
|
t.Fatal("WithGroup returned nil")
|
|
}
|
|
}
|
|
|
|
// TestInitStderrOnly verifies that an empty File falls back to stderr-only
|
|
// logging (no lumberjack file handler) — used by `anchorage version` and
|
|
// other short-lived commands that don't want to touch the filesystem.
|
|
func TestInitStderrOnly(t *testing.T) {
|
|
if err := Init(Config{Level: "debug", Format: "text"}); err != nil {
|
|
t.Fatalf("Init with empty File returned error: %v", err)
|
|
}
|
|
// Should be a fanoutHandler with exactly one child (stderr).
|
|
h, ok := slog.Default().Handler().(*fanoutHandler)
|
|
if !ok {
|
|
t.Fatalf("default handler is not fanoutHandler: %T", slog.Default().Handler())
|
|
}
|
|
if len(h.handlers) != 1 {
|
|
t.Errorf("expected 1 child handler when File is empty, got %d", len(h.handlers))
|
|
}
|
|
}
|
|
|
|
func TestInitRejectsBadFormat(t *testing.T) {
|
|
err := Init(Config{Level: "info", Format: "yaml"})
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown format, got nil")
|
|
}
|
|
}
|
|
|
|
func TestInitRejectsBadLevel(t *testing.T) {
|
|
err := Init(Config{Level: "trace"})
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown level, got nil")
|
|
}
|
|
}
|