package fold_test import ( "bytes" "context" "encoding/json" "log/slog" "path/filepath" "strings" "testing" "git.vakhrushev.me/av/healthlog/internal/archive" "git.vakhrushev.me/av/healthlog/internal/fold" "git.vakhrushev.me/av/healthlog/internal/store" ) // Данные о здоровье чувствительнее токенов, и требование «значения точек не в // логах» до сих пор не проверялось ничем: все тестовые логгеры выбрасывали // записи. Тест ловит записи и смотрит на них. func TestFoldНеПишетЗначенийВЛог(t *testing.T) { t.Parallel() var buf bytes.Buffer log := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})) dir := t.TempDir() arch, err := archive.New(filepath.Join(dir, "raw")) if err != nil { t.Fatalf("архив: %v", err) } st, err := store.Open(filepath.Join(dir, "healthlog.db")) if err != nil { t.Fatalf("база: %v", err) } t.Cleanup(func() { _ = st.Close() }) f := fold.New(arch, st, 0, log) // Значения и имя устройства выбраны так, чтобы их нельзя было спутать ни с // чем: если они окажутся в логе, это будет видно. body := []byte(`{"data":{"metrics":[{"name":"step_count","units":"count","data":[` + `{"date":"2025-06-05 10:00:00 +0300","qty":424242.7,"source":"Секретные Часы Антона"},` + `{"date":"2025-06-05 10:01:00 +0300","qty":313131.9,"source":"Секретные Часы Антона"},` + `{"date":"2025-06-05 10:02:00 +0300","qty":151515.1,"source":"Секретные Часы Антона"}` + `]}]}}`) deliver(t, arch, st, "d1", "Minutes", "auto-1", body) if _, err := f.Fold(context.Background(), "d1"); err != nil { t.Fatalf("свёртка: %v", err) } logged := buf.String() if logged == "" { t.Fatal("свёртка не записала ни одной строки — чекпоинт молчит") } for _, secret := range []string{"424242", "313131", "151515", "Секретные Часы"} { if strings.Contains(logged, secret) { t.Errorf("в логе оказалось %q:\n%s", secret, logged) } } // И одновременно — чекпоинт обязан нести счётчики, иначе он бесполезен. var rec map[string]any for line := range strings.SplitSeq(strings.TrimSpace(logged), "\n") { if err := json.Unmarshal([]byte(line), &rec); err != nil { t.Fatalf("строка лога не JSON: %v", err) } if rec["msg"] == "delivery folded" { break } } for _, attr := range []string{"delivery_id", "points", "stored", "buckets", "layer", "skipped"} { if _, ok := rec[attr]; !ok { t.Errorf("в записи нет атрибута %q: %v", attr, rec) } } } // Доставка, у которой отброшены ВСЕ точки, — это сломавшийся формат, а не // штатная работа. Без WARN смена формата метки выглядела бы как здоровый // поток: 200, parsed, INFO, points=0. func TestFoldВсеТочкиОтброшеныДаётWarn(t *testing.T) { t.Parallel() var buf bytes.Buffer log := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})) dir := t.TempDir() arch, err := archive.New(filepath.Join(dir, "raw")) if err != nil { t.Fatalf("архив: %v", err) } st, err := store.Open(filepath.Join(dir, "healthlog.db")) if err != nil { t.Fatalf("база: %v", err) } t.Cleanup(func() { _ = st.Close() }) f := fold.New(arch, st, 0, log) // Метки в формате, которого разбор не знает: HAE сменил формат. body := []byte(`{"data":{"metrics":[{"name":"step_count","units":"count","data":[` + `{"date":"2025-06-05T10:00:00Z","qty":1},{"date":"2025-06-05T10:01:00Z","qty":2}` + `]}]}}`) deliver(t, arch, st, "d1", "Minutes", "auto-1", body) if _, err := f.Fold(context.Background(), "d1"); err != nil { t.Fatalf("свёртка: %v", err) } if !strings.Contains(buf.String(), `"level":"WARN"`) { t.Errorf("доставка без единой сохранённой точки записана не как WARN:\n%s", buf.String()) } }