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") } }