- `healthlog reindex` собирает витрину из журнала (тела архива + учёт доставок) в ОТДЕЛЬНЫЙ файл базы, строго по `(received_at, id)`; рабочую базу читает без наката миграций и не трогает вовсе. Подмену делает человек при остановленном сервисе: переименование поверх открытого дескриптора портит базу молча. - Журналом считается архив, а не таблица доставок: тело без учётной записи заводится заново (метка из ULID, размер и хеш по распакованному телу), запись без тела переносится, но не сворачивается. Оракул сходимости встроен — два отпечатка и «объектов было/стало»; пустой журнал успехом не считается. - Прогон живого архива переехал на новый пакет: второго проигрывателя журнала в проекте не осталось, а его утверждение о ключе сна перестало быть константой, протухающей с каждой доставкой.
116 lines
3.8 KiB
Go
116 lines
3.8 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|