package archive_test import ( "io" "os" "path/filepath" "reflect" "strings" "testing" "time" "git.vakhrushev.me/av/healthlog/internal/archive" ) func TestWriteAndOpenRoundTrip(t *testing.T) { a := newArchive(t) body := []byte(`{"data":{"metrics":[]}}`) at := time.Date(2026, 7, 31, 14, 5, 0, 0, time.UTC) rel, err := a.Write("01k1abc", at, body) if err != nil { t.Fatalf("Write: %v", err) } if want := filepath.Join("2026", "07", "31", "01k1abc.json.gz"); rel != want { t.Errorf("путь = %q, ожидался %q", rel, want) } r, err := a.Open(rel) 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) } } // Раскладка по дате приёма — в UTC: иначе один и тот же момент попадал бы в // разные каталоги в зависимости от зоны машины. func TestWriteUsesUTCDate(t *testing.T) { a := newArchive(t) // 01:30 по московскому времени — это ещё предыдущие сутки по UTC. msk := time.FixedZone("MSK", 3*60*60) at := time.Date(2026, 8, 1, 1, 30, 0, 0, msk) rel, err := a.Write("01k1def", at, []byte(`{"data":{}}`)) if err != nil { t.Fatalf("Write: %v", err) } if want := filepath.Join("2026", "07", "31", "01k1def.json.gz"); rel != want { t.Errorf("путь = %q, ожидался %q", rel, want) } } // Временных файлов после успешной записи остаться не должно. func TestWriteLeavesNoTempFiles(t *testing.T) { root := t.TempDir() a, err := archive.New(root) if err != nil { t.Fatalf("New: %v", err) } if _, err := a.Write("01k1ghi", time.Date(2026, 7, 31, 0, 0, 0, 0, time.UTC), []byte(`{"data":{}}`)); err != nil { t.Fatalf("Write: %v", err) } err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if strings.HasSuffix(path, ".tmp") { t.Errorf("остался временный файл: %s", path) } return nil }) if err != nil { t.Fatalf("обход архива: %v", err) } } func newArchive(t *testing.T) *archive.Archive { t.Helper() a, err := archive.New(filepath.Join(t.TempDir(), "raw")) if err != nil { t.Fatalf("New: %v", err) } return a } // Перечисление тел — вход пересборки. Всё, что телом не является, обязано быть // посчитано, а не выброшено молча: тело есть, а в отчёте его нет. func TestListРазводитТелаИПрочиеФайлы(t *testing.T) { t.Parallel() root := t.TempDir() a, err := archive.New(root) if err != nil { t.Fatalf("New: %v", err) } at := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC) for _, id := range []string{"b", "a"} { if _, err := a.Write(id, at, []byte(`{"data":{}}`)); err != nil { t.Fatalf("Write: %v", err) } } day := filepath.Join(root, "2026", "08", "01") for _, name := range []string{"a.json.gz.tmp", "readme.txt"} { if err := os.WriteFile(filepath.Join(day, name), []byte("x"), 0o600); err != nil { t.Fatalf("подготовка файла: %v", err) } } got, err := a.List() if err != nil { t.Fatalf("List: %v", err) } want := []string{ filepath.Join("2026", "08", "01", "a.json.gz"), filepath.Join("2026", "08", "01", "b.json.gz"), } if !reflect.DeepEqual(got.Bodies, want) { t.Errorf("тела %v, ожидались %v", got.Bodies, want) } if len(got.Skipped) != 2 { t.Errorf("пропущено %v, ожидалось два файла", got.Skipped) } if id := archive.BodyID(got.Bodies[0]); id != "a" { t.Errorf("BodyID = %q, ожидался %q", id, "a") } } // Нечитаемый каталог означает «неизвестно, есть ли тела», а не «тел нет»: // пустой журнал даёт пустую витрину, которая по отпечатку совпадает с любой // другой пустой витриной и выглядит идеальной сходимостью. func TestОтсутствующийКаталогНеЯвляетсяПустымАрхивом(t *testing.T) { t.Parallel() missing := filepath.Join(t.TempDir(), "нет-такого") if _, err := archive.Existing(missing); err == nil { t.Error("Existing создал или принял отсутствующий каталог") } if _, err := os.Stat(missing); err == nil { t.Error("Existing создал каталог — пересборке этого делать нельзя") } // А приёму каталог создать надо: это и есть разница между конструкторами. if _, err := archive.New(missing); err != nil { t.Errorf("New: %v", err) } a, err := archive.Existing(missing) if err != nil { t.Fatalf("Existing после New: %v", err) } list, err := a.List() if err != nil { t.Fatalf("List: %v", err) } if len(list.Bodies) != 0 { t.Errorf("в пустом архиве нашлись тела: %v", list.Bodies) } }