159 lines
5.6 KiB
Go
159 lines
5.6 KiB
Go
package notify
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
"time"
|
||
|
||
"building-mgmt/internal/telegram"
|
||
)
|
||
|
||
// Тесты реестра ботов (миграция 0012): личный токен приоритетнее
|
||
// глобального, на каждый личный бот — свой поллер, остановка гасит его.
|
||
|
||
// newRegistryBot — бот с каналами, чтобы дождаться старта поллера и его
|
||
// выхода (idle — поллинг начался, stopped — поллер остановился).
|
||
func newRegistryBot() *fakeBot {
|
||
return &fakeBot{
|
||
enabled: true,
|
||
username: "test_bot",
|
||
idle: make(chan struct{}),
|
||
stopped: make(chan struct{}),
|
||
}
|
||
}
|
||
|
||
// waitStopped — ждёт остановки поллера, но не вечно.
|
||
func waitStopped(t *testing.T, bot *fakeBot) {
|
||
t.Helper()
|
||
select {
|
||
case <-bot.stopped:
|
||
case <-time.After(3 * time.Second):
|
||
t.Fatal("поллер не остановился")
|
||
}
|
||
}
|
||
|
||
// TestRegistryBotFor — без своего токена отдаётся глобальный бот;
|
||
// со своим — он, глобальный не задействуется.
|
||
func TestRegistryBotFor(t *testing.T) {
|
||
global := &fakeBot{enabled: false, username: "global_bot"}
|
||
r := NewRegistry(context.Background(), newFakeStore(), global, testLog())
|
||
|
||
if got := r.BotFor("user-1"); got != telegram.API(global) {
|
||
t.Errorf("BotFor = %v, want global fallback", got)
|
||
}
|
||
|
||
mine := newRegistryBot()
|
||
r.Register("user-1", mine)
|
||
if got := r.BotFor("user-1"); got != telegram.API(mine) {
|
||
t.Errorf("BotFor = %v, want personal bot", got)
|
||
}
|
||
// Чужой пользователь по-прежнему получает глобального.
|
||
if got := r.BotFor("user-2"); got != telegram.API(global) {
|
||
t.Errorf("BotFor другого = %v, want global", got)
|
||
}
|
||
|
||
r.Unregister("user-1")
|
||
if got := r.BotFor("user-1"); got != telegram.API(global) {
|
||
t.Errorf("BotFor после удаления = %v, want global", got)
|
||
}
|
||
}
|
||
|
||
// TestRegistryPollerLifecycle — Register поднимает поллер, Unregister
|
||
// останавливает: без этого процесс накапливал бы горутины на каждый
|
||
// ввод и удаление токена.
|
||
func TestRegistryPollerLifecycle(t *testing.T) {
|
||
r := NewRegistry(context.Background(), newFakeStore(), &fakeBot{enabled: false}, testLog())
|
||
|
||
bot := newRegistryBot()
|
||
r.Register("user-1", bot)
|
||
if r.Users() != 1 {
|
||
t.Fatalf("Users = %d, want 1", r.Users())
|
||
}
|
||
select {
|
||
case <-bot.idle: // поллинг пошёл
|
||
case <-time.After(3 * time.Second):
|
||
t.Fatal("поллер не стартовал")
|
||
}
|
||
|
||
r.Unregister("user-1")
|
||
if r.Users() != 0 {
|
||
t.Errorf("Users = %d, want 0", r.Users())
|
||
}
|
||
waitStopped(t, bot)
|
||
|
||
// Повторное удаление безопасно (идемпотентно).
|
||
r.Unregister("user-1")
|
||
}
|
||
|
||
// TestRegistryReRegister — смена токена гасит прежний поллер: две
|
||
// горутины не должны драть одну очередь getUpdates.
|
||
func TestRegistryReRegister(t *testing.T) {
|
||
r := NewRegistry(context.Background(), newFakeStore(), &fakeBot{enabled: false}, testLog())
|
||
|
||
first := newRegistryBot()
|
||
r.Register("user-1", first)
|
||
select {
|
||
case <-first.idle:
|
||
case <-time.After(3 * time.Second):
|
||
t.Fatal("первый поллер не стартовал")
|
||
}
|
||
|
||
second := newRegistryBot()
|
||
r.Register("user-1", second)
|
||
waitStopped(t, first) // прежний остановлен
|
||
|
||
if got := r.BotFor("user-1"); got != telegram.API(second) {
|
||
t.Errorf("BotFor = %v, want второй бот", got)
|
||
}
|
||
if r.Users() != 1 {
|
||
t.Errorf("Users = %d, want 1", r.Users())
|
||
}
|
||
// Дожидаемся, пока новый поллер повиснет в getUpdates, и гасим:
|
||
// до этого момента его отмена — штатный выход без вызова API.
|
||
select {
|
||
case <-second.idle:
|
||
case <-time.After(3 * time.Second):
|
||
t.Fatal("второй поллер не стартовал")
|
||
}
|
||
r.Unregister("user-1")
|
||
waitStopped(t, second)
|
||
}
|
||
|
||
// TestRegistryParentContext — остановка процесса (отмена родительского
|
||
// контекста) гасит все личные поллеры разом.
|
||
func TestRegistryParentContext(t *testing.T) {
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
r := NewRegistry(ctx, newFakeStore(), &fakeBot{enabled: false}, testLog())
|
||
|
||
first, second := newRegistryBot(), newRegistryBot()
|
||
r.Register("user-1", first)
|
||
r.Register("user-2", second)
|
||
// Оба должны повиснуть в getUpdates — иначе отмена пройдёт мимо и
|
||
// поллер выйдет, не вызв API (тест не увидит stopped).
|
||
for _, bot := range []*fakeBot{first, second} {
|
||
select {
|
||
case <-bot.idle:
|
||
case <-time.After(3 * time.Second):
|
||
t.Fatal("поллер не стартовал")
|
||
}
|
||
}
|
||
cancel()
|
||
waitStopped(t, first)
|
||
waitStopped(t, second)
|
||
}
|
||
|
||
// TestRegistryIgnoresDisabled — выключенный бот (пустой токен) поллер
|
||
// не поднимает: держать горутину, которая сразу вернёт ErrDisabled,
|
||
// незачем.
|
||
func TestRegistryIgnoresDisabled(t *testing.T) {
|
||
r := NewRegistry(context.Background(), newFakeStore(), &fakeBot{enabled: false}, testLog())
|
||
|
||
r.Register("user-1", &fakeBot{enabled: false, idle: make(chan struct{})})
|
||
if r.Users() != 0 {
|
||
t.Errorf("Users = %d, want 0", r.Users())
|
||
}
|
||
if got := r.BotFor("user-1"); got.Enabled() {
|
||
t.Error("BotFor = включённый бот, want выключенный fallback")
|
||
}
|
||
}
|