хранилище переехало с PocketBase на SQLite со своим каталогом файлов
- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
package ident
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Идентификатор — ULID в нижнем регистре: постоянная ширина, алфавит Crockford,
|
||||
// и разбор своего же значения его не меняет.
|
||||
func TestNewIsCanonical(t *testing.T) {
|
||||
id := New()
|
||||
|
||||
assert.Len(t, id, Len)
|
||||
assert.Equal(t, strings.ToLower(id), id, "идентификатор выдан не в нижнем регистре")
|
||||
|
||||
parsed, ok := Parse(id)
|
||||
require.True(t, ok, "свой же идентификатор не разобрался")
|
||||
assert.Equal(t, id, parsed, "разбор изменил канонический вид")
|
||||
}
|
||||
|
||||
// Порядок выдачи и порядок сравнения строк совпадают — в том числе внутри одной
|
||||
// миллисекунды: колонка времени несёт секунды, и «новые сверху» держит ключ.
|
||||
func TestNewIsMonotonic(t *testing.T) {
|
||||
previous := New()
|
||||
for range 10000 {
|
||||
next := New()
|
||||
require.Less(t, previous, next, "идентификатор выдан не по возрастанию")
|
||||
previous = next
|
||||
}
|
||||
}
|
||||
|
||||
// Выдача идёт из нескольких потоков разом: два одинаковых ключа означали бы
|
||||
// отказ вставки на живой базе.
|
||||
func TestNewIsUniqueUnderConcurrency(t *testing.T) {
|
||||
const goroutines = 8
|
||||
const each = 500
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
all = map[string]bool{}
|
||||
)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range goroutines {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
own := make([]string, 0, each)
|
||||
for range each {
|
||||
own = append(own, New())
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for _, id := range own {
|
||||
all[id] = true
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
assert.Len(t, all, goroutines*each, "выданы одинаковые идентификаторы")
|
||||
}
|
||||
|
||||
// Разбор проверяет вид и приводит регистр: значение приходит от спрашивающего, а
|
||||
// сравнение в базе побайтово.
|
||||
func TestParse(t *testing.T) {
|
||||
id := New()
|
||||
|
||||
upper, ok := Parse(strings.ToUpper(id))
|
||||
require.True(t, ok, "верхний регистр не принят: алфавит к нему нечувствителен")
|
||||
assert.Equal(t, id, upper, "разбор не привёл регистр к каноническому")
|
||||
|
||||
for name, value := range map[string]string{
|
||||
"пусто": "",
|
||||
"короче": id[:Len-1],
|
||||
"длиннее": id + "0",
|
||||
"знак вне алфавита": strings.Repeat("u", Len),
|
||||
"время за пределом": "8" + id[1:],
|
||||
"не латиница": strings.Repeat("я", Len),
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, ok := Parse(value)
|
||||
assert.False(t, ok, "негодное значение принято разбором")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Время, зашитое в идентификатор, идёт в ту же сторону, что и колонка времени.
|
||||
func TestTimestampGrowsWithIssue(t *testing.T) {
|
||||
first, ok := Timestamp(New())
|
||||
require.True(t, ok)
|
||||
|
||||
second, ok := Timestamp(New())
|
||||
require.True(t, ok)
|
||||
|
||||
assert.False(t, second.Before(first), "время в ключе пошло назад")
|
||||
}
|
||||
Reference in New Issue
Block a user