package fold import ( "bytes" "context" "encoding/json" "errors" "log/slog" "path/filepath" "strings" "testing" "time" "git.vakhrushev.me/av/healthlog/internal/store" ) // Внутренний тест, а не через `Fold`, и это осознанно. Сломать сверку снаружи // больше нечем: испорченную учётную запись запрос теперь пропускает, а // единственная настоящая причина отказа — отмена контекста и занятость базы — // в полном проходе свёртки утащила бы за собой и слияние, то есть проверялась // бы уже другая ветвь. Заводить интерфейс хранилища ради мока дороже: он // пережил бы тест и остался бы в коде навсегда. func TestNoveltyОтменённыйКонтекстНеМолчит(t *testing.T) { t.Parallel() dir := t.TempDir() st, err := store.Open(filepath.Join(dir, "healthlog.db")) if err != nil { t.Fatalf("база: %v", err) } t.Cleanup(func() { _ = st.Close() }) s := &Service{store: st, log: slog.New(slog.DiscardHandler)} ctx, cancel := context.WithCancel(context.Background()) cancel() got := s.novelty(ctx, []string{"ecg", "symptoms"}, store.DeliveryRef{ ID: "d1", ReceivedAt: time.Now().UTC(), }) if !got.unknown { t.Fatal("сверка на отменённом контексте объявила себя состоявшейся") } if len(got.fresh) != 2 { t.Errorf("новыми объявлены %v, ожидались все имена доставки", got.fresh) } if got.cause == nil { t.Error("причина отказа потеряна: занятость базы и вечную порчу колонки по признаку не различить") } } // Пустой список сверку не берёт вовсе: на живом потоке все три приезжающие // секции покрыты, и запрос по журналу в штатном режиме не стоит ничего. func TestNoveltyПустойСписокЗапросаНеБерёт(t *testing.T) { t.Parallel() s := &Service{store: nil, log: slog.New(slog.DiscardHandler)} // Хранилище nil: возьмись запрос — тест упал бы паникой. got := s.novelty(context.Background(), nil, store.DeliveryRef{ID: "d1"}) if got.unknown || len(got.fresh) != 0 { t.Errorf("на пустом списке сверка что-то решила: %+v", got) } } // Признак несостоявшейся сверки без причины неразбираем: занятость базы пройдёт // сама, испорченная колонка не пройдёт никогда и будет поднимать признак на // каждой доставке. func TestLogResultПечатаетПричинуНесостоявшейсяСверки(t *testing.T) { t.Parallel() var buf bytes.Buffer s := &Service{log: slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))} s.logResult(context.Background(), "d1", Stats{ Uncovered: []string{"ecg"}, UncoveredNew: []string{"ecg"}, UncoveredSeenUnknown: true, UncoveredSeenError: errors.New("журнал недоступен"), }) var rec map[string]any line := strings.TrimSpace(buf.String()) if err := json.Unmarshal([]byte(line), &rec); err != nil { t.Fatalf("запись не разбирается: %v", err) } if rec["uncovered_seen_unknown"] != true { t.Error("признак несостоявшейся сверки не выставлен") } if rec["uncovered_seen_error"] != "журнал недоступен" { t.Errorf("причина в записи: %v", rec["uncovered_seen_error"]) } if rec["level"] != "WARN" { t.Errorf("уровень %v, ожидался WARN: имена объявлены новыми", rec["level"]) } } // Запись об отказе несёт и признак несостоявшейся сверки, и причину: у отказа // разбора та же цена молчания, что у успешного пути — имя уже в учёте. func TestFailПечатаетНесостоявшуюсяСверку(t *testing.T) { t.Parallel() dir := t.TempDir() st, err := store.Open(filepath.Join(dir, "healthlog.db")) if err != nil { t.Fatalf("база: %v", err) } t.Cleanup(func() { _ = st.Close() }) ctx := context.Background() err = st.CreateDelivery(ctx, store.Delivery{ ID: "d1", ReceivedAt: time.Now().UTC(), RawPath: "d1.json.gz", SHA256: "-", ParseStatus: store.ParsePending, }) if err != nil { t.Fatalf("учёт доставки: %v", err) } var buf bytes.Buffer s := &Service{ store: st, log: slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})), } s.fail(ctx, "d1", errors.New("непонятое содержимое"), parseResidue{ uncovered: []string{"ecg"}, novelty: sectionNovelty{fresh: []string{"ecg"}, unknown: true, cause: errors.New("журнал недоступен")}, }) out := buf.String() for _, want := range []string{"uncovered_new", "ecg", "uncovered_seen_unknown", "журнал недоступен"} { if !strings.Contains(out, want) { t.Errorf("в записи об отказе нет %q:\n%s", want, out) } } }