Files
healthlog/internal/ident/ident_test.go
T
av 5ae0c5ff81 reindex: пересборка витрины проигрыванием журнала
- `healthlog reindex` собирает витрину из журнала (тела архива + учёт
  доставок) в ОТДЕЛЬНЫЙ файл базы, строго по `(received_at, id)`; рабочую
  базу читает без наката миграций и не трогает вовсе. Подмену делает
  человек при остановленном сервисе: переименование поверх открытого
  дескриптора портит базу молча.
- Журналом считается архив, а не таблица доставок: тело без учётной записи
  заводится заново (метка из ULID, размер и хеш по распакованному телу),
  запись без тела переносится, но не сворачивается. Оракул сходимости
  встроен — два отпечатка и «объектов было/стало»; пустой журнал успехом не
  считается.
- Прогон живого архива переехал на новый пакет: второго проигрывателя
  журнала в проекте не осталось, а его утверждение о ключе сна перестало
  быть константой, протухающей с каждой доставкой.
2026-08-02 09:07:46 +03:00

116 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ident_test
import (
"errors"
"strings"
"testing"
"time"
"git.vakhrushev.me/av/healthlog/internal/ident"
)
func TestNewIDIsLowercaseULID(t *testing.T) {
id := ident.NewID()
if len(id) != 26 {
t.Errorf("длина = %d, ожидалось 26: %q", len(id), id)
}
if id != strings.ToLower(id) {
t.Errorf("id не в нижнем регистре: %q", id)
}
if ident.NewID() == id {
t.Error("два вызова вернули один и тот же id")
}
}
// Идентификаторы сортируемы по времени создания — на этом держится
// хронология выборок и корреляция в логах.
func TestNewIDSortsByCreationTime(t *testing.T) {
prev := ident.NewID()
for range 10 {
next := ident.NewID()
if next <= prev {
t.Fatalf("порядок нарушен: %q после %q", next, prev)
}
prev = next
}
}
// Внешний id приводится к каноническому виду: сравнение строк в SQLite
// побайтовое, а base32 ULID при декодировании нечувствителен к регистру.
func TestParseNormalizesCase(t *testing.T) {
id := ident.NewID()
got, err := ident.Parse(strings.ToUpper(id))
if err != nil {
t.Fatalf("Parse: %v", err)
}
if got != id {
t.Errorf("Parse вернул %q, ожидалось %q", got, id)
}
}
func TestParseRejectsGarbage(t *testing.T) {
for _, s := range []string{"", "не-id", "01k1", strings.Repeat("z", 26)} {
if _, err := ident.Parse(s); !errors.Is(err, ident.ErrInvalid) {
t.Errorf("Parse(%q): ошибка = %v, ожидалась ErrInvalid", s, err)
}
}
}
// Время из ULID нужно телу, лежащему в архиве без учётной записи: другого
// источника метки приёма у него нет.
func TestTimeOfВUTCИСекундах(t *testing.T) {
t.Parallel()
id := ident.NewID()
at, err := ident.TimeOf(id)
if err != nil {
t.Fatalf("TimeOf(%q): %v", id, err)
}
// UTC, а не локальная зона: ulid.Time собирает время через time.Unix, то
// есть в зоне машины, и метка подобранного тела сравнивалась бы с меткой из
// БД по-разному на разных машинах.
if at.Location() != time.UTC {
t.Errorf("зона %v, ожидался UTC", at.Location())
}
// Секундная точность — та же, что у store.Now: метка уезжает в колонку
// фиксированной ширины, где лексикографический порядок равен хронологии.
if at.Nanosecond() != 0 {
t.Errorf("метка %v несёт доли секунды", at)
}
if d := time.Since(at); d < 0 || d > time.Minute {
t.Errorf("метка %v далека от настоящего времени (%v)", at, d)
}
}
// Монотонность ULID — то, на чём стоит порядок проигрывания журнала для
// подобранных тел.
func TestTimeOfСохраняетПорядок(t *testing.T) {
t.Parallel()
first, err := ident.TimeOf(ident.NewID())
if err != nil {
t.Fatalf("TimeOf: %v", err)
}
time.Sleep(1100 * time.Millisecond)
second, err := ident.TimeOf(ident.NewID())
if err != nil {
t.Fatalf("TimeOf: %v", err)
}
if !second.After(first) {
t.Errorf("порядок меток не сохранился: %v, затем %v", first, second)
}
}
func TestTimeOfОтвергаетНеИдентификатор(t *testing.T) {
t.Parallel()
for _, s := range []string{"", "не-ulid", "01kyzbb5zy4cbkbc0agb6ad07", "readme.txt"} {
if _, err := ident.TimeOf(s); !errors.Is(err, ident.ErrInvalid) {
t.Errorf("TimeOf(%q) дал %v, ожидался ErrInvalid", s, err)
}
}
}