refactor: merge internal/sync into internal/garmin, regroup api files and routes

Garmin auth/sync routes move under /api/garmin/*; sync.Service becomes
garmin.Sync with garmin.SyncConfig/ClientConfig; applog becomes
internal/log; the test mock moves into the garmin package as MockClient
(breaking the test-only import cycle the merge created); stale test
URLs and type names updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 16:04:18 +02:00
parent 19c9aecdeb
commit e2b2bf9611
61 changed files with 2007 additions and 982 deletions

View File

@@ -0,0 +1,51 @@
// Package applog provides geniusrun's structured JSON logging: a
// log/slog-based logger writing to stdout, plus context helpers so a
// logger enriched in one layer (e.g. internal/api's HTTP middleware,
// attaching a request_id) is picked up by another (e.g. internal/garmin's
// wrapper-call logging) without either package depending on the other.
package applog
import (
"context"
"io"
"log/slog"
"strings"
)
// NewLogger builds a JSON-handler *slog.Logger writing to w at the given
// level ("debug"|"info"|"warn"|"error", case-insensitive; anything else
// defaults to info).
func NewLogger(level string, w io.Writer) *slog.Logger {
return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{Level: parseLevel(level)}))
}
func parseLevel(level string) slog.Level {
switch strings.ToLower(level) {
case "debug":
return slog.LevelDebug
case "warn":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}
type ctxKey struct{}
// WithLogger returns a context carrying logger, retrievable via FromContext.
func WithLogger(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, ctxKey{}, logger)
}
// FromContext returns the logger stashed by WithLogger, or slog.Default()
// if none was -- callers (e.g. the background incremental-sync loop, or
// tests that don't bother injecting one) always get a working logger,
// never nil.
func FromContext(ctx context.Context) *slog.Logger {
if logger, ok := ctx.Value(ctxKey{}).(*slog.Logger); ok {
return logger
}
return slog.Default()
}

View File

@@ -0,0 +1,54 @@
package applog
import (
"bytes"
"context"
"encoding/json"
"log/slog"
"strings"
"testing"
)
func TestNewLogger_FiltersBelowConfiguredLevel(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("warn", &buf)
logger.Info("should be dropped")
if buf.Len() != 0 {
t.Fatalf("expected no output for an Info message under a warn-level logger, got %q", buf.String())
}
logger.Warn("should appear")
if !strings.Contains(buf.String(), "should appear") {
t.Fatalf("expected the Warn message in output, got %q", buf.String())
}
}
func TestNewLogger_WritesValidJSON(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("info", &buf)
logger.Info("hello", "key", "value")
var decoded map[string]any
if err := json.Unmarshal(buf.Bytes(), &decoded); err != nil {
t.Fatalf("output is not valid JSON: %v (%q)", err, buf.String())
}
if decoded["msg"] != "hello" || decoded["key"] != "value" {
t.Errorf("decoded = %+v, want msg=hello key=value", decoded)
}
}
func TestWithLogger_FromContext_RoundTrip(t *testing.T) {
logger := slog.New(slog.NewJSONHandler(&bytes.Buffer{}, nil))
ctx := WithLogger(context.Background(), logger)
if got := FromContext(ctx); got != logger {
t.Errorf("FromContext returned a different logger than what was stashed")
}
}
func TestFromContext_DefaultsWhenNoneSet(t *testing.T) {
if got := FromContext(context.Background()); got == nil {
t.Fatal("FromContext on a bare context returned nil, want slog.Default()")
}
}