Files
ratatoskr-go/internal/app/app_test.go
Hermes 91e91ac3ce
All checks were successful
CI / test (push) Successful in 47s
CI / build-and-package (amd64, linux) (push) Successful in 41s
CI / build-and-package (amd64, windows) (push) Successful in 41s
feat: команда /help — справка по всем командам бота
2026-08-16 20:05:11 +05:00

195 lines
6.2 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 app
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
)
// TestNew проверяет, что composition root собирается без ошибок.
// Использует :memory: (? или file-based с t.TempDir).
func TestNew(t *testing.T) {
tmp := t.TempDir()
dbPath := filepath.Join(tmp, "test.db")
// Настраиваем минимальные env для обязательных полей
t.Setenv("TG_TOKEN", "test:token")
t.Setenv("TG_CHAT_ID", "12345")
t.Setenv("RATATOSKR_DB", dbPath)
// Загружаем без config-файла (дефолты + env)
a, err := New("", "dev", "")
if err != nil {
t.Fatalf("New() err = %v", err)
}
if a.Store == nil {
t.Fatal("Store не создан")
}
if a.Router == nil {
t.Fatal("Router не создан")
}
if a.CoreCtx == nil {
t.Fatal("Core не создан")
}
if a.Worker == nil {
t.Fatal("Worker не создан")
}
if a.tg == nil {
t.Fatal("Telegram-канал не создан")
}
t.Logf("app OK: db=%s", a.Config.Paths.DB)
}
// TestNew_MissingToken проверяет ошибку при пустом токене.
func TestNew_MissingToken(t *testing.T) {
tmp := t.TempDir()
dbPath := filepath.Join(tmp, "test.db")
t.Setenv("TG_TOKEN", "")
t.Setenv("TG_CHAT_ID", "12345")
t.Setenv("RATATOSKR_DB", dbPath)
_, err := New("", "dev", "")
if err == nil {
t.Fatal("expected error for missing token")
}
t.Logf("got expected error: %v", err)
}
// TestNew_BadDB проверяет ошибку при пути к БД, который нельзя создать.
func TestNew_BadDB(t *testing.T) {
tmp := t.TempDir()
dbPath := filepath.Join(tmp, "nonexistent", "test.db") // родителя нет
t.Setenv("TG_TOKEN", "test:token")
t.Setenv("TG_CHAT_ID", "12345")
t.Setenv("RATATOSKR_DB", dbPath)
_, err := New("", "dev", "")
if err == nil {
t.Fatal("expected error for invalid db path")
}
if !strings.Contains(err.Error(), "db open error") {
t.Errorf("expected 'db open error' in message, got %v", err)
}
}
// TestNew_CtxCancel проверяет, что Run завершается на отмене контекста.
func TestNew_RunCtxCancel(t *testing.T) {
tmp := t.TempDir()
dbPath := filepath.Join(tmp, "test-runcancel.db")
t.Setenv("TG_TOKEN", "test:token")
t.Setenv("TG_CHAT_ID", "12345")
t.Setenv("RATATOSKR_DB", dbPath)
a, err := New("", "dev", "")
if err != nil {
t.Fatalf("New() err = %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel() // сразу отменяем
err = a.Run(ctx)
if err != nil {
t.Fatalf("Run() expected nil, got %v", err)
}
t.Log("Run OK on cancelled context")
}
// TestMain — дёргает fatalf, нормально что os.Exit не тестируется.
// Просто тест, что main.go компилируется.
func TestMain(t *testing.T) {
// main() не вызываем — он бы os.Exit
_, err := os.Stat("../../cmd/ratatoskr/main.go")
if err != nil {
t.Fatalf("main.go not found: %v", err)
}
t.Log("main.go exists")
}
// TestNew_UpdateWiring проверяет: 1) update.base_url берётся из update-блока
// (не git.base_url); 2) приоритет токена: вшитый updateToken > update.token.
func TestNew_UpdateWiring(t *testing.T) {
tmp := t.TempDir()
dbPath := filepath.Join(tmp, "test.db")
configPath := filepath.Join(tmp, "config.yaml")
content := strings.Join([]string{
"telegram:",
" token: \"test:token\"",
" chat_id: \"12345\"",
"git:",
" base_url: \"http://local-git/\"",
" token: \"git-token\"",
"update:",
" enabled: true",
" base_url: \"https://hub.example.com\"",
" token: \"cfg-update-token\"",
"paths:",
" db: \"" + dbPath + "\"",
"", // пустая строка в конце
}, "\n")
if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
// 1) base_url из update-блока, а НЕ git
a, err := New(configPath, "dev", "")
if err != nil {
t.Fatalf("New() err = %v", err)
}
if a.Updater == nil {
t.Fatal("Updater не создан")
}
if want := "https://hub.example.com"; a.Updater.BaseURL != want {
t.Errorf("BaseURL = %q, want %q (должен быть update.base_url, не git.base_url)", a.Updater.BaseURL, want)
}
// владелец пакета — жёсткая константа
if a.Updater.Owner != packageOwner {
t.Errorf("Owner = %q, want %q (константа)", a.Updater.Owner, packageOwner)
}
// без вшитого токена берём update.token из конфига
if a.Updater.Token != "cfg-update-token" {
t.Errorf("Token = %q, want cfg-update-token (при пустом вшитом)", a.Updater.Token)
}
// 2) вшитый updateToken перекрывает конфиг
a2, err := New(configPath, "dev", "embedded-update-token")
if err != nil {
t.Fatalf("New() err = %v", err)
}
if a2.Updater.Token != "embedded-update-token" {
t.Errorf("Token = %q, want embedded-update-token (вшитый приоритетнее)", a2.Updater.Token)
}
}
// TestHelp_IsBinaryCommand проверяет, что /help распознаётся cmdName и, значит,
// в handleIncoming попадёт в бинарную ветку (не в машину состояний задач).
// Косвенно: /help не должен иметь аргумента-номера задачи (как /status N).
func TestHelp_IsBinaryCommand(t *testing.T) {
if got := cmdName("/help"); got != "/help" {
t.Errorf("cmdName(/help) = %q, want /help", got)
}
if hasArg("/help") {
t.Errorf("/help не должен требовать аргумент")
}
if got := cmdName("/help foo"); got != "/help" {
t.Errorf("cmdName(/help foo) = %q, want /help", got)
}
}
// TestHelpText_ContainsCommands проверяет, что справка упоминает все команды.
func TestHelpText_ContainsCommands(t *testing.T) {
for _, cmd := range []string{
"/start", "/cancel", "/skip", "/retry", "/status", "/continue",
"/update", "/help",
} {
if !strings.Contains(helpText, cmd) {
t.Errorf("helpText не упоминает команду %s", cmd)
}
}
if !strings.Contains(helpText, "Ratatoskr") {
t.Errorf("helpText не содержит имени бота")
}
}