package ingest_test import ( "context" "crypto/sha256" "encoding/hex" "errors" "io" "log/slog" "path/filepath" "testing" "git.vakhrushev.me/av/healthlog/internal/archive" "git.vakhrushev.me/av/healthlog/internal/ingest" "git.vakhrushev.me/av/healthlog/internal/store" ) func TestAcceptStoresBodyVerbatim(t *testing.T) { svc, arch, st := newService(t) // Незнакомая метрика и неизвестная форма точки — не повод отвергать пакет. body := []byte(`{"data":{"metrics":[{"name":"новая_метрика","units":"?","data":[{"чего_то":1,"date":"2026-07-31 12:00:00 +0300"}]}]}}`) res, err := svc.Accept(context.Background(), body, ingest.Meta{ AutomationName: "всё подряд", Aggregation: "None", Period: "Since Last Sync", }) if err != nil { t.Fatalf("Accept: %v", err) } if res.Bytes != int64(len(body)) { t.Errorf("bytes = %d, ожидалось %d", res.Bytes, len(body)) } sum := sha256.Sum256(body) if res.SHA256 != hex.EncodeToString(sum[:]) { t.Errorf("sha256 = %q", res.SHA256) } // Тело в архиве — байт в байт то, что прислали. r, err := arch.Open(res.RawPath) if err != nil { t.Fatalf("Open: %v", err) } defer func() { _ = r.Close() }() got, err := io.ReadAll(r) if err != nil { t.Fatalf("чтение архива: %v", err) } if string(got) != string(body) { t.Errorf("в архиве %q, прислано %q", got, body) } n, err := st.CountDeliveries(context.Background()) if err != nil { t.Fatalf("CountDeliveries: %v", err) } if n != 1 { t.Errorf("доставок в БД: %d, ожидалась 1", n) } } // Повтор того же тела пока принимается — отсев идентичных доставок отложен // (docs/plan.md). Проверяем, что повтор не ломается и не затирает первую. func TestAcceptAllowsRepeatedBody(t *testing.T) { svc, _, st := newService(t) body := []byte(`{"data":{"metrics":[]}}`) first, err := svc.Accept(context.Background(), body, ingest.Meta{}) if err != nil { t.Fatalf("первый Accept: %v", err) } second, err := svc.Accept(context.Background(), body, ingest.Meta{}) if err != nil { t.Fatalf("второй Accept: %v", err) } if first.DeliveryID == second.DeliveryID { t.Error("у двух доставок совпали идентификаторы") } if first.SHA256 != second.SHA256 { t.Error("у одинаковых тел разошёлся sha256") } n, err := st.CountDeliveries(context.Background()) if err != nil { t.Fatalf("CountDeliveries: %v", err) } if n != 2 { t.Errorf("доставок в БД: %d, ожидалось 2", n) } } func TestAcceptRejectsMalformed(t *testing.T) { cases := map[string]string{ "пустое тело": ``, "не JSON": `не json`, "обрезанный JSON": `{"data":{"metrics":[`, "нет data": `{"metrics":[]}`, "data не объект": `{"data":[]}`, } for name, body := range cases { t.Run(name, func(t *testing.T) { svc, _, st := newService(t) _, err := svc.Accept(context.Background(), []byte(body), ingest.Meta{}) if !errors.Is(err, ingest.ErrMalformed) { t.Fatalf("ошибка = %v, ожидалась ErrMalformed", err) } // Отвергнутая доставка не оставляет следа в учёте. n, err := st.CountDeliveries(context.Background()) if err != nil { t.Fatalf("CountDeliveries: %v", err) } if n != 0 { t.Errorf("доставок в БД: %d, ожидалось 0", n) } }) } } func newService(t *testing.T) (*ingest.Service, *archive.Archive, *store.Store) { t.Helper() dir := t.TempDir() st, err := store.Open(filepath.Join(dir, "healthlog.db")) if err != nil { t.Fatalf("store.Open: %v", err) } t.Cleanup(func() { _ = st.Close() }) arch, err := archive.New(filepath.Join(dir, "raw")) if err != nil { t.Fatalf("archive.New: %v", err) } log := slog.New(slog.DiscardHandler) return ingest.New(arch, st, log), arch, st }