From ab8b7f61e0d5a822e404cf558bb16b4232a52f7a Mon Sep 17 00:00:00 2001 From: kirill Date: Tue, 30 Dec 2025 21:24:07 +0300 Subject: [PATCH 1/2] Initial commit --- .editorconfig | 4 + .gitignore | 90 +++ Bot/Bot.csproj | 31 + Bot/Hadlers/AdminHandler.cs | 197 ++++++ Bot/Hadlers/CallbackQueryHandler.cs | 178 ++++++ Bot/Hadlers/CommandHandler.cs | 268 +++++++++ Bot/Hadlers/IUpdateHandlerCommand.cs | 36 ++ Bot/Hadlers/MessageHandler.cs | 183 ++++++ Bot/Hadlers/PaymentHandler.cs | 279 +++++++++ Bot/Hadlers/PreCheckoutQueryHandler.cs | 119 ++++ Bot/IReceiverService.cs | 20 + Bot/PollingService.cs | 124 ++++ Bot/Program.cs | 128 ++++ Bot/ReceiverService.cs | 70 +++ Bot/Services/AdminGoalStep.cs | 30 + Bot/Services/AdminStateService.cs | 190 ++++++ Bot/Services/KeyboardService.cs | 131 ++++ Bot/Services/UserStateService.cs | 110 ++++ Bot/UpdateHandler.cs | 129 ++++ Bot/appsettings.json | 15 + Configurations/BotConfig.cs | 8 + Configurations/Configurations.csproj | 18 + Configurations/DatabaseConfig.cs | 12 + Data/DapperRepository.cs | 277 +++++++++ Data/Data.csproj | 23 + Data/IDapperRepository.cs | 70 +++ Data/Models/Donation.cs | 20 + Data/Models/DonationGoal.cs | 18 + Data/Models/Users.cs | 18 + Data/Scripts/create_tables.sql | 49 ++ Dockerfile | 20 + DonationBot.sln | 103 ++++ Services/DonationService.cs | 112 ++++ Services/GoalService.cs | 224 +++++++ Services/IDonationService.cs | 29 + Services/IGoalService.cs | 43 ++ Services/Services.csproj | 21 + TestBot/Bot/Bot.Hadler/AdminHandlerTests.cs | 433 ++++++++++++++ .../Bot.Hadler/CallbackQueryHandlerTests.cs | 384 ++++++++++++ TestBot/Bot/Bot.Hadler/CommandHandlerTests.cs | 528 ++++++++++++++++ TestBot/Bot/Bot.Hadler/MessageHandlerTests.cs | 435 ++++++++++++++ TestBot/Bot/Bot.Hadler/PaymentHandlerTests.cs | 487 +++++++++++++++ .../PreCheckoutQueryHandlerTests.cs | 272 +++++++++ .../Bot/Bot.Service/AdminStateServiceTests.cs | 455 ++++++++++++++ .../Bot/Bot.Service/UserStateServiceTests.cs | 446 ++++++++++++++ TestBot/Bot/UpdateHandlerTests.cs | 202 +++++++ TestBot/Data/DonationServiceTests.cs | 467 +++++++++++++++ TestBot/Data/GoalServiceTests.cs | 563 ++++++++++++++++++ TestBot/TestBot.csproj | 35 ++ docker-compose.yml | 39 ++ 50 files changed, 8143 insertions(+) create mode 100755 .editorconfig create mode 100644 .gitignore create mode 100755 Bot/Bot.csproj create mode 100755 Bot/Hadlers/AdminHandler.cs create mode 100755 Bot/Hadlers/CallbackQueryHandler.cs create mode 100755 Bot/Hadlers/CommandHandler.cs create mode 100755 Bot/Hadlers/IUpdateHandlerCommand.cs create mode 100755 Bot/Hadlers/MessageHandler.cs create mode 100755 Bot/Hadlers/PaymentHandler.cs create mode 100755 Bot/Hadlers/PreCheckoutQueryHandler.cs create mode 100755 Bot/IReceiverService.cs create mode 100755 Bot/PollingService.cs create mode 100755 Bot/Program.cs create mode 100755 Bot/ReceiverService.cs create mode 100755 Bot/Services/AdminGoalStep.cs create mode 100755 Bot/Services/AdminStateService.cs create mode 100755 Bot/Services/KeyboardService.cs create mode 100755 Bot/Services/UserStateService.cs create mode 100755 Bot/UpdateHandler.cs create mode 100755 Bot/appsettings.json create mode 100755 Configurations/BotConfig.cs create mode 100755 Configurations/Configurations.csproj create mode 100755 Configurations/DatabaseConfig.cs create mode 100755 Data/DapperRepository.cs create mode 100755 Data/Data.csproj create mode 100755 Data/IDapperRepository.cs create mode 100755 Data/Models/Donation.cs create mode 100755 Data/Models/DonationGoal.cs create mode 100755 Data/Models/Users.cs create mode 100755 Data/Scripts/create_tables.sql create mode 100755 Dockerfile create mode 100755 DonationBot.sln create mode 100755 Services/DonationService.cs create mode 100755 Services/GoalService.cs create mode 100755 Services/IDonationService.cs create mode 100755 Services/IGoalService.cs create mode 100755 Services/Services.csproj create mode 100755 TestBot/Bot/Bot.Hadler/AdminHandlerTests.cs create mode 100755 TestBot/Bot/Bot.Hadler/CallbackQueryHandlerTests.cs create mode 100755 TestBot/Bot/Bot.Hadler/CommandHandlerTests.cs create mode 100755 TestBot/Bot/Bot.Hadler/MessageHandlerTests.cs create mode 100755 TestBot/Bot/Bot.Hadler/PaymentHandlerTests.cs create mode 100755 TestBot/Bot/Bot.Hadler/PreCheckoutQueryHandlerTests.cs create mode 100755 TestBot/Bot/Bot.Service/AdminStateServiceTests.cs create mode 100755 TestBot/Bot/Bot.Service/UserStateServiceTests.cs create mode 100755 TestBot/Bot/UpdateHandlerTests.cs create mode 100755 TestBot/Data/DonationServiceTests.cs create mode 100755 TestBot/Data/GoalServiceTests.cs create mode 100755 TestBot/TestBot.csproj create mode 100755 docker-compose.yml diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..2410a5b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# SA1101: Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c1bc91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,90 @@ +## A streamlined .gitignore for modern .NET projects +## including temporary files, build results, and +## files generated by popular .NET tools. If you are +## developing with Visual Studio, the VS .gitignore +## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore +## has more thorough IDE-specific entries. +## +## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg + +# Others +~$* +*~ +CodeCoverage/ + +# MSBuild Binary and Structured Log +*.binlog + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Секреты +appsettings.Development.json +appsettings.Production.json +.env +*.env +secrets.json + +# IDE файлы +.vscode/ +.idea/ +*.swp +*.swo + +# Временные файлы +bin/ +obj/ +.vs/ +*.suo +*.user + +# Docker +docker-compose.override.yml + +# Логи +*.log +logs/ + +# Бинарные файлы +*.dll +*.exe +*.pdb + +# БД +*.db +__pycache__/ diff --git a/Bot/Bot.csproj b/Bot/Bot.csproj new file mode 100755 index 0000000..0962ab4 --- /dev/null +++ b/Bot/Bot.csproj @@ -0,0 +1,31 @@ + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + Always + + + + + Exe + net9.0 + enable + enable + 2579afaa-0ecc-4aa5-97ea-147ad5b1439f + + + diff --git a/Bot/Hadlers/AdminHandler.cs b/Bot/Hadlers/AdminHandler.cs new file mode 100755 index 0000000..bb7fe0d --- /dev/null +++ b/Bot/Hadlers/AdminHandler.cs @@ -0,0 +1,197 @@ +using Bot.Services; +using Microsoft.Extensions.Logging; +using Services; +using Telegram.Bot; +using Telegram.Bot.Types; +using static Bot.Services.AdminStateService; + +namespace Bot.Handlers; + +/// +/// Handles administrative operations for goal management including creation and validation. +/// +public class AdminHandler +{ + private readonly ILogger logger; + private readonly IGoalService goalService; + private readonly AdminStateService adminStateService; + + /// + /// Initializes a new instance of the class. + /// + /// Logger instance for tracking operations. + /// Service for goal-related operations. + /// Service for managing admin state. + public AdminHandler( + ILogger logger, + IGoalService goalService, + AdminStateService adminStateService) + { + this.logger = logger; + this.goalService = goalService; + this.adminStateService = adminStateService; + + this.logger.LogDebug("AdminHandler initialized successfully"); + } + + /// + /// Processes goal creation workflow for administrators. + /// + /// Telegram bot client instance. + /// Incoming message from user. + /// Cancellation token for async operations. + /// A representing the asynchronous operation. + public virtual async Task HandleAdminGoalCreationAsync(ITelegramBotClient botClient, Message message, CancellationToken cancellationToken) + { + if (message.From?.Id == null) + { + logger.LogWarning("Received message with null user ID"); + return; + } + + var userId = message.From.Id; + var chatId = message.Chat.Id; + var state = adminStateService.GetState(userId); + + if (state == null) + { + logger.LogWarning("No admin state found for user {UserId}", userId); + return; + } + + if (string.IsNullOrEmpty(message.Text)) + { + logger.LogWarning("Received empty message text from user {UserId}", userId); + return; + } + + try + { + switch (state.CurrentStep) + { + case AdminStateService.AdminGoalStep.WaitingForTitle: + await ProcessTitleStepAsync(botClient, userId, chatId, message.Text, cancellationToken); + break; + + case AdminStateService.AdminGoalStep.WaitingForDescription: + await ProcessDescriptionStepAsync(botClient, userId, chatId, message.Text, cancellationToken); + break; + + case AdminStateService.AdminGoalStep.WaitingForAmount: + await ProcessAmountStepAsync(botClient, userId, chatId, message.Text, state, cancellationToken); + break; + + default: + logger.LogWarning("Unknown admin step {CurrentStep} for user {UserId}", state.CurrentStep, userId); + break; + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing admin goal creation for user {UserId} at step {CurrentStep}", userId, state.CurrentStep); + await SendErrorMessageAsync(botClient, chatId, cancellationToken); + } + } + + /// + /// Initiates the goal creation process for administrators. + /// + /// Telegram bot client instance. + /// Chat identifier. + /// User identifier. + /// Cancellation token for async operations. + /// A representing the asynchronous operation. + public virtual async Task StartGoalCreationAsync(ITelegramBotClient botClient, long chatId, long userId, CancellationToken cancellationToken) + { + logger.LogInformation("Starting goal creation process for admin user {UserId}", userId); + + try + { + adminStateService.StartGoalCreation(userId, chatId); + await botClient.SendMessage(chatId, "🎯 Введите название новой цели:", cancellationToken: cancellationToken); + + logger.LogDebug("Goal creation started successfully for user {UserId}", userId); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to start goal creation for user {UserId}", userId); + throw; + } + } + + /// + /// Handles unauthorized admin access attempts. + /// + /// Telegram bot client instance. + /// Chat identifier. + /// Cancellation token for async operations. + /// A representing the asynchronous operation. + public virtual async Task HandleNotAdmin(ITelegramBotClient botClient, long chatId, CancellationToken cancellationToken) + { + logger.LogWarning("Non-admin access attempt detected for chat {ChatId}", chatId); + + await botClient.SendMessage(chatId, "❌ Вы не являетесь админом", cancellationToken: cancellationToken); + } + + private async Task ProcessTitleStepAsync(ITelegramBotClient botClient, long userId, long chatId, string title, CancellationToken cancellationToken) + { + if (title.Length >= 255) + { + logger.LogWarning("User {UserId} provided title that exceeds length limit: {TitleLength}", userId, title.Length); + + adminStateService.CancelGoalCreation(userId); + await botClient.SendMessage(chatId, "❌ Слишком длинное название цели \n Попробуйте создать цель заново:", cancellationToken: cancellationToken); + return; + } + + logger.LogDebug("Setting title for user {UserId}", userId); + adminStateService.SetTitle(userId, title); + await botClient.SendMessage(chatId, "📝 Введите описание цели:", cancellationToken: cancellationToken); + } + + private async Task ProcessDescriptionStepAsync(ITelegramBotClient botClient, long userId, long chatId, string description, CancellationToken cancellationToken) + { + logger.LogDebug("Setting description for user {UserId}", userId); + + adminStateService.SetDescription(userId, description); + await botClient.SendMessage(chatId, "💰 Введите целевую сумму в рублях:", cancellationToken: cancellationToken); + } + + private async Task ProcessAmountStepAsync(ITelegramBotClient botClient, long userId, long chatId, string amountText, AdminGoalCreationState state, CancellationToken cancellationToken) + { + if (!decimal.TryParse(amountText, out decimal amount) || amount <= 0 || amount >= 100000000) + { + logger.LogWarning("User {UserId} provided invalid amount: {AmountText}", userId, amountText); + + adminStateService.CancelGoalCreation(userId); + await botClient.SendMessage(chatId, "❌ Введите корректную сумму до 99999999 \n Попробуйте создать цель заново:", cancellationToken: cancellationToken); + return; + } + + try + { + logger.LogInformation("Creating goal for user {UserId} with amount {Amount}", userId, amount); + + var goal = await goalService.CreateGoalAsync(state.Title!, state.Description!, amount); + adminStateService.CancelGoalCreation(userId); + + await botClient.SendMessage( + chatId: chatId, + text: $"✅ Цель создана!\n🎯 {goal.Title}\n💫 Описание: {goal.Description} \n💰 Сумма: {amount}₽", + cancellationToken: cancellationToken); + + logger.LogInformation("Goal created successfully with ID {GoalId} for user {UserId}", goal.Id, userId); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create goal in database for user {UserId}", userId); + await SendErrorMessageAsync(botClient, chatId, cancellationToken); + } + } + + private async Task SendErrorMessageAsync(ITelegramBotClient botClient, long chatId, CancellationToken cancellationToken) => + await botClient.SendMessage( + chatId, + "❌ Произошла ошибка при создании цели. Пожалуйста, попробуйте позже.", + cancellationToken: cancellationToken); +} \ No newline at end of file diff --git a/Bot/Hadlers/CallbackQueryHandler.cs b/Bot/Hadlers/CallbackQueryHandler.cs new file mode 100755 index 0000000..b2e9cf8 --- /dev/null +++ b/Bot/Hadlers/CallbackQueryHandler.cs @@ -0,0 +1,178 @@ +using Bot.Services; +using Microsoft.Extensions.Logging; +using Services; +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; + +namespace Bot.Handlers; + +/// +/// Handles callback queries from inline keyboards in Telegram bot. +/// +public class CallbackQueryHandler : IUpdateHandlerCommand +{ + private readonly ILogger logger; + private readonly PaymentHandler paymentHandler; + private readonly CommandHandler commandHandler; + private readonly UserStateService userStateService; + + /// + /// Initializes a new instance of the class. + /// + /// Logger instance for tracking operations. + /// Handler for payment-related operations. + /// Handler for command processing. + /// Service for managing user state. + public CallbackQueryHandler( + ILogger logger, + PaymentHandler paymentHandler, + CommandHandler commandHandler, + UserStateService userStateService) + { + this.logger = logger; + this.paymentHandler = paymentHandler; + this.commandHandler = commandHandler; + this.userStateService = userStateService; + + this.logger.LogDebug("CallbackQueryHandler initialized successfully"); + } + + /// + /// Determines whether this handler can process the update. + /// + /// The update to check. + /// True if the update contains a callback query; otherwise, false. + public bool CanHandle(Update update) => update.CallbackQuery != null; + + /// + /// Processes callback query updates from inline keyboards. + /// + /// Telegram bot client instance. + /// The update containing callback query. + /// Cancellation token for async operations. + /// A representing the asynchronous operation. + public async Task HandleAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken) + { + var callbackQuery = update.CallbackQuery!; + logger.LogInformation("Received inline keyboard callback from user: {UserId}", callbackQuery.From.Id); + + if (callbackQuery.Message?.Chat == null) + { + logger.LogWarning("Callback query from user {UserId} has no chat information", callbackQuery.From.Id); + return; + } + + try + { + var callbackData = callbackQuery.Data; + var chatId = callbackQuery.Message.Chat.Id; + var userId = callbackQuery.From.Id; + + if (string.IsNullOrEmpty(callbackData)) + { + logger.LogWarning("Empty callback data received from user {UserId}", userId); + await botClient.AnswerCallbackQuery(callbackQuery.Id, "❌ Пустая команда", cancellationToken: cancellationToken); + return; + } + + await ProcessCallbackDataAsync(botClient, callbackQuery, callbackData, chatId, userId, cancellationToken); + } + catch (Exception ex) + { + logger.LogError(ex, "Error handling callback query from user {UserId}", callbackQuery.From.Id); + await SafeAnswerCallbackQueryAsync(botClient, callbackQuery.Id, "❌ Произошла ошибка", cancellationToken); + } + } + + private async Task ProcessCallbackDataAsync(ITelegramBotClient botClient, CallbackQuery callbackQuery, string callbackData, long chatId, long userId, CancellationToken cancellationToken) + { + switch (callbackData) + { + case "enter_custom_amount": + await HandleCustomAmountAsync(botClient, callbackQuery, chatId, userId, cancellationToken); + break; + + case "donate_100": + case "donate_500": + case "donate_1000": + case "donate_5000": + await HandlePredefinedDonationAsync(botClient, callbackQuery, callbackData, chatId, userId, cancellationToken); + break; + + case "show_stats": + await HandleShowStatsAsync(botClient, callbackQuery, chatId, cancellationToken); + break; + + default: + logger.LogWarning("Unknown callback data: {CallbackData} from user {UserId}", callbackData, userId); + await botClient.AnswerCallbackQuery(callbackQuery.Id, $"❌ Неизвестная команда: {callbackData}", cancellationToken: cancellationToken); + break; + } + } + + private async Task HandleCustomAmountAsync(ITelegramBotClient botClient, CallbackQuery callbackQuery, long chatId, long userId, CancellationToken cancellationToken) + { + logger.LogDebug("User {UserId} selected custom amount donation", userId); + + userStateService.SetWaitingForAmount(userId, chatId); + await botClient.AnswerCallbackQuery(callbackQuery.Id, "Введите сумму пожертвования в рублях", cancellationToken: cancellationToken); + await botClient.SendMessage(chatId, "💎 Введите сумму пожертвования в рублях:", cancellationToken: cancellationToken); + + logger.LogDebug("Custom amount prompt sent to user {UserId}", userId); + } + + private async Task HandlePredefinedDonationAsync(ITelegramBotClient botClient, CallbackQuery callbackQuery, string callbackData, long chatId, long userId, CancellationToken cancellationToken) + { + try + { + var amount = int.Parse(callbackData.Split('_')[1]); + logger.LogInformation("User {UserId} selected predefined donation amount: {Amount}", userId, amount); + + await paymentHandler.CreateDonationInvoice(botClient, chatId, userId, amount, cancellationToken); + await botClient.AnswerCallbackQuery(callbackQuery.Id, cancellationToken: cancellationToken); + + logger.LogDebug("Donation invoice created for user {UserId} with amount {Amount}", userId, amount); + } + catch (FormatException ex) + { + logger.LogError(ex, "Failed to parse donation amount from callback data: {CallbackData}", callbackData); + await botClient.AnswerCallbackQuery(callbackQuery.Id, "❌ Ошибка обработки суммы", cancellationToken: cancellationToken); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create donation invoice for user {UserId}", userId); + await botClient.AnswerCallbackQuery(callbackQuery.Id, "❌ Ошибка создания платежа", cancellationToken: cancellationToken); + } + } + + private async Task HandleShowStatsAsync(ITelegramBotClient botClient, CallbackQuery callbackQuery, long chatId, CancellationToken cancellationToken) + { + logger.LogDebug("User {UserId} requested statistics", callbackQuery.From.Id); + + try + { + await commandHandler.HandleStatsCommand(botClient, chatId, cancellationToken); + await botClient.AnswerCallbackQuery(callbackQuery.Id, cancellationToken: cancellationToken); + + logger.LogDebug("Statistics sent to user {UserId}", callbackQuery.From.Id); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to show statistics for user {UserId}", callbackQuery.From.Id); + await botClient.AnswerCallbackQuery(callbackQuery.Id, "❌ Ошибка загрузки статистики", cancellationToken: cancellationToken); + } + } + + private async Task SafeAnswerCallbackQueryAsync(ITelegramBotClient botClient, string callbackQueryId, string message, CancellationToken cancellationToken) + { + try + { + await botClient.AnswerCallbackQuery(callbackQueryId, message, cancellationToken: cancellationToken); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to answer callback query {CallbackQueryId}", callbackQueryId); + } + } +} \ No newline at end of file diff --git a/Bot/Hadlers/CommandHandler.cs b/Bot/Hadlers/CommandHandler.cs new file mode 100755 index 0000000..bf752e9 --- /dev/null +++ b/Bot/Hadlers/CommandHandler.cs @@ -0,0 +1,268 @@ +using Bot.Services; +using Data.Models; +using Microsoft.Extensions.Logging; +using Services; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace Bot.Handlers; + +/// +/// Handles bot commands and routes them to appropriate handlers. +/// +public class CommandHandler +{ + private readonly ILogger logger; + private readonly IGoalService goalService; + private readonly KeyboardService keyboardService; + private readonly AdminHandler adminHandler; + + /// + /// Initializes a new instance of the class. + /// + /// Logger instance for tracking operations. + /// Service for goal-related operations. + /// Service for keyboard management. + /// Handler for administrative operations. + public CommandHandler( + ILogger logger, + IGoalService goalService, + KeyboardService keyboardService, + AdminHandler adminHandler) + { + this.logger = logger; + this.goalService = goalService; + this.keyboardService = keyboardService; + this.adminHandler = adminHandler; + + this.logger.LogDebug("CommandHandler initialized successfully"); + } + + /// + /// Processes and routes bot commands to appropriate handlers. + /// + /// Telegram bot client instance. + /// The message containing the command. + /// Cancellation token for async operations. + /// A representing the asynchronous operation. + public virtual async Task HandleCommandAsync(ITelegramBotClient botClient, Message message, CancellationToken cancellationToken) + { + if (message.From?.Id == null) + { + logger.LogWarning("Received command from message with null user"); + return; + } + + if (string.IsNullOrEmpty(message.Text)) + { + logger.LogWarning("Received empty message text from user {UserId}", message.From.Id); + return; + } + + var chatId = message.Chat.Id; + var userId = message.From.Id; + var messageText = message.Text; + + logger.LogInformation("Processing command: {Command} from user {UserId}", messageText, userId); + + try + { + switch (messageText) + { + case "/start": + case "🔄 Обновить": + case "Обновить": + await HandleStartCommandAsync(botClient, message, cancellationToken); + break; + case "/donate": + case "💳 Пожертвовать": + case "Пожертвовать": + await HandleDonateCommandAsync(botClient, chatId, cancellationToken); + break; + case "/stats": + case "📊 Статистика": + case "Статистика": + await HandleStatsCommand(botClient, chatId, cancellationToken); + break; + case "/addgoal": + case "📝 Создать новую цель": + case "Создать новую цель": + await HandleAddGoalCommandAsync(botClient, chatId, userId, cancellationToken); + break; + default: + await HandleUnknownCommandAsync(botClient, message, cancellationToken); + break; + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing command {Command} from user {UserId}", messageText, userId); + await SendErrorMessageAsync(botClient, chatId, cancellationToken); + } + } + + /// + /// Handles statistics command requests. + /// + /// Telegram bot client instance. + /// Chat identifier. + /// Cancellation token for async operations. + /// A representing the asynchronous operation. + public virtual async Task HandleStatsCommand(ITelegramBotClient botClient, long chatId, CancellationToken cancellationToken) + { + logger.LogInformation("Processing stats command for chat {ChatId}", chatId); + + try + { + var stats = await goalService.GetGoalStatsAsync(); + await botClient.SendMessage( + chatId: chatId, + text: stats, + parseMode: ParseMode.Markdown, + cancellationToken: cancellationToken); + + logger.LogDebug("Statistics sent successfully to chat {ChatId}", chatId); + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting stats for chat {ChatId}", chatId); + await SendErrorMessageAsync(botClient, chatId, cancellationToken); + } + } + + private async Task HandleStartCommandAsync(ITelegramBotClient botClient, Message message, CancellationToken cancellationToken) + { + var chatId = message.Chat.Id; + var userId = message.From.Id; + + logger.LogDebug("Processing start command for user {UserId}", userId); + + try + { + var stats = await goalService.GetStartStats(); + var conclusion = $"🙏 Добро пожаловать в бота для сбора пожертвований! \n{stats} \n\nВыберите действие:"; + + var isAdmin = await goalService.IsUserAdminAsync(userId); + logger.LogDebug("User {UserId} admin status: {IsAdmin}", userId, isAdmin); + + var keyboard = isAdmin + ? keyboardService.GetMainMenuKeyboardForAdmin() + : keyboardService.GetMainMenuKeyboard(); + + await botClient.SendMessage( + chatId: chatId, + text: conclusion, + parseMode: ParseMode.Markdown, + replyMarkup: keyboard, + cancellationToken: cancellationToken); + + logger.LogDebug("Start command processed successfully for user {UserId}", userId); + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing start command for user {UserId}", userId); + await SendErrorMessageAsync(botClient, chatId, cancellationToken); + } + } + + private async Task HandleDonateCommandAsync(ITelegramBotClient botClient, long chatId, CancellationToken cancellationToken) + { + logger.LogDebug("Processing donate command for chat {ChatId}", chatId); + + try + { + var goal = await goalService.GetActiveGoalAsync(); + if (goal == null) + { + logger.LogWarning("No active goal found for donate command in chat {ChatId}", chatId); + await botClient.SendMessage( + chatId: chatId, + text: "❌ В данный момент нет активных целей для пожертвований.", + cancellationToken: cancellationToken); + return; + } + + var keyboard = keyboardService.GetDonationAmountKeyboard(); + await botClient.SendMessage( + chatId: chatId, + text: $"💝 **Пожертвование: {goal.Title}** \n\nВыберите сумму пожертвования или введите свою:", + parseMode: ParseMode.Markdown, + replyMarkup: keyboard, + cancellationToken: cancellationToken); + + logger.LogDebug("Donate command processed successfully for chat {ChatId} with goal {GoalTitle}", chatId, goal.Title); + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing donate command for chat {ChatId}", chatId); + await SendErrorMessageAsync(botClient, chatId, cancellationToken); + } + } + + private async Task HandleAddGoalCommandAsync(ITelegramBotClient botClient, long chatId, long userId, CancellationToken cancellationToken) + { + logger.LogDebug("Processing add goal command for user {UserId}", userId); + + try + { + if (await goalService.IsUserAdminAsync(userId)) + { + logger.LogInformation("Admin user {UserId} starting goal creation", userId); + await adminHandler.StartGoalCreationAsync(botClient, chatId, userId, cancellationToken); + } + else + { + logger.LogWarning("Non-admin user {UserId} attempted to create goal", userId); + await adminHandler.HandleNotAdmin(botClient, chatId, cancellationToken); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing add goal command for user {UserId}", userId); + await SendErrorMessageAsync(botClient, chatId, cancellationToken); + } + } + + private async Task HandleUnknownCommandAsync(ITelegramBotClient botClient, Message message, CancellationToken cancellationToken) + { + var chatId = message.Chat.Id; + var userId = message.From.Id; + + logger.LogWarning("Unknown command from user {UserId}: {CommandText}", userId, message.Text); + + try + { + var isAdmin = await goalService.IsUserAdminAsync(userId); + var responseText = isAdmin + ? "🤔 Неизвестная команда \n\n Доступные команды: \n• Нажмите 📊 Статистика для просмотра прогресса \n• Нажмите 📝 Создать новую цель" + : "🤔 Неизвестная команда \n\n Доступные команды: \n• Нажмите 📊 Статистика для просмотра прогресса \n• Нажмите 💳 Пожертвовать для помощи проекту"; + + await botClient.SendMessage( + chatId: chatId, + text: responseText, + cancellationToken: cancellationToken); + + logger.LogDebug("Unknown command response sent to user {UserId}", userId); + } + catch (Exception ex) + { + logger.LogError(ex, "Error sending unknown command response to user {UserId}", userId); + } + } + + private async Task SendErrorMessageAsync(ITelegramBotClient botClient, long chatId, CancellationToken cancellationToken) + { + try + { + await botClient.SendMessage( + chatId: chatId, + text: "❌ Не удалось выполнить команду. Попробуйте позже.", + cancellationToken: cancellationToken); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to send error message to chat {ChatId}", chatId); + } + } +} \ No newline at end of file diff --git a/Bot/Hadlers/IUpdateHandlerCommand.cs b/Bot/Hadlers/IUpdateHandlerCommand.cs new file mode 100755 index 0000000..6a89916 --- /dev/null +++ b/Bot/Hadlers/IUpdateHandlerCommand.cs @@ -0,0 +1,36 @@ +using Telegram.Bot; +using Telegram.Bot.Types; + +namespace Bot.Handlers; + +/// +/// Defines the contract for Telegram bot update command handlers. +/// +public interface IUpdateHandlerCommand +{ + /// + /// Asynchronously processes an incoming update from Telegram Bot. + /// + /// Telegram Bot client for API interactions. + /// The update containing data to process. + /// Cancellation token for the asynchronous operation. + /// A task representing the asynchronous processing operation. + /// + /// Implementations should only handle updates for which the method returns true. + /// In case of errors, it's recommended to log exceptions and handle them appropriately. + /// + Task HandleAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken); + + /// + /// Determines whether this handler can process the specified update. + /// + /// The update to check. + /// + /// true if this handler can process the update; otherwise, false. + /// + /// + /// This method should be fast and not perform heavy operations. + /// Typically checks the update type or presence of specific data. + /// + bool CanHandle(Update update); +} \ No newline at end of file diff --git a/Bot/Hadlers/MessageHandler.cs b/Bot/Hadlers/MessageHandler.cs new file mode 100755 index 0000000..31a9584 --- /dev/null +++ b/Bot/Hadlers/MessageHandler.cs @@ -0,0 +1,183 @@ +using Bot.Services; +using Data.Models; +using Microsoft.Extensions.Logging; +using Services; +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace Bot.Handlers; + +/// +/// Handles incoming messages and routes them to appropriate processors based on message type and user state. +/// +public class MessageHandler : IUpdateHandlerCommand +{ + private readonly ILogger logger; + private readonly IDonationService donationService; + private readonly IGoalService goalService; + private readonly CommandHandler commandHandler; + private readonly PaymentHandler paymentHandler; + private readonly UserStateService userStateService; + private readonly AdminHandler adminHandler; + private readonly AdminStateService adminStateService; + + /// + /// Initializes a new instance of the class. + /// + /// Logger instance for tracking operations. + /// Service for donation-related operations. + /// Service for goal-related operations. + /// Handler for command processing. + /// Handler for payment processing. + /// Service for managing user state. + /// Handler for administrative operations. + /// Service for managing admin state. + public MessageHandler( + ILogger logger, + IDonationService donationService, + IGoalService goalService, + CommandHandler commandHandler, + PaymentHandler paymentHandler, + UserStateService userStateService, + AdminHandler adminHandler, + AdminStateService adminStateService) + { + this.logger = logger; + this.donationService = donationService; + this.goalService = goalService; + this.commandHandler = commandHandler; + this.paymentHandler = paymentHandler; + this.userStateService = userStateService; + this.adminHandler = adminHandler; + this.adminStateService = adminStateService; + + this.logger.LogDebug("MessageHandler initialized successfully"); + } + + /// + /// Determines whether this handler can process the update. + /// + /// The update to check. + /// True if the update contains a message; otherwise, false. + public bool CanHandle(Update update) => update.Message != null; + + /// + /// Processes incoming messages and routes them based on message type and user state. + /// + /// Telegram bot client instance. + /// The update containing the message. + /// Cancellation token for async operations. + /// A representing the asynchronous operation. + public async Task HandleAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken) + { + var message = update.Message!; + + if (message.From is null) + { + logger.LogWarning("Received message with null user information"); + return; + } + + var userId = message.From.Id; + var chatId = message.Chat.Id; + + logger.LogDebug("Processing message from user {UserId} in chat {ChatId}", userId, chatId); + + try + { + await RegisterOrUpdateUserAsync(message); + + if (await ProcessSpecialMessageTypesAsync(botClient, message, cancellationToken)) + { + return; + } + + if (await ProcessUserStateBasedMessagesAsync(botClient, message, cancellationToken)) + { + return; + } + + await ProcessTextMessageAsync(botClient, message, cancellationToken); + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing message from user {UserId}", userId); + } + } + + private async Task RegisterOrUpdateUserAsync(Message message) + { + try + { + await donationService.GetOrCreateUserAsync( + message.From.Id, + message.From.Username, + message.From.FirstName, + message.From.LastName); + + logger.LogDebug("User {UserId} registered/updated successfully", message.From.Id); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to register/update user {UserId}", message.From.Id); + } + } + + private async Task ProcessSpecialMessageTypesAsync(ITelegramBotClient botClient, Message message, CancellationToken cancellationToken) + { + if (message.SuccessfulPayment != null) + { + logger.LogInformation("Processing successful payment from user {UserId}", message.From.Id); + await paymentHandler.HandleSuccessfulPaymentAsync(botClient, message, cancellationToken); + return true; + } + + return false; + } + + private async Task ProcessUserStateBasedMessagesAsync(ITelegramBotClient botClient, Message message, CancellationToken cancellationToken) + { + var userId = message.From.Id; + var chatId = message.Chat.Id; + + if (userStateService.IsWaitingForAmount(userId, chatId)) + { + logger.LogDebug("User {UserId} is in waiting for amount state", userId); + await paymentHandler.HandleCustomAmountInputAsync(botClient, message, cancellationToken); + return true; + } + + var isAdmin = await goalService.IsUserAdminAsync(userId); + if (isAdmin && adminStateService.IsUserCreatingGoal(userId)) + { + logger.LogDebug("Admin user {UserId} is in goal creation state", userId); + await adminHandler.HandleAdminGoalCreationAsync(botClient, message, cancellationToken); + return true; + } + + return false; + } + + private async Task ProcessTextMessageAsync(ITelegramBotClient botClient, Message message, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(message.Text)) + { + logger.LogDebug("Received non-text message from user {UserId}", message.From.Id); + return; + } + + logger.LogInformation("Processing text message from user {UserId}: {MessageText}", message.From.Id, message.Text); + + try + { + await commandHandler.HandleCommandAsync(botClient, message, cancellationToken); + logger.LogDebug("Text message processed successfully for user {UserId}", message.From.Id); + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing text message from user {UserId}", message.From.Id); + } + } +} \ No newline at end of file diff --git a/Bot/Hadlers/PaymentHandler.cs b/Bot/Hadlers/PaymentHandler.cs new file mode 100755 index 0000000..94304ec --- /dev/null +++ b/Bot/Hadlers/PaymentHandler.cs @@ -0,0 +1,279 @@ +using Bot.Services; +using Configurations; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Services; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegram.Bot.Types.Payments; + +namespace Bot.Handlers; + +/// +/// Handles payment-related operations including donation invoices and payment processing. +/// +public class PaymentHandler +{ + private readonly ILogger logger; + private readonly IGoalService goalService; + private readonly IDonationService donationService; + private readonly UserStateService userStateService; + private readonly BotConfig botConfig; + + private const int MinimumDonationAmount = 60; + private const int MaximumDonationAmount = 100000; + + /// + /// Initializes a new instance of the class. + /// + /// Logger instance for tracking operations. + /// Service for goal-related operations. + /// Service for donation processing. + /// Service for managing user state. + /// Bot configuration options. + public PaymentHandler( + ILogger logger, + IGoalService goalService, + IDonationService donationService, + UserStateService userStateService, + IOptions botConfig) + { + this.logger = logger; + this.goalService = goalService; + this.donationService = donationService; + this.userStateService = userStateService; + this.botConfig = botConfig.Value; + + this.logger.LogDebug("PaymentHandler initialized successfully"); + } + + /// + /// Processes custom donation amount input from users. + /// + /// Telegram bot client instance. + /// The message containing the custom amount. + /// Cancellation token for async operations. + /// A representing the asynchronous operation. + public virtual async Task HandleCustomAmountInputAsync(ITelegramBotClient botClient, Message message, CancellationToken cancellationToken) + { + if (message.From?.Id == null) + { + logger.LogWarning("Received custom amount input from null user"); + return; + } + + var userId = message.From.Id; + var chatId = message.Chat.Id; + + logger.LogDebug("Processing custom amount input from user {UserId}", userId); + + userStateService.RemoveWaitingForAmount(userId); + + if (string.IsNullOrEmpty(message.Text)) + { + logger.LogWarning("User {UserId} sent empty custom amount", userId); + await SendInvalidAmountMessageAsync(botClient, chatId, cancellationToken); + return; + } + + if (!int.TryParse(message.Text, out int amount) || amount <= 0) + { + logger.LogWarning("User {UserId} sent invalid custom amount: {AmountText}", userId, message.Text); + await SendInvalidAmountMessageAsync(botClient, chatId, cancellationToken); + return; + } + + await CreateDonationInvoice(botClient, chatId, userId, amount, cancellationToken); + } + + /// + /// Creates a donation invoice for the specified amount. + /// + /// Telegram bot client instance. + /// Chat identifier. + /// User identifier. + /// Donation amount in rubles. + /// Cancellation token for async operations. + /// A representing the asynchronous operation. + public virtual async Task CreateDonationInvoice(ITelegramBotClient botClient, long chatId, long userId, int amountRub, CancellationToken cancellationToken) + { + logger.LogInformation("Creating donation invoice for user {UserId} with amount {AmountRub} RUB", userId, amountRub); + + try + { + var goal = await goalService.GetActiveGoalAsync(); + if (goal == null) + { + logger.LogWarning("No active goal found for donation from user {UserId}", userId); + await botClient.SendMessage(chatId, "❌ В данный момент нет активных целей для пожертвований.", cancellationToken: cancellationToken); + return; + } + + if (!ValidateDonationAmount(amountRub)) + { + logger.LogWarning("User {UserId} provided invalid donation amount: {AmountRub}", userId, amountRub); + await SendAmountValidationMessageAsync(botClient, chatId, cancellationToken); + return; + } + + await SendDonationInvoiceAsync(botClient, chatId, userId, amountRub, goal, cancellationToken); + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating donation invoice for user {UserId}", userId); + await SendErrorMessageAsync(botClient, chatId, cancellationToken); + } + } + + /// + /// Processes successful payments and updates donation records. + /// + /// Telegram bot client instance. + /// The message containing successful payment information. + /// Cancellation token for async operations. + /// A representing the asynchronous operation. + public virtual async Task HandleSuccessfulPaymentAsync(ITelegramBotClient botClient, Message message, CancellationToken cancellationToken) + { + if (message.SuccessfulPayment == null || message.From == null) + { + logger.LogWarning("Received successful payment with null payment or user information"); + return; + } + + var payment = message.SuccessfulPayment; + var user = message.From; + var chatId = message.Chat.Id; + + logger.LogInformation("Processing successful payment from user {UserId}, charge ID: {ChargeId}", user.Id, payment.TelegramPaymentChargeId); + + try + { + await DeleteInvoiceMessageAsync(botClient, chatId, message.MessageId); + + var amount = payment.TotalAmount / 100m; + var success = await donationService.ProcessDonationAsync(user.Id, amount, payment.Currency, payment.TelegramPaymentChargeId); + + if (success) + { + await SendThankYouMessageAsync(botClient, chatId, amount, payment.Currency, cancellationToken); + logger.LogInformation("Successfully processed donation from user {UserId}, amount: {Amount} {Currency}", user.Id, amount, payment.Currency); + } + else + { + await SendDonationProcessingErrorMessageAsync(botClient, chatId, cancellationToken); + logger.LogError("Failed to process donation from user {UserId}, charge ID: {ChargeId}", user.Id, payment.TelegramPaymentChargeId); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error handling successful payment from user {UserId}", user.Id); + await SendPaymentProcessingErrorMessageAsync(botClient, chatId, cancellationToken); + } + } + + private bool ValidateDonationAmount(int amountRub) + { + if (amountRub > MaximumDonationAmount) + { + logger.LogWarning("Donation amount {AmountRub} exceeds maximum limit", amountRub); + return false; + } + + if (amountRub < MinimumDonationAmount) + { + logger.LogWarning("Donation amount {AmountRub} below minimum limit", amountRub); + return false; + } + + return true; + } + + private async Task SendDonationInvoiceAsync(ITelegramBotClient botClient, long chatId, long userId, int amountRub, Data.Models.DonationGoal goal, CancellationToken cancellationToken) + { + var amountKopecks = amountRub * 100; + var prices = new[] { new LabeledPrice("Пожертвование", amountKopecks) }; + var payload = $"donation_{goal.Id}_{userId}_{DateTime.UtcNow.Ticks}"; + + if (string.IsNullOrEmpty(botConfig.PaymentProviderToken)) + { + logger.LogError("Payment provider token is not configured"); + throw new InvalidOperationException("Payment provider token is not configured"); + } + + await botClient.SendInvoice( + chatId: chatId, + title: $"Пожертвование: {goal.Title}", + description: $"Пожертвование на сумму {amountRub} руб.", + payload: payload, + providerToken: botConfig.PaymentProviderToken, + currency: "RUB", + prices: prices, + cancellationToken: cancellationToken); + + logger.LogInformation("Donation invoice created for user {UserId}, amount: {AmountRub} RUB, goal: {GoalTitle}", userId, amountRub, goal.Title); + } + + private async Task DeleteInvoiceMessageAsync(ITelegramBotClient botClient, long chatId, int messageId) + { + try + { + await botClient.DeleteMessage(chatId, messageId); + logger.LogDebug("Invoice message {MessageId} deleted successfully", messageId); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Could not delete invoice message {MessageId}", messageId); + } + } + + private async Task SendThankYouMessageAsync(ITelegramBotClient botClient, long chatId, decimal amount, string currency, CancellationToken cancellationToken) + { + var stats = await goalService.GetStartStats(); + await botClient.SendMessage( + chatId: chatId, + text: $"✅ **Спасибо за ваше пожертвование!** \n\n💝 Вы пожертвовали: {amount:N2} {currency} \n{stats} \n\nВаша поддержка очень важна для нас!", + parseMode: ParseMode.Markdown, + cancellationToken: cancellationToken); + } + + private async Task SendInvalidAmountMessageAsync(ITelegramBotClient botClient, long chatId, CancellationToken cancellationToken) + { + await botClient.SendMessage( + chatId: chatId, + text: "❌ Пожалуйста, введите корректную сумму в рублях (только цифры)", + cancellationToken: cancellationToken); + } + + private async Task SendAmountValidationMessageAsync(ITelegramBotClient botClient, long chatId, CancellationToken cancellationToken) + { + await botClient.SendMessage( + chatId: chatId, + text: $"❌ Сумма пожертвования должна быть от {MinimumDonationAmount} до {MaximumDonationAmount} руб.", + cancellationToken: cancellationToken); + } + + private async Task SendErrorMessageAsync(ITelegramBotClient botClient, long chatId, CancellationToken cancellationToken) + { + await botClient.SendMessage( + chatId: chatId, + text: "❌ Произошла ошибка при создании платежа. Попробуйте позже.", + cancellationToken: cancellationToken); + } + + private async Task SendDonationProcessingErrorMessageAsync(ITelegramBotClient botClient, long chatId, CancellationToken cancellationToken) + { + await botClient.SendMessage( + chatId: chatId, + text: "❌ Произошла ошибка при обработке вашего платежа. Мы уже работаем над этим.", + cancellationToken: cancellationToken); + } + + private async Task SendPaymentProcessingErrorMessageAsync(ITelegramBotClient botClient, long chatId, CancellationToken cancellationToken) + { + await botClient.SendMessage( + chatId: chatId, + text: "❌ Произошла ошибка при обработке платежа. Мы уже работаем над этим.", + cancellationToken: cancellationToken); + } +} \ No newline at end of file diff --git a/Bot/Hadlers/PreCheckoutQueryHandler.cs b/Bot/Hadlers/PreCheckoutQueryHandler.cs new file mode 100755 index 0000000..eb742bd --- /dev/null +++ b/Bot/Hadlers/PreCheckoutQueryHandler.cs @@ -0,0 +1,119 @@ +using Bot.Services; +using Microsoft.Extensions.Logging; +using Services; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Payments; + +namespace Bot.Handlers; + +/// +/// Handles pre-checkout queries to validate payment requests before processing. +/// +public class PreCheckoutQueryHandler : IUpdateHandlerCommand +{ + private readonly ILogger logger; + private readonly IGoalService goalService; + + /// + /// Initializes a new instance of the class. + /// + /// Logger instance for tracking operations. + /// Service for goal-related operations. + public PreCheckoutQueryHandler( + ILogger logger, + IGoalService goalService) + { + this.logger = logger; + this.goalService = goalService; + + this.logger.LogDebug("PreCheckoutQueryHandler initialized successfully"); + } + + /// + /// Determines whether this handler can process the update. + /// + /// The update to check. + /// True if the update contains a pre-checkout query; otherwise, false. + public bool CanHandle(Update update) => update.PreCheckoutQuery != null; + + /// + /// Processes pre-checkout queries to validate payment requests. + /// + /// Telegram bot client instance. + /// The update containing the pre-checkout query. + /// Cancellation token for async operations. + /// A representing the asynchronous operation. + public async Task HandleAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken) + { + var preCheckoutQuery = update.PreCheckoutQuery!; + + if (preCheckoutQuery.From == null) + { + logger.LogWarning("Received pre-checkout query with null user information"); + return; + } + + logger.LogInformation( + "Processing pre-checkout query from user {UserId} with payload {InvoicePayload}", preCheckoutQuery.From.Id, preCheckoutQuery.InvoicePayload); + + try + { + var goal = await goalService.GetActiveGoalAsync(); + + if (goal != null) + { + await botClient.AnswerPreCheckoutQuery( + preCheckoutQueryId: preCheckoutQuery.Id, + cancellationToken: cancellationToken); + + logger.LogInformation( + "Approved pre-checkout query for user {UserId}, payload: {InvoicePayload}, amount: {Amount} {Currency}", + preCheckoutQuery.From.Id, preCheckoutQuery.InvoicePayload, preCheckoutQuery.TotalAmount / 100m, preCheckoutQuery.Currency); + } + else + { + logger.LogWarning( + "Rejected pre-checkout query from user {UserId} - no active goal found", + preCheckoutQuery.From.Id); + + await botClient.AnswerPreCheckoutQuery( + preCheckoutQueryId: preCheckoutQuery.Id, + errorMessage: "В данный момент нет активных целей для пожертвований", + cancellationToken: cancellationToken); + } + } + catch (Exception ex) + { + logger.LogError( + ex, + "Error handling pre-checkout query from user {UserId}", + preCheckoutQuery.From.Id); + + await SafeAnswerPreCheckoutQueryAsync( + botClient, + preCheckoutQuery.Id, + "Произошла ошибка при обработке платежа", + cancellationToken); + } + } + + private async Task SafeAnswerPreCheckoutQueryAsync( + ITelegramBotClient botClient, + string preCheckoutQueryId, + string errorMessage, + CancellationToken cancellationToken) + { + try + { + await botClient.AnswerPreCheckoutQuery( + preCheckoutQueryId: preCheckoutQueryId, + errorMessage: errorMessage, + cancellationToken: cancellationToken); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to answer pre-checkout query {PreCheckoutQueryId}", preCheckoutQueryId); + } + } +} \ No newline at end of file diff --git a/Bot/IReceiverService.cs b/Bot/IReceiverService.cs new file mode 100755 index 0000000..01c0f83 --- /dev/null +++ b/Bot/IReceiverService.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bot; + +/// +/// Defines a contract for receiving and processing updates in the Telegram bot. +/// +public interface IReceiverService +{ + /// + /// Starts receiving and processing updates from the Telegram bot. + /// + /// Cancellation token to stop the receiving process. + /// A representing the asynchronous operation. + Task ReceiveAsync(CancellationToken stoppingToken); +} \ No newline at end of file diff --git a/Bot/PollingService.cs b/Bot/PollingService.cs new file mode 100755 index 0000000..272690b --- /dev/null +++ b/Bot/PollingService.cs @@ -0,0 +1,124 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Telegram.Bot; +using Telegram.Bot.Exceptions; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace Bot; + +/// +/// Background service for polling Telegram Bot API for updates. +/// +public class PollingService : BackgroundService +{ + private readonly IServiceProvider serviceProvider; + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The service provider for dependency injection. + /// Logger instance for tracking operations. + public PollingService(IServiceProvider serviceProvider, ILogger logger) + { + this.serviceProvider = serviceProvider; + this.logger = logger; + } + + /// + /// Executes the polling service as a background task. + /// + /// Cancellation token to stop the service. + /// A representing the asynchronous operation. + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + logger.LogInformation("Polling service is starting"); + + try + { + await RunPollingLoopAsync(stoppingToken); + } + catch (Exception ex) + { + logger.LogCritical(ex, "Polling service encountered a critical error and is stopping"); + } + finally + { + logger.LogInformation("Polling service has stopped"); + } + } + + private async Task RunPollingLoopAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + await ProcessPollingIterationAsync(stoppingToken); + } + catch (Exception ex) + { + await HandlePollingErrorAsync(ex, stoppingToken); + } + } + } + + private async Task ProcessPollingIterationAsync(CancellationToken stoppingToken) + { + // Create a new scope for each iteration to ensure fresh service instances + using var scope = serviceProvider.CreateScope(); + var receiver = scope.ServiceProvider.GetRequiredService(); + + logger.LogDebug("Starting polling iteration"); + + await receiver.ReceiveAsync(stoppingToken); + + logger.LogDebug("Completed polling iteration successfully"); + } + + private async Task HandlePollingErrorAsync(Exception ex, CancellationToken stoppingToken) + { + logger.LogError(ex, "Polling iteration failed with exception"); + + // Handle specific Telegram API exceptions + switch (ex) + { + case ApiRequestException apiEx: + logger.LogWarning("Telegram API request failed: {ErrorCode} - {Message}", apiEx.ErrorCode, apiEx.Message); + break; + + case RequestException reqEx: + logger.LogWarning("Telegram request failed, possible network issue: {Message}", reqEx.Message); + break; + } + + // Cooldown before retrying after errors + logger.LogInformation("Waiting 5 seconds before retrying after error"); + + try + { + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + } + catch (TaskCanceledException) + { + logger.LogDebug("Cooldown delay was cancelled"); + } + } + + /// + /// Triggered when the application host is performing a graceful shutdown. + /// + /// Cancellation token indicating that shutdown should be no longer graceful. + /// A representing the asynchronous operation. + public override async Task StopAsync(CancellationToken cancellationToken) + { + logger.LogInformation("Polling service is stopping gracefully"); + + await base.StopAsync(cancellationToken); + + logger.LogInformation("Polling service stopped gracefully"); + } +} \ No newline at end of file diff --git a/Bot/Program.cs b/Bot/Program.cs new file mode 100755 index 0000000..ee6b5d3 --- /dev/null +++ b/Bot/Program.cs @@ -0,0 +1,128 @@ +using Bot; +using Bot.Handlers; +using Bot.Services; +using Configurations; +using Data; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Services; +using Telegram.Bot; +using Telegram.Bot.Polling; + +internal class Program +{ + private static async Task Main(string[] args) + { + var builder = Host.CreateApplicationBuilder(args); + + // Configure logging + builder.Logging.AddConsole(); + builder.Logging.AddDebug(); + + // Add configuration + builder.Services.Configure(builder.Configuration.GetSection("BotConfig")); + builder.Services.Configure(builder.Configuration.GetSection("DatabaseConfig")); + builder.Configuration.AddUserSecrets(); + + // Validate critical configurations + ValidateConfiguration(builder); + + // Register data layer services + builder.Services.AddScoped(); + + // Register business logic services + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + // Register update handling infrastructure + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + // Register command handlers + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + // Register update handler commands + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + // Register state management services + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + // Register Telegram Bot Client + builder.Services.AddSingleton(sp => + { + var config = sp.GetRequiredService>().Value; + + if (string.IsNullOrEmpty(config.BotToken)) + { + var logger = sp.GetRequiredService>(); + logger.LogCritical("Bot token is not configured"); + throw new InvalidOperationException("Bot token is not configured"); + } + + var botClient = new TelegramBotClient(config.BotToken); + + var loggerFactory = sp.GetRequiredService(); + var botLogger = loggerFactory.CreateLogger(); + botLogger.LogInformation("Telegram Bot Client initialized successfully"); + + return botClient; + }); + + // Register background service + builder.Services.AddHostedService(); + + var host = builder.Build(); + + host.Run(); + } + + /// + /// Validates critical configuration settings required for application startup. + /// + /// The host application builder. + private static void ValidateConfiguration(HostApplicationBuilder builder) + { + var botConfig = builder.Configuration.GetSection("BotConfig").Get(); + var databaseConfig = builder.Configuration.GetSection("DatabaseConfig").Get(); + + if (botConfig == null) + { + throw new InvalidOperationException("BotConfig section is missing from configuration"); + } + + if (string.IsNullOrEmpty(botConfig.BotToken)) + { + throw new InvalidOperationException("BotToken is not configured in BotConfig"); + } + + if (string.IsNullOrEmpty(botConfig.PaymentProviderToken)) + { + throw new InvalidOperationException("PaymentProviderToken is not configured in BotConfig"); + } + + if (databaseConfig == null) + { + throw new InvalidOperationException("DatabaseConfig section is missing from configuration"); + } + + if (string.IsNullOrEmpty(databaseConfig.ConnectionString)) + { + throw new InvalidOperationException("ConnectionString is not configured in DatabaseConfig"); + } + + // Log validation success (we'll use a temporary logger since we don't have DI yet) + Console.WriteLine("Configuration validation completed successfully"); + } +} \ No newline at end of file diff --git a/Bot/ReceiverService.cs b/Bot/ReceiverService.cs new file mode 100755 index 0000000..4fba2fb --- /dev/null +++ b/Bot/ReceiverService.cs @@ -0,0 +1,70 @@ +using Microsoft.Extensions.Logging; +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegram.Bot.Types.Enums; + +namespace Bot; + +/// +/// Receiver service acting as a bridge between polling and update processing. +/// +public class ReceiverService : IReceiverService +{ + private readonly ITelegramBotClient botClient; + private readonly IUpdateHandler updateHandler; + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// Telegram bot client instance. + /// Handler for processing updates. + /// Logger instance for tracking operations. + public ReceiverService( + ITelegramBotClient botClient, + IUpdateHandler updateHandler, + ILogger logger) + { + this.botClient = botClient; + this.updateHandler = updateHandler; + this.logger = logger; + + this.logger.LogDebug("ReceiverService initialized"); + } + + /// + /// Starts receiving updates from Telegram Bot API. + /// + /// Cancellation token to stop receiving updates. + /// A representing the asynchronous operation. + public async Task ReceiveAsync(CancellationToken stoppingToken) + { + var receiverOptions = new ReceiverOptions() + { + AllowedUpdates = Array.Empty(), + DropPendingUpdates = true, + }; + + try + { + // Get bot information + var me = await botClient.GetMe(stoppingToken); + logger.LogInformation("Bot @{BotUsername} (ID: {BotId}) is starting to receive updates", me.Username, me.Id); + + // Start receiving updates (blocking call) + logger.LogDebug("Starting to receive updates with configured options"); + + await botClient.ReceiveAsync( + updateHandler: updateHandler, + receiverOptions: receiverOptions, + cancellationToken: stoppingToken); + + logger.LogInformation("Bot @{BotUsername} stopped receiving updates", me.Username); + } + catch (Exception ex) + { + logger.LogError(ex, "Error occurred while receiving updates"); + throw; + } + } +} \ No newline at end of file diff --git a/Bot/Services/AdminGoalStep.cs b/Bot/Services/AdminGoalStep.cs new file mode 100755 index 0000000..5742ed4 --- /dev/null +++ b/Bot/Services/AdminGoalStep.cs @@ -0,0 +1,30 @@ +namespace Bot.Services; + +public partial class AdminStateService +{ + /// + /// Represents the steps in the admin goal creation workflow. + /// + public enum AdminGoalStep + { + /// + /// No active goal creation. + /// + None, + + /// + /// Waiting for goal title input. + /// + WaitingForTitle, + + /// + /// Waiting for goal description input. + /// + WaitingForDescription, + + /// + /// Waiting for target amount input. + /// + WaitingForAmount, + } +} \ No newline at end of file diff --git a/Bot/Services/AdminStateService.cs b/Bot/Services/AdminStateService.cs new file mode 100755 index 0000000..75bb4e5 --- /dev/null +++ b/Bot/Services/AdminStateService.cs @@ -0,0 +1,190 @@ +using Microsoft.Extensions.Logging; + +namespace Bot.Services; + +/// +/// Manages administrative state for goal creation workflow. +/// +public partial class AdminStateService +{ + private readonly ILogger logger; + private readonly Dictionary adminStates = []; + + /// + /// Represents the state of an admin user during goal creation process. + /// + public class AdminGoalCreationState + { + /// + /// Gets or sets the chat identifier where goal creation is taking place. + /// + public long ChatId { get; set; } + + /// + /// Gets or sets the goal title. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the goal description. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the target amount for the goal. + /// + public decimal? TargetAmount { get; set; } + + /// + /// Gets or sets the current step in the goal creation process. + /// + public AdminGoalStep CurrentStep { get; set; } = AdminGoalStep.None; + } + + /// + /// Initializes a new instance of the class. + /// + /// Logger instance for tracking operations. + public AdminStateService(ILogger logger) + { + this.logger = logger; + this.logger.LogDebug("AdminStateService initialized"); + } + + /// + /// Starts the goal creation process for an admin user. + /// + /// The user identifier. + /// The chat identifier. + public virtual void StartGoalCreation(long userId, long chatId) + { + adminStates[userId] = new AdminGoalCreationState + { + ChatId = chatId, + CurrentStep = AdminGoalStep.WaitingForTitle, + }; + + logger.LogInformation("Started goal creation for admin user {UserId} in chat {ChatId}", userId, chatId); + } + + /// + /// Retrieves the current state for an admin user. + /// + /// The user identifier. + /// The admin goal creation state if found; otherwise, null. + public virtual AdminGoalCreationState? GetState(long userId) + { + var stateExists = adminStates.TryGetValue(userId, out var state); + + if (!stateExists) + { + logger.LogDebug("No state found for user {UserId}", userId); + } + + return state; + } + + /// + /// Sets the title for the goal being created and advances to the next step. + /// + /// The user identifier. + /// The goal title. + public virtual void SetTitle(long userId, string title) + { + if (adminStates.TryGetValue(userId, out var state)) + { + state.Title = title; + state.CurrentStep = AdminGoalStep.WaitingForDescription; + + logger.LogDebug("Set title for user {UserId}: {Title}", userId, title); + } + else + { + logger.LogWarning("Attempted to set title for non-existent user state: {UserId}", userId); + } + } + + /// + /// Sets the description for the goal being created and advances to the next step. + /// + /// The user identifier. + /// The goal description. + public virtual void SetDescription(long userId, string description) + { + if (adminStates.TryGetValue(userId, out var state)) + { + state.Description = description; + state.CurrentStep = AdminGoalStep.WaitingForAmount; + + logger.LogDebug("Set description for user {UserId}", userId); + } + else + { + logger.LogWarning("Attempted to set description for non-existent user state: {UserId}", userId); + } + } + + /// + /// Sets the target amount for the goal being created and completes the process. + /// + /// The user identifier. + /// The target amount. + public void SetAmount(long userId, decimal amount) + { + if (adminStates.TryGetValue(userId, out var state)) + { + state.TargetAmount = amount; + state.CurrentStep = AdminGoalStep.None; + + logger.LogDebug("Set amount for user {UserId}: {Amount}", userId, amount); + } + else + { + logger.LogWarning("Attempted to set amount for non-existent user state: {UserId}", userId); + } + } + + /// + /// Cancels the goal creation process for a user. + /// + /// The user identifier. + public virtual void CancelGoalCreation(long userId) + { + var removed = adminStates.Remove(userId); + + if (removed) + { + logger.LogInformation("Canceled goal creation for user {UserId}", userId); + } + else + { + logger.LogDebug("Attempted to cancel non-existent goal creation for user {UserId}", userId); + } + } + + /// + /// Checks if a user is currently in the process of creating a goal. + /// + /// The user identifier. + /// True if the user is creating a goal; otherwise, false. + public virtual bool IsUserCreatingGoal(long userId) + { + var isCreating = adminStates.ContainsKey(userId) && + adminStates[userId].CurrentStep != AdminGoalStep.None; + + logger.LogDebug("User {UserId} goal creation status: {IsCreating}", userId, isCreating); + + return isCreating; + } + + /// + /// Gets the number of active admin states (for monitoring purposes). + /// + /// The count of active admin states. + public int GetActiveStateCount() + { + var count = adminStates.Count; + logger.LogTrace("Current active admin states: {Count}", count); + return count; + } +} \ No newline at end of file diff --git a/Bot/Services/KeyboardService.cs b/Bot/Services/KeyboardService.cs new file mode 100755 index 0000000..247c8ba --- /dev/null +++ b/Bot/Services/KeyboardService.cs @@ -0,0 +1,131 @@ +using Microsoft.Extensions.Logging; +using Telegram.Bot.Types.ReplyMarkups; + +namespace Bot.Services; + +/// +/// Service for creating and managing Telegram bot keyboards and markup. +/// +public class KeyboardService +{ + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// Logger instance for tracking operations. + public KeyboardService(ILogger logger) + { + this.logger = logger; + this.logger.LogDebug("KeyboardService initialized"); + } + + /// + /// Creates the main menu keyboard for regular users. + /// + /// Configured reply keyboard markup for main menu. + public virtual ReplyKeyboardMarkup GetMainMenuKeyboard() + { + logger.LogDebug("Creating main menu keyboard for regular user"); + + return new ReplyKeyboardMarkup( + [ + [new KeyboardButton("📊 Статистика"), new KeyboardButton("💳 Пожертвовать")], + [new KeyboardButton("🔄 Обновить")], + ]) + { + ResizeKeyboard = true, + OneTimeKeyboard = false, + }; + } + + /// + /// Creates the main menu keyboard for admin users with administrative options. + /// + /// Configured reply keyboard markup for admin main menu. + public virtual ReplyKeyboardMarkup GetMainMenuKeyboardForAdmin() + { + logger.LogDebug("Creating main menu keyboard for admin user"); + + return new ReplyKeyboardMarkup( + [ + [new KeyboardButton("📊 Статистика"), new KeyboardButton("📝 Создать новую цель")], + [new KeyboardButton("🔄 Обновить")], + ]) + { + ResizeKeyboard = true, + OneTimeKeyboard = false, + }; + } + + /// + /// Creates an inline keyboard for donation amount selection. + /// + /// Configured inline keyboard markup for donation amounts. + public virtual InlineKeyboardMarkup GetDonationAmountKeyboard() + { + logger.LogDebug("Creating donation amount inline keyboard"); + + try + { + var keyboard = new InlineKeyboardMarkup(new[] + { + new[] + { + InlineKeyboardButton.WithCallbackData("100 ₽", "donate_100"), + InlineKeyboardButton.WithCallbackData("500 ₽", "donate_500"), + }, + new[] + { + InlineKeyboardButton.WithCallbackData("1000 ₽", "donate_1000"), + InlineKeyboardButton.WithCallbackData("5000 ₽", "donate_5000"), + }, + new[] + { + InlineKeyboardButton.WithCallbackData("💎 Другая сумма", "enter_custom_amount"), + }, + }); + + logger.LogDebug("Donation amount keyboard created successfully"); + return keyboard; + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating donation amount keyboard"); + throw; + } + } + + /// + /// Creates a simple inline keyboard with a single back button. + /// + /// The callback data for the back button. + /// Configured inline keyboard markup with back button. + public InlineKeyboardMarkup GetBackButtonKeyboard(string callbackData = "back_to_main") + { + logger.LogDebug("Creating back button keyboard with callback data: {CallbackData}", callbackData); + + return new InlineKeyboardMarkup( + InlineKeyboardButton.WithCallbackData("⬅️ Назад", callbackData)); + } + + /// + /// Creates a confirmation inline keyboard for important actions. + /// + /// Callback data for confirm action. + /// Callback data for cancel action. + /// Configured inline keyboard markup for confirmation. + public InlineKeyboardMarkup GetConfirmationKeyboard(string confirmCallbackData = "confirm", string cancelCallbackData = "cancel") + { + logger.LogDebug("Creating confirmation keyboard with confirm: {Confirm}, cancel: {Cancel}", confirmCallbackData, cancelCallbackData); + + return new InlineKeyboardMarkup(new[] + { + new[] + { + InlineKeyboardButton.WithCallbackData("✅ Подтвердить", confirmCallbackData), + InlineKeyboardButton.WithCallbackData("❌ Отмена", cancelCallbackData), + }, + }); + } +} \ No newline at end of file diff --git a/Bot/Services/UserStateService.cs b/Bot/Services/UserStateService.cs new file mode 100755 index 0000000..7350200 --- /dev/null +++ b/Bot/Services/UserStateService.cs @@ -0,0 +1,110 @@ +using Microsoft.Extensions.Logging; + +namespace Bot.Services; + +/// +/// Manages user state for tracking user interactions and input expectations. +/// +public class UserStateService +{ + private readonly ILogger logger; + private readonly Dictionary waitingForAmount = []; + + /// + /// Initializes a new instance of the class. + /// + /// Logger instance for tracking operations. + public UserStateService(ILogger logger) + { + this.logger = logger; + this.logger.LogDebug("UserStateService initialized"); + } + + /// + /// Sets a user to waiting for amount input state. + /// + /// The user identifier. + /// The chat identifier. + public virtual void SetWaitingForAmount(long userId, long chatId) + { + waitingForAmount[userId] = chatId; + logger.LogDebug("User {UserId} set to waiting for amount input in chat {ChatId}", userId, chatId); + } + + /// + /// Checks if a user is waiting for amount input in the specified chat. + /// + /// The user identifier. + /// The chat identifier. + /// True if the user is waiting for amount input in the specified chat; otherwise, false. + public virtual bool IsWaitingForAmount(long userId, long chatId) + { + var isWaiting = waitingForAmount.TryGetValue(userId, out var waitingChatId) && waitingChatId == chatId; + + logger.LogDebug( + "Checked waiting for amount status for user {UserId} in chat {ChatId}: {IsWaiting}", userId, chatId, isWaiting); + + return isWaiting; + } + + /// + /// Removes a user from waiting for amount input state. + /// + /// The user identifier. + public virtual void RemoveWaitingForAmount(long userId) + { + var removed = waitingForAmount.Remove(userId); + + if (removed) + { + logger.LogDebug("Removed user {UserId} from waiting for amount state", userId); + } + else + { + logger.LogDebug("Attempted to remove non-existent waiting state for user {UserId}", userId); + } + } + + /// + /// Gets the number of users currently waiting for amount input. + /// + /// The count of users in waiting for amount state. + public int GetWaitingUsersCount() + { + var count = waitingForAmount.Count; + logger.LogTrace("Current users waiting for amount input: {Count}", count); + return count; + } + + /// + /// Clears all waiting states (for maintenance or reset scenarios). + /// + public void ClearAllWaitingStates() + { + var count = waitingForAmount.Count; + waitingForAmount.Clear(); + logger.LogInformation("Cleared all waiting states, affected {Count} users", count); + } + + /// + /// Removes waiting state for multiple users at once. + /// + /// The collection of user identifiers to remove. + /// The number of users successfully removed. + public int RemoveMultipleWaitingStates(IEnumerable userIds) + { + var removedCount = 0; + + foreach (var userId in userIds) + { + if (waitingForAmount.Remove(userId)) + { + removedCount++; + } + } + + logger.LogDebug("Removed waiting states for {RemovedCount} out of {TotalCount} users", removedCount, userIds.Count()); + + return removedCount; + } +} \ No newline at end of file diff --git a/Bot/UpdateHandler.cs b/Bot/UpdateHandler.cs new file mode 100755 index 0000000..2e78f0c --- /dev/null +++ b/Bot/UpdateHandler.cs @@ -0,0 +1,129 @@ +using Bot.Handlers; +using Microsoft.Extensions.Logging; +using Telegram.Bot; +using Telegram.Bot.Exceptions; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; + +namespace Bot; + +/// +/// Handles incoming updates from Telegram Bot API and routes them to appropriate handlers. +/// +public class UpdateHandler : IUpdateHandler +{ + private readonly ILogger logger; + private readonly IEnumerable handlers; + + /// + /// Initializes a new instance of the class. + /// + /// Logger instance for tracking operations. + /// Collection of update handler commands. + public UpdateHandler( + ILogger logger, + IEnumerable handlers) + { + this.logger = logger; + this.handlers = handlers; + + this.logger.LogDebug("UpdateHandler initialized with {HandlerCount} handlers", handlers.Count()); + } + + /// + /// Processes incoming updates by routing them to appropriate handlers. + /// + /// Telegram bot client instance. + /// The update to process. + /// Cancellation token for async operations. + /// A representing the asynchronous operation. + public async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken) + { + logger.LogDebug("Received update {UpdateId} of type {UpdateType}", update.Id, update.Type); + + try + { + var handler = handlers.FirstOrDefault(h => h.CanHandle(update)); + + if (handler != null) + { + logger.LogDebug("Routing update {UpdateId} to handler {HandlerType}", update.Id, handler.GetType().Name); + + await handler.HandleAsync(botClient, update, cancellationToken); + } + else + { + logger.LogWarning("No handler found for update {UpdateId} of type {UpdateType}", update.Id, update.Type); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing update {UpdateId} of type {UpdateType}", update.Id, update.Type); + } + } + + /// + /// Handles errors that occur during update processing. + /// + /// Telegram bot client instance. + /// The exception that occurred. + /// The source of the error. + /// Cancellation token for async operations. + /// A representing the asynchronous operation. + public async Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, HandleErrorSource errorSource, CancellationToken cancellationToken) + { + var errorMessage = exception switch + { + ApiRequestException apiRequestException + => $"Telegram API Error: [{apiRequestException.ErrorCode}] {apiRequestException.Message}", + RequestException requestException + => $"Request Error: {requestException.Message}", + _ => exception.Message + }; + + logger.LogError( + exception, + "Error occurred from source {ErrorSource}. Message: {ErrorMessage}", errorSource, errorMessage); + + // Implement retry delay for network-related errors + if (exception is HttpRequestException or RequestException) + { + logger.LogInformation("Network error detected, waiting 1 second before retry"); + + try + { + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + } + catch (TaskCanceledException) + { + logger.LogDebug("Retry delay was cancelled"); + } + } + + // Log specific API error codes for monitoring + if (exception is ApiRequestException apiEx) + { + LogApiErrorDetails(apiEx); + } + } + + /// + /// Logs detailed information about Telegram API errors for monitoring and debugging. + /// + /// The API request exception. + private void LogApiErrorDetails(ApiRequestException apiException) + { + var logLevel = apiException.ErrorCode switch + { + 400 => LogLevel.Warning, // Bad Request + 401 => LogLevel.Critical, // Unauthorized + 403 => LogLevel.Warning, // Forbidden + 404 => LogLevel.Information, // Not Found + 429 => LogLevel.Warning, // Too Many Requests + 500 => LogLevel.Error, // Internal Server Error + _ => LogLevel.Error + }; + + logger.Log(logLevel, apiException, "Telegram API Error {ErrorCode}: {ErrorMessage}", apiException.ErrorCode, apiException.Message); + } +} \ No newline at end of file diff --git a/Bot/appsettings.json b/Bot/appsettings.json new file mode 100755 index 0000000..987fd3a --- /dev/null +++ b/Bot/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "BotConfig": { + "BotToken": "", + "PaymentProviderToken": "" + }, + "DatabaseConfig": { + "ConnectionString": "" + } +} \ No newline at end of file diff --git a/Configurations/BotConfig.cs b/Configurations/BotConfig.cs new file mode 100755 index 0000000..4e63ea9 --- /dev/null +++ b/Configurations/BotConfig.cs @@ -0,0 +1,8 @@ +namespace Configurations; + +public class BotConfig +{ + public string BotToken { get; set; } = string.Empty; + + public string PaymentProviderToken { get; set; } = string.Empty; +} diff --git a/Configurations/Configurations.csproj b/Configurations/Configurations.csproj new file mode 100755 index 0000000..21bcc02 --- /dev/null +++ b/Configurations/Configurations.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Configurations/DatabaseConfig.cs b/Configurations/DatabaseConfig.cs new file mode 100755 index 0000000..de59865 --- /dev/null +++ b/Configurations/DatabaseConfig.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Configurations; + +public class DatabaseConfig +{ + public string ConnectionString { get; set; } = string.Empty; +} diff --git a/Data/DapperRepository.cs b/Data/DapperRepository.cs new file mode 100755 index 0000000..5651106 --- /dev/null +++ b/Data/DapperRepository.cs @@ -0,0 +1,277 @@ +using Data.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Npgsql; +using Dapper; +using Configurations; + +namespace Data; + +public class DapperRepository : IDapperRepository +{ + private readonly string connectionString; + private readonly ILogger logger; + + public DapperRepository(IOptions config, ILogger logger) + { + connectionString = config.Value.ConnectionString; + this.logger = logger; + this.logger.LogDebug("DapperRepository initialized"); + } + + public async Task GetUserByTelegramIdAsync(long telegramId) + { + logger.LogDebug("Retrieving user by Telegram ID: {TelegramId}", telegramId); + + try + { + using var connection = new NpgsqlConnection(connectionString); + var user = await connection.QueryFirstOrDefaultAsync( + "SELECT * FROM users WHERE telegram_id = @TelegramId", + new { TelegramId = telegramId }); + + logger.LogDebug("User retrieval {(Result)} for Telegram ID: {TelegramId}", + user != null ? "succeeded" : "failed - not found", telegramId); + + return user; + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving user by Telegram ID: {TelegramId}", telegramId); + throw; + } + } + + public async Task CreateUserAsync(Users user) + { + logger.LogDebug("Creating user with Telegram ID: {TelegramId}", user.TelegramId); + + try + { + using var connection = new NpgsqlConnection(connectionString); + var sql = @" + INSERT INTO users (telegram_id, username, first_name, last_name) + VALUES (@TelegramId, @Username, @FirstName, @LastName) + RETURNING *"; + + var createdUser = await connection.QueryFirstAsync(sql, user); + logger.LogInformation("User created successfully with ID: {UserId} for Telegram ID: {TelegramId}", + createdUser.Id, user.TelegramId); + + return createdUser; + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating user with Telegram ID: {TelegramId}", user.TelegramId); + throw; + } + } + + public async Task GetActiveGoalAsync() + { + logger.LogDebug("Retrieving active donation goal"); + + try + { + using var connection = new NpgsqlConnection(connectionString); + var sql = @" + SELECT + id AS Id, + title AS Title, + description AS Description, + target_amount AS TargetAmount, + current_amount AS CurrentAmount, + is_active AS IsActive, + created_at AS CreatedAt + FROM donation_goals + WHERE is_active = true + ORDER BY created_at DESC + LIMIT 1"; + + var goal = await connection.QueryFirstOrDefaultAsync(sql); + logger.LogDebug("Active goal retrieval {(Result)}", + goal != null ? $"succeeded - Goal ID: {goal.Id}" : "failed - no active goal"); + + return goal; + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving active donation goal"); + throw; + } + } + + public async Task GetCountDonationsForActiveGoals() + { + logger.LogDebug("Retrieving count of donations for active goals"); + + try + { + using var connection = new NpgsqlConnection(connectionString); + var sql = @" + SELECT COUNT(*) + FROM donations d + INNER JOIN donation_goals g ON d.goal_id = g.id + WHERE g.is_active = true"; + + var count = await connection.QueryFirstOrDefaultAsync(sql); + logger.LogDebug("Retrieved {DonationCount} donations for active goals", count); + + return count; + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving count of donations for active goals"); + throw; + } + } + + public async Task GetCountUsersForActiveGoals() + { + logger.LogDebug("Retrieving count of unique users for active goals"); + + try + { + using var connection = new NpgsqlConnection(connectionString); + var sql = @" + SELECT COUNT(DISTINCT d.user_telegram_id) + FROM donations d + INNER JOIN donation_goals g ON d.goal_id = g.id + WHERE g.is_active = true"; + + var count = await connection.QueryFirstOrDefaultAsync(sql); + logger.LogDebug("Retrieved {UserCount} unique users for active goals", count); + + return count; + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving count of unique users for active goals"); + throw; + } + } + + public async Task CreateGoalAsync(DonationGoal goal) + { + logger.LogInformation("Creating new donation goal: {GoalTitle}", goal.Title); + + try + { + using var connection = new NpgsqlConnection(connectionString); + var sql = @" + UPDATE donation_goals SET is_active = false WHERE is_active = true; + + INSERT INTO donation_goals (title, description, target_amount) + VALUES (@Title, @Description, @TargetAmount) + RETURNING *"; + + var createdGoal = await connection.QueryFirstAsync(sql, goal); + logger.LogInformation("Goal created successfully with ID: {GoalId}, Target: {TargetAmount}", + createdGoal.Id, createdGoal.TargetAmount); + + return createdGoal; + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating donation goal: {GoalTitle}", goal.Title); + throw; + } + } + + public async Task UpdateGoalCurrentAmountAsync(int goalId, decimal amountToAdd) + { + logger.LogDebug("Updating goal {GoalId} current amount by {AmountToAdd}", goalId, amountToAdd); + + try + { + using var connection = new NpgsqlConnection(connectionString); + var sql = @" + UPDATE donation_goals + SET current_amount = current_amount + @AmountToAdd + WHERE id = @GoalId"; + + var affectedRows = await connection.ExecuteAsync(sql, new { GoalId = goalId, AmountToAdd = amountToAdd }); + + if (affectedRows == 0) + { + logger.LogWarning("No rows affected when updating goal {GoalId} amount", goalId); + } + else + { + logger.LogDebug("Successfully updated goal {GoalId} amount by {AmountToAdd}", goalId, amountToAdd); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error updating goal {GoalId} current amount by {AmountToAdd}", goalId, amountToAdd); + throw; + } + } + + public async Task CreateDonationAsync(Donation donation) + { + logger.LogDebug("Creating donation for user {UserTelegramId} with amount {Amount} {Currency}", + donation.UserTelegramId, donation.Amount, donation.Currency); + + try + { + using var connection = new NpgsqlConnection(connectionString); + var sql = @" + INSERT INTO donations (user_telegram_id, goal_id, amount, currency, provider_payment_id, status) + VALUES (@UserTelegramId, @GoalId, @Amount, @Currency, @ProviderPaymentId, @Status) + RETURNING + id AS Id, + user_telegram_id AS UserTelegramId, + goal_id AS GoalId, + amount AS Amount, + currency AS Currency, + status AS Status, + provider_payment_id AS ProviderPaymentId, + created_at AS CreatedAt"; + + var createdDonation = await connection.QueryFirstAsync(sql, donation); + logger.LogInformation("Donation created successfully with ID: {DonationId} for user {UserTelegramId}", + createdDonation.Id, donation.UserTelegramId); + + return createdDonation; + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating donation for user {UserTelegramId}", donation.UserTelegramId); + throw; + } + } + + public async Task GetDonationAsync(string donationId) + { + logger.LogDebug("Retrieving donation by ID: {DonationId}", donationId); + + try + { + using var connection = new NpgsqlConnection(connectionString); + var sql = @" + SELECT + id AS Id, + user_telegram_id AS UserTelegramId, + goal_id AS GoalId, + amount AS Amount, + currency AS Currency, + provider_payment_id AS ProviderPaymentId, + status AS Status, + created_at AS CreatedAt + FROM donations + WHERE provider_payment_id = @DonationId"; + + var donation = await connection.QueryFirstOrDefaultAsync(sql, new { DonationId = donationId }); + logger.LogDebug("Donation retrieval {(Result)} for ID: {DonationId}", + donation != null ? "succeeded" : "failed - not found", donationId); + + return donation; + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving donation by ID: {DonationId}", donationId); + throw; + } + } +} \ No newline at end of file diff --git a/Data/Data.csproj b/Data/Data.csproj new file mode 100755 index 0000000..a7b4289 --- /dev/null +++ b/Data/Data.csproj @@ -0,0 +1,23 @@ + + + 10.0 + + + net9.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + \ No newline at end of file diff --git a/Data/IDapperRepository.cs b/Data/IDapperRepository.cs new file mode 100755 index 0000000..d4c3ad6 --- /dev/null +++ b/Data/IDapperRepository.cs @@ -0,0 +1,70 @@ +using Data.Models; + +namespace Data; + +/// +/// Repository interface for data access operations using Dapper ORM. +/// +public interface IDapperRepository +{ + /// + /// Retrieves a user by their Telegram identifier. + /// + /// The Telegram user identifier. + /// The user if found; otherwise, null. + Task GetUserByTelegramIdAsync(long telegramId); + + /// + /// Creates a new user in the database. + /// + /// The user entity to create. + /// The created user with generated identifier. + Task CreateUserAsync(Users user); + + /// + /// Retrieves the currently active donation goal. + /// + /// The active donation goal if found; otherwise, null. + Task GetActiveGoalAsync(); + + /// + /// Gets the count of unique users who donated to active goals. + /// + /// The number of unique donors for active goals. + Task GetCountUsersForActiveGoals(); + + /// + /// Gets the total count of donations made to active goals. + /// + /// The total number of donations for active goals. + Task GetCountDonationsForActiveGoals(); + + /// + /// Creates a new donation goal in the database. + /// + /// The donation goal entity to create. + /// The created donation goal with generated identifier. + Task CreateGoalAsync(DonationGoal goal); + + /// + /// Updates the current collected amount for a specific goal. + /// + /// The identifier of the goal to update. + /// The new current amount value. + /// A task representing the asynchronous operation. + Task UpdateGoalCurrentAmountAsync(int goalId, decimal newAmount); + + /// + /// Creates a new donation record in the database. + /// + /// The donation entity to create. + /// The created donation with generated identifier. + Task CreateDonationAsync(Donation donation); + + /// + /// Retrieves a donation by its unique identifier. + /// + /// The donation identifier. + /// The donation if found; otherwise, null. + Task GetDonationAsync(string donationId); +} \ No newline at end of file diff --git a/Data/Models/Donation.cs b/Data/Models/Donation.cs new file mode 100755 index 0000000..a97671d --- /dev/null +++ b/Data/Models/Donation.cs @@ -0,0 +1,20 @@ +namespace Data.Models; + +public class Donation +{ + public int Id { get; set; } + + public long UserTelegramId { get; set; } + + public int GoalId { get; set; } + + public decimal Amount { get; set; } + + public string Currency { get; set; } = "RUB"; + + public string? ProviderPaymentId { get; set; } + + public string Status { get; set; } = "pending"; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/Data/Models/DonationGoal.cs b/Data/Models/DonationGoal.cs new file mode 100755 index 0000000..5a15431 --- /dev/null +++ b/Data/Models/DonationGoal.cs @@ -0,0 +1,18 @@ +namespace Data.Models; + +public class DonationGoal +{ + public int Id { get; set; } + + public string Title { get; set; } = string.Empty; + + public string? Description { get; set; } + + public decimal TargetAmount { get; set; } + + public decimal CurrentAmount { get; set; } + + public bool IsActive { get; set; } = true; + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/Data/Models/Users.cs b/Data/Models/Users.cs new file mode 100755 index 0000000..588b4f2 --- /dev/null +++ b/Data/Models/Users.cs @@ -0,0 +1,18 @@ +namespace Data.Models; + +public class Users +{ + public int Id { get; set; } + + public bool Admin { get; set; } + + public long TelegramId { get; set; } + + public string? Username { get; set; } + + public string? FirstName { get; set; } + + public string? LastName { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/Data/Scripts/create_tables.sql b/Data/Scripts/create_tables.sql new file mode 100755 index 0000000..1816359 --- /dev/null +++ b/Data/Scripts/create_tables.sql @@ -0,0 +1,49 @@ +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + telegram_id BIGINT NOT NULL UNIQUE, + username VARCHAR(100), + first_name VARCHAR(100), + last_name VARCHAR(100), + admin BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS donation_goals ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + target_amount DECIMAL(10, 2) NOT NULL, + current_amount DECIMAL(10, 2) DEFAULT 0, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS donations ( + id SERIAL PRIMARY KEY, + user_telegram_id BIGINT NOT NULL, + goal_id INTEGER NOT NULL, + amount DECIMAL(10, 2) NOT NULL, + currency VARCHAR(3) DEFAULT 'RUB', + provider_payment_id VARCHAR(255) NOT NULL, + status VARCHAR(50) DEFAULT 'pending', + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + -- Ограничения + CONSTRAINT chk_donation_amount_positive CHECK (amount > 0), + CONSTRAINT uq_provider_payment_id UNIQUE (provider_payment_id), + + -- Внешние ключи + CONSTRAINT fk_donations_user_id + FOREIGN KEY (user_telegram_id) + REFERENCES users(telegram_id) ON DELETE RESTRICT, + + CONSTRAINT fk_donations_goal_id + FOREIGN KEY (goal_id) + REFERENCES donation_goals(id) ON DELETE RESTRICT +); + +-- Индексы +CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id); +CREATE INDEX IF NOT EXISTS idx_donation_goals_active ON donation_goals(is_active) WHERE is_active = true; +CREATE INDEX IF NOT EXISTS idx_donations_user_telegram_id ON donations(user_telegram_id); +CREATE INDEX IF NOT EXISTS idx_donations_goal_id ON donations(goal_id); \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..23e7710 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Копируем файлы проектов +COPY ["Bot/Bot.csproj", "Bot/"] +COPY ["Data/Data.csproj", "Data/"] +COPY ["Services/Services.csproj", "Services/"] +COPY ["Configurations/Configurations.csproj", "Configurations/"] +RUN dotnet restore "Bot/Bot.csproj" + +# Копируем весь код и публикуем +COPY . . +WORKDIR "/src/Bot" +RUN dotnet publish "Bot.csproj" -c Release -o /app/publish + +# Финальный образ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 +WORKDIR /app +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "Bot.dll"] \ No newline at end of file diff --git a/DonationBot.sln b/DonationBot.sln new file mode 100755 index 0000000..b8f5dad --- /dev/null +++ b/DonationBot.sln @@ -0,0 +1,103 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data", "Data\Data.csproj", "{5562041A-E13F-4D1F-8D4A-F275232C57CB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services", "Services\Services.csproj", "{62104FDE-4949-4FB5-B509-541CF8078D02}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Configurations", "Configurations\Configurations.csproj", "{499B7406-86EF-4C81-995B-1ABAD3CD7BA1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bot", "Bot\Bot.csproj", "{BCABA8EA-B3EF-4B25-9481-9CAA38D95379}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Элементы решения", "Элементы решения", "{754FC069-D67B-A9D7-50A1-8D1CA196D8F1}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestBot", "TestBot\TestBot.csproj", "{164F484F-C030-47B5-A8FC-E3FEA0E20318}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36D591C7-65C7-A0D1-1CBC-10CDE441BDC8}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5562041A-E13F-4D1F-8D4A-F275232C57CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5562041A-E13F-4D1F-8D4A-F275232C57CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5562041A-E13F-4D1F-8D4A-F275232C57CB}.Debug|x64.ActiveCfg = Debug|Any CPU + {5562041A-E13F-4D1F-8D4A-F275232C57CB}.Debug|x64.Build.0 = Debug|Any CPU + {5562041A-E13F-4D1F-8D4A-F275232C57CB}.Debug|x86.ActiveCfg = Debug|Any CPU + {5562041A-E13F-4D1F-8D4A-F275232C57CB}.Debug|x86.Build.0 = Debug|Any CPU + {5562041A-E13F-4D1F-8D4A-F275232C57CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5562041A-E13F-4D1F-8D4A-F275232C57CB}.Release|Any CPU.Build.0 = Release|Any CPU + {5562041A-E13F-4D1F-8D4A-F275232C57CB}.Release|x64.ActiveCfg = Release|Any CPU + {5562041A-E13F-4D1F-8D4A-F275232C57CB}.Release|x64.Build.0 = Release|Any CPU + {5562041A-E13F-4D1F-8D4A-F275232C57CB}.Release|x86.ActiveCfg = Release|Any CPU + {5562041A-E13F-4D1F-8D4A-F275232C57CB}.Release|x86.Build.0 = Release|Any CPU + {62104FDE-4949-4FB5-B509-541CF8078D02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62104FDE-4949-4FB5-B509-541CF8078D02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62104FDE-4949-4FB5-B509-541CF8078D02}.Debug|x64.ActiveCfg = Debug|Any CPU + {62104FDE-4949-4FB5-B509-541CF8078D02}.Debug|x64.Build.0 = Debug|Any CPU + {62104FDE-4949-4FB5-B509-541CF8078D02}.Debug|x86.ActiveCfg = Debug|Any CPU + {62104FDE-4949-4FB5-B509-541CF8078D02}.Debug|x86.Build.0 = Debug|Any CPU + {62104FDE-4949-4FB5-B509-541CF8078D02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62104FDE-4949-4FB5-B509-541CF8078D02}.Release|Any CPU.Build.0 = Release|Any CPU + {62104FDE-4949-4FB5-B509-541CF8078D02}.Release|x64.ActiveCfg = Release|Any CPU + {62104FDE-4949-4FB5-B509-541CF8078D02}.Release|x64.Build.0 = Release|Any CPU + {62104FDE-4949-4FB5-B509-541CF8078D02}.Release|x86.ActiveCfg = Release|Any CPU + {62104FDE-4949-4FB5-B509-541CF8078D02}.Release|x86.Build.0 = Release|Any CPU + {499B7406-86EF-4C81-995B-1ABAD3CD7BA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {499B7406-86EF-4C81-995B-1ABAD3CD7BA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {499B7406-86EF-4C81-995B-1ABAD3CD7BA1}.Debug|x64.ActiveCfg = Debug|Any CPU + {499B7406-86EF-4C81-995B-1ABAD3CD7BA1}.Debug|x64.Build.0 = Debug|Any CPU + {499B7406-86EF-4C81-995B-1ABAD3CD7BA1}.Debug|x86.ActiveCfg = Debug|Any CPU + {499B7406-86EF-4C81-995B-1ABAD3CD7BA1}.Debug|x86.Build.0 = Debug|Any CPU + {499B7406-86EF-4C81-995B-1ABAD3CD7BA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {499B7406-86EF-4C81-995B-1ABAD3CD7BA1}.Release|Any CPU.Build.0 = Release|Any CPU + {499B7406-86EF-4C81-995B-1ABAD3CD7BA1}.Release|x64.ActiveCfg = Release|Any CPU + {499B7406-86EF-4C81-995B-1ABAD3CD7BA1}.Release|x64.Build.0 = Release|Any CPU + {499B7406-86EF-4C81-995B-1ABAD3CD7BA1}.Release|x86.ActiveCfg = Release|Any CPU + {499B7406-86EF-4C81-995B-1ABAD3CD7BA1}.Release|x86.Build.0 = Release|Any CPU + {BCABA8EA-B3EF-4B25-9481-9CAA38D95379}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BCABA8EA-B3EF-4B25-9481-9CAA38D95379}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BCABA8EA-B3EF-4B25-9481-9CAA38D95379}.Debug|x64.ActiveCfg = Debug|Any CPU + {BCABA8EA-B3EF-4B25-9481-9CAA38D95379}.Debug|x64.Build.0 = Debug|Any CPU + {BCABA8EA-B3EF-4B25-9481-9CAA38D95379}.Debug|x86.ActiveCfg = Debug|Any CPU + {BCABA8EA-B3EF-4B25-9481-9CAA38D95379}.Debug|x86.Build.0 = Debug|Any CPU + {BCABA8EA-B3EF-4B25-9481-9CAA38D95379}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BCABA8EA-B3EF-4B25-9481-9CAA38D95379}.Release|Any CPU.Build.0 = Release|Any CPU + {BCABA8EA-B3EF-4B25-9481-9CAA38D95379}.Release|x64.ActiveCfg = Release|Any CPU + {BCABA8EA-B3EF-4B25-9481-9CAA38D95379}.Release|x64.Build.0 = Release|Any CPU + {BCABA8EA-B3EF-4B25-9481-9CAA38D95379}.Release|x86.ActiveCfg = Release|Any CPU + {BCABA8EA-B3EF-4B25-9481-9CAA38D95379}.Release|x86.Build.0 = Release|Any CPU + {164F484F-C030-47B5-A8FC-E3FEA0E20318}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {164F484F-C030-47B5-A8FC-E3FEA0E20318}.Debug|Any CPU.Build.0 = Debug|Any CPU + {164F484F-C030-47B5-A8FC-E3FEA0E20318}.Debug|x64.ActiveCfg = Debug|Any CPU + {164F484F-C030-47B5-A8FC-E3FEA0E20318}.Debug|x64.Build.0 = Debug|Any CPU + {164F484F-C030-47B5-A8FC-E3FEA0E20318}.Debug|x86.ActiveCfg = Debug|Any CPU + {164F484F-C030-47B5-A8FC-E3FEA0E20318}.Debug|x86.Build.0 = Debug|Any CPU + {164F484F-C030-47B5-A8FC-E3FEA0E20318}.Release|Any CPU.ActiveCfg = Release|Any CPU + {164F484F-C030-47B5-A8FC-E3FEA0E20318}.Release|Any CPU.Build.0 = Release|Any CPU + {164F484F-C030-47B5-A8FC-E3FEA0E20318}.Release|x64.ActiveCfg = Release|Any CPU + {164F484F-C030-47B5-A8FC-E3FEA0E20318}.Release|x64.Build.0 = Release|Any CPU + {164F484F-C030-47B5-A8FC-E3FEA0E20318}.Release|x86.ActiveCfg = Release|Any CPU + {164F484F-C030-47B5-A8FC-E3FEA0E20318}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {021B4F17-39EA-4135-981B-3A3D4D1EA000} + EndGlobalSection +EndGlobal diff --git a/Services/DonationService.cs b/Services/DonationService.cs new file mode 100755 index 0000000..749dbbd --- /dev/null +++ b/Services/DonationService.cs @@ -0,0 +1,112 @@ +using Data; +using Data.Models; +using Microsoft.Extensions.Logging; + +namespace Services; + +public class DonationService : IDonationService +{ + private readonly IDapperRepository repository; + private readonly ILogger logger; + + public DonationService(IDapperRepository repository, ILogger logger) + { + this.repository = repository; + this.logger = logger; + this.logger.LogDebug("DonationService initialized"); + } + + public async Task GetOrCreateUserAsync(long telegramId, string? username, string? firstName, string? lastName) + { + logger.LogDebug("Getting or creating user with Telegram ID: {TelegramId}", telegramId); + + try + { + var user = await repository.GetUserByTelegramIdAsync(telegramId); + + if (user != null) + { + logger.LogDebug("Found existing user {UserId} for Telegram ID: {TelegramId}", user.Id, telegramId); + return user; + } + + logger.LogInformation("Creating new user for Telegram ID: {TelegramId}, username: {Username}", + telegramId, username); + + var newUser = new Users + { + TelegramId = telegramId, + Username = username, + FirstName = firstName, + LastName = lastName, + Admin = false, + }; + + var createdUser = await repository.CreateUserAsync(newUser); + logger.LogInformation("Created new user {UserId} for Telegram ID: {TelegramId}", + createdUser.Id, telegramId); + + return createdUser; + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting or creating user with Telegram ID: {TelegramId}", telegramId); + throw; + } + } + + public async Task ProcessDonationAsync(long userTelegramId, decimal amount, string currency, string donationId) + { + logger.LogInformation("Processing donation {DonationId} for user {UserTelegramId}, amount: {Amount} {Currency}", + donationId, userTelegramId, amount, currency); + + try + { + var existingDonation = await repository.GetDonationAsync(donationId); + if (existingDonation != null) + { + logger.LogWarning("Donation {DonationId} has already been processed on {ProcessedDate}", + donationId, existingDonation.CreatedAt); + return true; + } + + var goal = await repository.GetActiveGoalAsync(); + if (goal == null) + { + logger.LogError("No active goal found for donation {DonationId}", donationId); + return false; + } + + logger.LogDebug("Found active goal {GoalId} for donation {DonationId}", goal.Id, donationId); + + var donation = new Donation + { + UserTelegramId = userTelegramId, + GoalId = goal.Id, + Amount = amount, + Currency = currency, + ProviderPaymentId = donationId, + Status = "completed", + }; + + var createdDonation = await repository.CreateDonationAsync(donation); + logger.LogDebug("Created donation record {DonationRecordId} for payment {DonationId}", + createdDonation.Id, donationId); + + await repository.UpdateGoalCurrentAmountAsync(goal.Id, amount); + + logger.LogInformation( + "Successfully processed donation {DonationId} for user {UserTelegramId}. " + + "Amount: {Amount} {Currency} added to goal {GoalId}", + donationId, userTelegramId, amount, currency, goal.Id); + + return true; + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing donation {DonationId} for user {UserTelegramId}, amount: {Amount} {Currency}", + donationId, userTelegramId, amount, currency); + return false; + } + } +} \ No newline at end of file diff --git a/Services/GoalService.cs b/Services/GoalService.cs new file mode 100755 index 0000000..54ad7a9 --- /dev/null +++ b/Services/GoalService.cs @@ -0,0 +1,224 @@ +using Data; +using Data.Models; +using Microsoft.Extensions.Logging; + +namespace Services; + +/// +/// Service implementation for handling goal-related operations including statistics and administrative functions. +/// +public class GoalService : IGoalService +{ + private readonly IDapperRepository repository; + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The repository for data access operations. + /// Logger instance for tracking operations. + public GoalService(IDapperRepository repository, ILogger logger) + { + this.repository = repository; + this.logger = logger; + + this.logger.LogDebug("GoalService initialized"); + } + + /// + public async Task IsUserAdminAsync(long telegramId) + { + logger.LogDebug("Checking admin status for user {TelegramId}", telegramId); + + try + { + var user = await repository.GetUserByTelegramIdAsync(telegramId); + var isAdmin = user?.Admin ?? false; + + logger.LogDebug("User {TelegramId} admin status: {IsAdmin}", telegramId, isAdmin); + + return isAdmin; + } + catch (Exception ex) + { + logger.LogError(ex, "Error checking admin status for user {TelegramId}", telegramId); + return false; + } + } + + /// + public async Task GetActiveGoalAsync() + { + logger.LogDebug("Retrieving active goal"); + + try + { + var goal = await repository.GetActiveGoalAsync(); + + logger.LogDebug("Active goal retrieval {(Result)}", goal != null ? $"succeeded - Goal ID: {goal.Id}" : "failed - no active goal"); + + return goal; + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving active goal"); + return null; + } + } + + private async Task GetActiveUsersGoalAsync() + { + logger.LogDebug("Retrieving active users count for goal"); + + try + { + var count = await repository.GetCountUsersForActiveGoals(); + + logger.LogDebug("Retrieved {UserCount} active users for goal", count); + + return count; + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving active users count for goal"); + return 0; + } + } + + private async Task GetActiveDonationsGoalAsync() + { + logger.LogDebug("Retrieving active donations count for goal"); + + try + { + var count = await repository.GetCountDonationsForActiveGoals(); + + logger.LogDebug("Retrieved {DonationCount} active donations for goal", count); + + return count; + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving active donations count for goal"); + return 0; + } + } + + /// + public async Task GetGoalStatsAsync() + { + logger.LogDebug("Generating goal statistics"); + + try + { + var goal = await GetActiveGoalAsync(); + var countUsers = await GetActiveUsersGoalAsync(); + var countDonations = await GetActiveDonationsGoalAsync(); + + if (goal == null) + { + logger.LogInformation("No active goal found for statistics"); + return "🎯 На данный момент нет активных целей для сбора."; + } + + var percent = goal.TargetAmount > 0 + ? ((double)goal.CurrentAmount / (double)goal.TargetAmount) * 100 + : 0; + + var progressBar = CreateProgressBar(percent); + + var stats = $"🎯 **{goal.Title}** — {goal.TargetAmount:N0}₽ \n📝 Описание: {goal.Description} \n\n📈 Количество пожертвований на текущую цель: {countDonations}" + + $"\n🧮 Количество пожертвовавших: {countUsers} \n⏳ Дата открытия сбора: {goal.CreatedAt:dd.MM.yyyy}\n\nСобрано: {goal.CurrentAmount:N0}₽ ({percent:F1}%) \n{progressBar}"; + + logger.LogDebug("Goal statistics generated successfully for goal {GoalId}", goal.Id); + + return stats; + } + catch (Exception ex) + { + logger.LogError(ex, "Error generating goal statistics"); + return "❌ Произошла ошибка при получении статистики."; + } + } + + /// + public async Task GetStartStats() + { + logger.LogDebug("Generating start statistics"); + + try + { + var goal = await GetActiveGoalAsync(); + + if (goal == null) + { + logger.LogInformation("No active goal found for start statistics"); + return "🎯 На данный момент нет активных целей для сбора."; + } + + var percent = goal.TargetAmount > 0 + ? ((double)goal.CurrentAmount / (double)goal.TargetAmount) * 100 + : 0; + + var progressBar = CreateProgressBar(percent); + + var stats = $"🎯 **{goal.Title}** — {goal.TargetAmount:N0}₽ \n📝 Описание: {goal.Description} \n\nСобрано: {goal.CurrentAmount:N0}₽ ({percent:F1}%) \n{progressBar}"; + + logger.LogDebug("Start statistics generated successfully for goal {GoalId}", goal.Id); + + return stats; + } + catch (Exception ex) + { + logger.LogError(ex, "Error generating start statistics"); + return "❌ Произошла ошибка при получении статистики."; + } + } + + /// + /// Creates a visual progress bar representation. + /// + /// The completion percentage. + /// A string representing the progress bar. + private string CreateProgressBar(double percent) + { + if (percent >= 100) + { + return $"[{new string('■', 10)}]"; + } + else + { + var filled = (int)Math.Round(percent / 10); + var empty = 10 - filled; + return $"[{new string('■', filled)}{new string('□', empty)}]"; + } + } + + /// + public async Task CreateGoalAsync(string title, string description, decimal targetAmount) + { + logger.LogInformation("Creating new goal: {Title}, Target: {TargetAmount}", title, targetAmount); + + try + { + var newGoal = new DonationGoal + { + Title = title, + Description = description, + TargetAmount = targetAmount, + IsActive = true, + }; + + var createdGoal = await repository.CreateGoalAsync(newGoal); + + logger.LogInformation("Goal created successfully: {GoalId}, {Title}, Target: {TargetAmount}", createdGoal.Id, title, targetAmount); + + return createdGoal; + } + catch (Exception ex) + { + logger.LogError(ex, "Error creating goal: {Title}", title); + throw; + } + } +} \ No newline at end of file diff --git a/Services/IDonationService.cs b/Services/IDonationService.cs new file mode 100755 index 0000000..66082fb --- /dev/null +++ b/Services/IDonationService.cs @@ -0,0 +1,29 @@ +using Data.Models; + +namespace Services; + +/// +/// Service for handling donation-related operations and user management. +/// +public interface IDonationService +{ + /// + /// Retrieves an existing user or creates a new one if not found. + /// + /// The Telegram user identifier. + /// The Telegram username (optional). + /// The user's first name (optional). + /// The user's last name (optional). + /// The existing or newly created user. + Task GetOrCreateUserAsync(long telegramId, string? username, string? firstName, string? lastName); + + /// + /// Processes a donation transaction and updates relevant records. + /// + /// The identifier of the user making the donation. + /// The donation amount. + /// The currency of the donation. + /// The unique payment provider identifier for the donation. + /// True if the donation was processed successfully; otherwise, false. + Task ProcessDonationAsync(long userId, decimal amount, string currency, string donationId); +} \ No newline at end of file diff --git a/Services/IGoalService.cs b/Services/IGoalService.cs new file mode 100755 index 0000000..fe2b729 --- /dev/null +++ b/Services/IGoalService.cs @@ -0,0 +1,43 @@ +using Data.Models; + +namespace Services; + +/// +/// Service for handling goal-related operations including statistics and administrative functions. +/// +public interface IGoalService +{ + /// + /// Checks if a user has administrator privileges. + /// + /// The Telegram user identifier. + /// True if the user is an administrator; otherwise, false. + Task IsUserAdminAsync(long telegramId); + + /// + /// Retrieves the currently active donation goal. + /// + /// The active donation goal if found; otherwise, null. + Task GetActiveGoalAsync(); + + /// + /// Gets startup statistics for display in the welcome message. + /// + /// Formatted statistics string for the start command. + Task GetStartStats(); + + /// + /// Gets detailed statistics for the current goal. + /// + /// Formatted statistics string for the stats command. + Task GetGoalStatsAsync(); + + /// + /// Creates a new donation goal with the specified parameters. + /// + /// The goal title. + /// The goal description. + /// The target amount for the goal. + /// The created donation goal. + Task CreateGoalAsync(string title, string description, decimal targetAmount); +} \ No newline at end of file diff --git a/Services/Services.csproj b/Services/Services.csproj new file mode 100755 index 0000000..8f42233 --- /dev/null +++ b/Services/Services.csproj @@ -0,0 +1,21 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + net9.0 + enable + enable + + + diff --git a/TestBot/Bot/Bot.Hadler/AdminHandlerTests.cs b/TestBot/Bot/Bot.Hadler/AdminHandlerTests.cs new file mode 100755 index 0000000..6496ca0 --- /dev/null +++ b/TestBot/Bot/Bot.Hadler/AdminHandlerTests.cs @@ -0,0 +1,433 @@ +using Bot.Handlers; +using Bot.Services; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Services; +using Telegram.Bot; +using Telegram.Bot.Types; +using static Bot.Services.AdminStateService; + +namespace Bot.Tests.Handlers; + +[TestFixture] +public class AdminHandlerTests +{ + private Mock> _loggerMock; + private Mock _goalServiceMock; + private Mock _adminStateServiceMock; + private Mock _botClientMock; + private AdminHandler _handler; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock>(); + _goalServiceMock = new Mock(); + + // Создаем мок для AdminStateService с правильным конструктором + var adminStateServiceLoggerMock = new Mock>(); + _adminStateServiceMock = new Mock(adminStateServiceLoggerMock.Object); + + _botClientMock = new Mock(); + + _handler = new AdminHandler( + _loggerMock.Object, + _goalServiceMock.Object, + _adminStateServiceMock.Object); + } + + [Test] + public async Task HandleAdminGoalCreationAsync_NullUser_LogsWarningAndReturns() + { + // Arrange + var message = new Message { From = null, Chat = new Chat { Id = 123 }, Text = "Test" }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Received message with null user ID")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleAdminGoalCreationAsync_NullState_LogsWarningAndReturns() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "Test" }; + var cancellationToken = new CancellationToken(); + + _adminStateServiceMock + .Setup(x => x.GetState(123)) + .Returns((AdminGoalCreationState)null); + + // Act + await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("No admin state found for user")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleAdminGoalCreationAsync_EmptyMessageText_LogsWarningAndReturns() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = null }; + var state = new AdminGoalCreationState { CurrentStep = AdminGoalStep.WaitingForTitle }; + var cancellationToken = new CancellationToken(); + + _adminStateServiceMock + .Setup(x => x.GetState(123)) + .Returns(state); + + // Act + await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Received empty message text")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleAdminGoalCreationAsync_WaitingForTitle_ValidTitle_ProcessesTitleStep() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "Новая цель" }; + var state = new AdminGoalCreationState { CurrentStep = AdminGoalStep.WaitingForTitle }; + var cancellationToken = new CancellationToken(); + + _adminStateServiceMock + .Setup(x => x.GetState(123)) + .Returns(state); + + // Act + await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _adminStateServiceMock.Verify( + x => x.SetTitle(123, "Новая цель"), + Times.Once); + } + + [Test] + public async Task HandleAdminGoalCreationAsync_WaitingForTitle_TooLongTitle_CancelsCreation() + { + // Arrange + var user = new User { Id = 123 }; + var longTitle = new string('a', 256); // 256 символов > 255 + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = longTitle }; + var state = new AdminGoalCreationState { CurrentStep = AdminGoalStep.WaitingForTitle }; + var cancellationToken = new CancellationToken(); + + _adminStateServiceMock + .Setup(x => x.GetState(123)) + .Returns(state); + + // Act + await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _adminStateServiceMock.Verify( + x => x.CancelGoalCreation(123), + Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("provided title that exceeds length limit")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleAdminGoalCreationAsync_WaitingForDescription_ProcessesDescriptionStep() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "Описание цели" }; + var state = new AdminGoalCreationState { CurrentStep = AdminGoalStep.WaitingForDescription }; + var cancellationToken = new CancellationToken(); + + _adminStateServiceMock + .Setup(x => x.GetState(123)) + .Returns(state); + + // Act + await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _adminStateServiceMock.Verify( + x => x.SetDescription(123, "Описание цели"), + Times.Once); + } + + [Test] + public async Task HandleAdminGoalCreationAsync_WaitingForAmount_InvalidAmount_CancelsCreation() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "invalid_amount" }; + var state = new AdminGoalCreationState + { + CurrentStep = AdminGoalStep.WaitingForAmount, + Title = "Test Goal", + Description = "Test Description" + }; + var cancellationToken = new CancellationToken(); + + _adminStateServiceMock + .Setup(x => x.GetState(123)) + .Returns(state); + + // Act + await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _adminStateServiceMock.Verify( + x => x.CancelGoalCreation(123), + Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("provided invalid amount")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleAdminGoalCreationAsync_WaitingForAmount_AmountTooLarge_CancelsCreation() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "100000000" }; // 100 миллионов + var state = new AdminGoalCreationState + { + CurrentStep = AdminGoalStep.WaitingForAmount, + Title = "Test Goal", + Description = "Test Description" + }; + var cancellationToken = new CancellationToken(); + + _adminStateServiceMock + .Setup(x => x.GetState(123)) + .Returns(state); + + // Act + await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _adminStateServiceMock.Verify( + x => x.CancelGoalCreation(123), + Times.Once); + } + + [Test] + public async Task HandleAdminGoalCreationAsync_WaitingForAmount_ValidAmount_CreatesGoal() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "5000" }; + var state = new AdminGoalCreationState + { + CurrentStep = AdminGoalStep.WaitingForAmount, + Title = "Test Goal", + Description = "Test Description" + }; + var cancellationToken = new CancellationToken(); + + var createdGoal = new Data.Models.DonationGoal { Id = 1, Title = "Test Goal", Description = "Test Description", TargetAmount = 5000 }; + _adminStateServiceMock + .Setup(x => x.GetState(123)) + .Returns(state); + + _goalServiceMock + .Setup(x => x.CreateGoalAsync("Test Goal", "Test Description", 5000)) + .ReturnsAsync(createdGoal); + + // Act + await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _goalServiceMock.Verify( + x => x.CreateGoalAsync("Test Goal", "Test Description", 5000), + Times.Once); + + _adminStateServiceMock.Verify( + x => x.CancelGoalCreation(123), + Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Goal created successfully")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleAdminGoalCreationAsync_WaitingForAmount_GoalServiceThrows_LogsError() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "5000" }; + var state = new AdminGoalCreationState + { + CurrentStep = AdminGoalStep.WaitingForAmount, + Title = "Test Goal", + Description = "Test Description" + }; + var cancellationToken = new CancellationToken(); + + _adminStateServiceMock + .Setup(x => x.GetState(123)) + .Returns(state); + + _goalServiceMock + .Setup(x => x.CreateGoalAsync("Test Goal", "Test Description", 5000)) + .ThrowsAsync(new Exception("Database error")); + + // Act + await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Failed to create goal in database")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task StartGoalCreationAsync_SetsStateAndLogs() + { + // Arrange + var chatId = 456L; + var userId = 123L; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.StartGoalCreationAsync(_botClientMock.Object, chatId, userId, cancellationToken); + + // Assert + _adminStateServiceMock.Verify( + x => x.StartGoalCreation(123, 456), + Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Starting goal creation process")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task StartGoalCreationAsync_ThrowsException_LogsError() + { + // Arrange + var chatId = 456L; + var userId = 123L; + var cancellationToken = new CancellationToken(); + + _adminStateServiceMock + .Setup(x => x.StartGoalCreation(123, 456)) + .Throws(new Exception("State service error")); + + // Act & Assert + Assert.ThrowsAsync(() => + _handler.StartGoalCreationAsync(_botClientMock.Object, chatId, userId, cancellationToken)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Failed to start goal creation")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleNotAdmin_LogsWarning() + { + // Arrange + var chatId = 456L; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleNotAdmin(_botClientMock.Object, chatId, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Non-admin access attempt detected")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleAdminGoalCreationAsync_UnknownStep_LogsWarning() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "Test" }; + var state = new AdminGoalCreationState { CurrentStep = (AdminGoalStep)999 }; // Неизвестный шаг + var cancellationToken = new CancellationToken(); + + _adminStateServiceMock + .Setup(x => x.GetState(123)) + .Returns(state); + + // Act + await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Unknown admin step")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } +} \ No newline at end of file diff --git a/TestBot/Bot/Bot.Hadler/CallbackQueryHandlerTests.cs b/TestBot/Bot/Bot.Hadler/CallbackQueryHandlerTests.cs new file mode 100755 index 0000000..a1c9478 --- /dev/null +++ b/TestBot/Bot/Bot.Hadler/CallbackQueryHandlerTests.cs @@ -0,0 +1,384 @@ +using Bot.Handlers; +using Bot.Services; +using Configurations; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Services; +using Telegram.Bot; +using Telegram.Bot.Types; + +namespace Bot.Tests.Handlers; + +[TestFixture] +public class CallbackQueryHandlerTests +{ + private Mock> _loggerMock; + private Mock _paymentHandlerMock; + private Mock _commandHandlerMock; + private Mock _userStateServiceMock; + private Mock _botClientMock; + private CallbackQueryHandler _handler; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock>(); + + // Явно создаем мок логгера для UserStateService + var userStateServiceLoggerMock = new Mock>(); + _userStateServiceMock = new Mock(userStateServiceLoggerMock.Object); + + // Создаем моки для зависимостей PaymentHandler + var paymentLoggerMock = new Mock>(); + var goalServiceMock = new Mock(); + var donationServiceMock = new Mock(); + var userStateServiceForPaymentMock = new Mock(Mock.Of>()); + var botConfigMock = new Mock>(); + botConfigMock.Setup(x => x.Value).Returns(new BotConfig + { + PaymentProviderToken = "test-token", + }); + + _paymentHandlerMock = new Mock( + paymentLoggerMock.Object, + goalServiceMock.Object, + donationServiceMock.Object, + userStateServiceForPaymentMock.Object, + botConfigMock.Object); + + // Создаем моки для зависимостей CommandHandler + var commandLoggerMock = new Mock>(); + var commandGoalServiceMock = new Mock(); + var keyboardServiceMock = new Mock(Mock.Of>()); + + // Создаем мок для AdminStateService + var adminStateServiceLoggerMock = new Mock>(); + var adminStateServiceMock = new Mock(adminStateServiceLoggerMock.Object); + + // Создаем мок для AdminHandler + var adminHandlerMock = new Mock( + Mock.Of>(), + Mock.Of(), + adminStateServiceMock.Object); + + _commandHandlerMock = new Mock( + commandLoggerMock.Object, + commandGoalServiceMock.Object, + keyboardServiceMock.Object, + adminHandlerMock.Object); + + _botClientMock = new Mock(); + + // Настраиваем бизнес-методы + _paymentHandlerMock + .Setup(x => x.CreateDonationInvoice( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.CompletedTask); + + _commandHandlerMock + .Setup(x => x.HandleStatsCommand( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.CompletedTask); + + _userStateServiceMock + .Setup(x => x.SetWaitingForAmount( + It.IsAny(), + It.IsAny())) + .Verifiable(); + + _handler = new CallbackQueryHandler( + _loggerMock.Object, + _paymentHandlerMock.Object, + _commandHandlerMock.Object, + _userStateServiceMock.Object); + } + + [Test] + public void CanHandle_WithCallbackQuery_ReturnsTrue() + { + // Arrange + var update = new Update { CallbackQuery = new CallbackQuery() }; + + // Act + var result = _handler.CanHandle(update); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void CanHandle_WithoutCallbackQuery_ReturnsFalse() + { + // Arrange + var update = new Update { CallbackQuery = null }; + + // Act + var result = _handler.CanHandle(update); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public async Task HandleAsync_NoChatInformation_DoesNotProcessFurther() + { + // Arrange + var callbackQuery = new CallbackQuery + { + Id = "test_query_id", + From = new User { Id = 123 }, + Data = "donate_100", + Message = null // No message/chat information + }; + var update = new Update { CallbackQuery = callbackQuery }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert - убеждаемся, что бизнес-логика не вызывалась + _paymentHandlerMock.Verify( + x => x.CreateDonationInvoice(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task HandleAsync_EmptyCallbackData_DoesNotProcessFurther() + { + // Arrange + var callbackQuery = new CallbackQuery + { + Id = "test_query_id", + From = new User { Id = 123 }, + Data = null, // Empty callback data + Message = new Message { Chat = new Chat { Id = 456 } } + }; + var update = new Update { CallbackQuery = callbackQuery }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert - убеждаемся, что бизнес-логика не вызывалась + _paymentHandlerMock.Verify( + x => x.CreateDonationInvoice(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task HandleAsync_EnterCustomAmount_SetsUserState() + { + // Arrange + var callbackQuery = new CallbackQuery + { + Id = "test_query_id", + From = new User { Id = 123 }, + Data = "enter_custom_amount", + Message = new Message { Chat = new Chat { Id = 456 } } + }; + var update = new Update { CallbackQuery = callbackQuery }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _userStateServiceMock.Verify( + x => x.SetWaitingForAmount(123, 456), + Times.Once); + } + + [Test] + public async Task HandleAsync_PredefinedDonation_CreatesInvoice() + { + // Arrange + var testCases = new[] + { + new { CallbackData = "donate_100", ExpectedAmount = 100 }, + new { CallbackData = "donate_500", ExpectedAmount = 500 }, + new { CallbackData = "donate_1000", ExpectedAmount = 1000 }, + new { CallbackData = "donate_5000", ExpectedAmount = 5000 } + }; + + foreach (var testCase in testCases) + { + // Reset mocks for each test case + _paymentHandlerMock.Invocations.Clear(); + + var callbackQuery = new CallbackQuery + { + Id = "test_query_id", + From = new User { Id = 123 }, + Data = testCase.CallbackData, + Message = new Message { Chat = new Chat { Id = 456 } } + }; + var update = new Update { CallbackQuery = callbackQuery }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _paymentHandlerMock.Verify( + x => x.CreateDonationInvoice(_botClientMock.Object, 456, 123, testCase.ExpectedAmount, cancellationToken), + Times.Once, + $"Failed for callback data: {testCase.CallbackData}"); + } + } + + [Test] + public async Task HandleAsync_ShowStats_CallsStatsCommand() + { + // Arrange + var callbackQuery = new CallbackQuery + { + Id = "test_query_id", + From = new User { Id = 123 }, + Data = "show_stats", + Message = new Message { Chat = new Chat { Id = 456 } } + }; + var update = new Update { CallbackQuery = callbackQuery }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _commandHandlerMock.Verify( + x => x.HandleStatsCommand(_botClientMock.Object, 456, cancellationToken), + Times.Once); + } + + [Test] + public async Task HandleAsync_UnknownCallbackData_DoesNotCallBusinessLogic() + { + // Arrange + var callbackQuery = new CallbackQuery + { + Id = "test_query_id", + From = new User { Id = 123 }, + Data = "unknown_command", + Message = new Message { Chat = new Chat { Id = 456 } } + }; + var update = new Update { CallbackQuery = callbackQuery }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert - убеждаемся, что бизнес-логика не вызывалась + _paymentHandlerMock.Verify( + x => x.CreateDonationInvoice(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + + _commandHandlerMock.Verify( + x => x.HandleStatsCommand(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + + _userStateServiceMock.Verify( + x => x.SetWaitingForAmount(It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task HandleAsync_PaymentHandlerThrowsFormatException_LogsError() + { + // Arrange + var callbackQuery = new CallbackQuery + { + Id = "test_query_id", + From = new User { Id = 123 }, + Data = "donate_invalid", // This will cause FormatException in parsing + Message = new Message { Chat = new Chat { Id = 456 } } + }; + var update = new Update { CallbackQuery = callbackQuery }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert - проверяем, что ошибка была залогирована + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Failed to parse donation amount")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Never); + } + + [Test] + public async Task HandleAsync_GeneralExceptionInProcess_CallsSafeAnswer() + { + // Arrange + var callbackQuery = new CallbackQuery + { + Id = "test_query_id", + From = new User { Id = 123 }, + Data = "donate_100", + Message = new Message { Chat = new Chat { Id = 456 } } + }; + var update = new Update { CallbackQuery = callbackQuery }; + var cancellationToken = new CancellationToken(); + + // Симулируем исключение в PaymentHandler + _paymentHandlerMock + .Setup(x => x.CreateDonationInvoice(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("Test exception")); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert - проверяем, что ошибка была залогирована + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Failed to create donation invoice")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleAsync_CommandHandlerThrowsException_LogsError() + { + // Arrange + var callbackQuery = new CallbackQuery + { + Id = "test_query_id", + From = new User { Id = 123 }, + Data = "show_stats", + Message = new Message { Chat = new Chat { Id = 456 } } + }; + var update = new Update { CallbackQuery = callbackQuery }; + var cancellationToken = new CancellationToken(); + + // Симулируем исключение в CommandHandler + _commandHandlerMock + .Setup(x => x.HandleStatsCommand(It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("Test exception")); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert - проверяем, что ошибка была залогирована + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Failed to show statistics")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } +} \ No newline at end of file diff --git a/TestBot/Bot/Bot.Hadler/CommandHandlerTests.cs b/TestBot/Bot/Bot.Hadler/CommandHandlerTests.cs new file mode 100755 index 0000000..73786d6 --- /dev/null +++ b/TestBot/Bot/Bot.Hadler/CommandHandlerTests.cs @@ -0,0 +1,528 @@ +using Bot.Handlers; +using Bot.Services; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Services; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegram.Bot.Types.ReplyMarkups; +using Data.Models; + +namespace Bot.Tests.Handlers; + +[TestFixture] +public class CommandHandlerTests +{ + private Mock> _loggerMock; + private Mock _goalServiceMock; + private Mock _keyboardServiceMock; + private Mock _adminHandlerMock; + private Mock _botClientMock; + private CommandHandler _handler; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock>(); + _goalServiceMock = new Mock(); + + // Создаем мок для KeyboardService с правильным конструктором + var keyboardServiceLoggerMock = new Mock>(); + _keyboardServiceMock = new Mock(keyboardServiceLoggerMock.Object); + + // Создаем мок для AdminHandler с правильным конструктором + var adminHandlerLoggerMock = new Mock>(); + var adminStateServiceMock = new Mock(Mock.Of>()); + _adminHandlerMock = new Mock( + adminHandlerLoggerMock.Object, + Mock.Of(), + adminStateServiceMock.Object); + + _botClientMock = new Mock(); + + _handler = new CommandHandler( + _loggerMock.Object, + _goalServiceMock.Object, + _keyboardServiceMock.Object, + _adminHandlerMock.Object); + } + + [Test] + public async Task HandleCommandAsync_NullUser_LogsWarningAndReturns() + { + // Arrange + var message = new Message { From = null, Chat = new Chat { Id = 123 }, Text = "/start" }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Received command from message with null user")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleCommandAsync_EmptyMessageText_LogsWarningAndReturns() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = null }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Received empty message text")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleCommandAsync_StartCommand_RegularUser_SendsMainMenu() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; + var cancellationToken = new CancellationToken(); + var startStats = "Статистика: 5000/10000"; + var keyboard = new ReplyKeyboardMarkup(new[] { new KeyboardButton[] { "💳 Пожертвовать" } }); + + _goalServiceMock + .Setup(x => x.GetStartStats()) + .ReturnsAsync(startStats); + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(123)) + .ReturnsAsync(false); + + _keyboardServiceMock + .Setup(x => x.GetMainMenuKeyboard()) + .Returns(keyboard); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _goalServiceMock.Verify(x => x.GetStartStats(), Times.Once); + _goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); + _keyboardServiceMock.Verify(x => x.GetMainMenuKeyboard(), Times.Once); + _keyboardServiceMock.Verify(x => x.GetMainMenuKeyboardForAdmin(), Times.Never); + } + + [Test] + public async Task HandleCommandAsync_StartCommand_AdminUser_SendsAdminMenu() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; + var cancellationToken = new CancellationToken(); + var startStats = "Статистика: 5000/10000"; + var keyboard = new ReplyKeyboardMarkup(new[] { new KeyboardButton[] { "📝 Создать новую цель" } }); + + _goalServiceMock + .Setup(x => x.GetStartStats()) + .ReturnsAsync(startStats); + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(123)) + .ReturnsAsync(true); + + _keyboardServiceMock + .Setup(x => x.GetMainMenuKeyboardForAdmin()) + .Returns(keyboard); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); + _keyboardServiceMock.Verify(x => x.GetMainMenuKeyboardForAdmin(), Times.Once); + _keyboardServiceMock.Verify(x => x.GetMainMenuKeyboard(), Times.Never); + } + + [Test] + public async Task HandleCommandAsync_DonateCommand_WithActiveGoal_SendsDonationKeyboard() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/donate" }; + var cancellationToken = new CancellationToken(); + var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; + var keyboard = new InlineKeyboardMarkup(new[] { new InlineKeyboardButton[] { InlineKeyboardButton.WithCallbackData("100") } }); + + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(activeGoal); + + _keyboardServiceMock + .Setup(x => x.GetDonationAmountKeyboard()) + .Returns(keyboard); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _goalServiceMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + _keyboardServiceMock.Verify(x => x.GetDonationAmountKeyboard(), Times.Once); + } + + [Test] + public async Task HandleCommandAsync_DonateCommand_NoActiveGoal_SendsErrorMessage() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/donate" }; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync((DonationGoal)null); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _goalServiceMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + _keyboardServiceMock.Verify(x => x.GetDonationAmountKeyboard(), Times.Never); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("No active goal found for donate command")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleCommandAsync_StatsCommand_CallsHandleStatsCommand() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/stats" }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert - проверяем, что вызывается метод HandleStatsCommand + // Поскольку HandleStatsCommand виртуальный, мы можем проверить его вызов + } + + [Test] + public async Task HandleStatsCommand_Successful_LogsAndSendsStats() + { + // Arrange + var chatId = 456L; + var cancellationToken = new CancellationToken(); + var stats = "Статистика: 5000/10000"; + + _goalServiceMock + .Setup(x => x.GetGoalStatsAsync()) + .ReturnsAsync(stats); + + // Act + await _handler.HandleStatsCommand(_botClientMock.Object, chatId, cancellationToken); + + // Assert + _goalServiceMock.Verify(x => x.GetGoalStatsAsync(), Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Processing stats command")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Statistics sent successfully")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleStatsCommand_ThrowsException_LogsError() + { + // Arrange + var chatId = 456L; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.GetGoalStatsAsync()) + .ThrowsAsync(new Exception("Database error")); + + // Act + await _handler.HandleStatsCommand(_botClientMock.Object, chatId, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error getting stats")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleCommandAsync_AddGoalCommand_AdminUser_StartsGoalCreation() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/addgoal" }; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(123)) + .ReturnsAsync(true); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _adminHandlerMock.Verify( + x => x.StartGoalCreationAsync(_botClientMock.Object, 456, 123, cancellationToken), + Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Admin user") && v.ToString().Contains("starting goal creation")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleCommandAsync_AddGoalCommand_NonAdminUser_HandlesNotAdmin() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/addgoal" }; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(123)) + .ReturnsAsync(false); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _adminHandlerMock.Verify( + x => x.HandleNotAdmin(_botClientMock.Object, 456, cancellationToken), + Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Non-admin user") && v.ToString().Contains("attempted to create goal")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleCommandAsync_UnknownCommand_AdminUser_SendsAdminHelp() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "unknown_command" }; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(123)) + .ReturnsAsync(true); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Unknown command from user")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleCommandAsync_UnknownCommand_RegularUser_SendsRegularHelp() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "unknown_command" }; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(123)) + .ReturnsAsync(false); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Unknown command from user")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [TestCase("/start")] + [TestCase("🔄 Обновить")] + [TestCase("Обновить")] + public async Task HandleCommandAsync_StartCommandVariants_CallsHandleStartCommand(string command) + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = command }; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.GetStartStats()) + .ReturnsAsync("Статистика"); + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(123)) + .ReturnsAsync(false); + + _keyboardServiceMock + .Setup(x => x.GetMainMenuKeyboard()) + .Returns(new ReplyKeyboardMarkup(new KeyboardButton[0][])); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _goalServiceMock.Verify(x => x.GetStartStats(), Times.Once); + } + + [TestCase("/donate")] + [TestCase("💳 Пожертвовать")] + [TestCase("Пожертвовать")] + public async Task HandleCommandAsync_DonateCommandVariants_CallsHandleDonateCommand(string command) + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = command }; + var cancellationToken = new CancellationToken(); + var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; + + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(activeGoal); + + _keyboardServiceMock + .Setup(x => x.GetDonationAmountKeyboard()) + .Returns(new InlineKeyboardMarkup(new InlineKeyboardButton[0][])); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _goalServiceMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + _keyboardServiceMock.Verify(x => x.GetDonationAmountKeyboard(), Times.Once); + } + + [Test] + public async Task HandleCommandAsync_StatsCommandAliases_AllCallHandleStatsCommand() + { + // Arrange + var user = new User { Id = 123 }; + var aliases = new[] { "/stats", "📊 Статистика", "Статистика" }; + var cancellationToken = new CancellationToken(); + + foreach (var alias in aliases) + { + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = alias }; + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert - проверяем, что команда обрабатывается без ошибок + } + } + + [TestCase("/addgoal")] + [TestCase("📝 Создать новую цель")] + [TestCase("Создать новую цель")] + public async Task HandleCommandAsync_AddGoalCommandAliases_AllCallHandleAddGoalCommand(string command) + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = command }; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(123)) + .ReturnsAsync(false); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); + + } + + [Test] + public async Task HandleCommandAsync_ThrowsException_LogsErrorAndSendsErrorMessage() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.GetStartStats()) + .ThrowsAsync(new Exception("Test exception")); + + // Act + await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error processing command")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Never); + } +} \ No newline at end of file diff --git a/TestBot/Bot/Bot.Hadler/MessageHandlerTests.cs b/TestBot/Bot/Bot.Hadler/MessageHandlerTests.cs new file mode 100755 index 0000000..20ec42c --- /dev/null +++ b/TestBot/Bot/Bot.Hadler/MessageHandlerTests.cs @@ -0,0 +1,435 @@ +using Bot.Handlers; +using Bot.Services; +using Configurations; +using Data.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Services; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Payments; + +namespace Bot.Tests.Handlers; + +[TestFixture] +public class MessageHandlerTests +{ + private Mock> _loggerMock; + private Mock _donationServiceMock; + private Mock _goalServiceMock; + private Mock _commandHandlerMock; + private Mock _paymentHandlerMock; + private Mock _userStateServiceMock; + private Mock _adminHandlerMock; + private Mock _adminStateServiceMock; + private Mock _botClientMock; + private MessageHandler _handler; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock>(); + _donationServiceMock = new Mock(); + _goalServiceMock = new Mock(); + + // Явно создаем моки для всех зависимостей, как в предыдущем SetUp + + // Создаем мок логгера для UserStateService + var userStateServiceLoggerMock = new Mock>(); + _userStateServiceMock = new Mock(userStateServiceLoggerMock.Object); + + // Создаем моки для зависимостей PaymentHandler (как в предыдущем SetUp) + var paymentLoggerMock = new Mock>(); + var paymentGoalServiceMock = new Mock(); + var paymentDonationServiceMock = new Mock(); + var userStateServiceForPaymentMock = new Mock(Mock.Of>()); + var botConfigMock = new Mock>(); + botConfigMock.Setup(x => x.Value).Returns(new BotConfig + { + PaymentProviderToken = "test-token", + }); + + _paymentHandlerMock = new Mock( + paymentLoggerMock.Object, + paymentGoalServiceMock.Object, + paymentDonationServiceMock.Object, + userStateServiceForPaymentMock.Object, + botConfigMock.Object); + + // Создаем моки для зависимостей CommandHandler + var commandLoggerMock = new Mock>(); + var commandGoalServiceMock = new Mock(); + var keyboardServiceMock = new Mock(Mock.Of>()); + + // Создаем мок для AdminStateService + var adminStateServiceLoggerMock = new Mock>(); + _adminStateServiceMock = new Mock(adminStateServiceLoggerMock.Object); + + // Создаем мок для AdminHandler + _adminHandlerMock = new Mock( + Mock.Of>(), + Mock.Of(), + _adminStateServiceMock.Object); + + _commandHandlerMock = new Mock( + commandLoggerMock.Object, + commandGoalServiceMock.Object, + keyboardServiceMock.Object, + _adminHandlerMock.Object); + + _botClientMock = new Mock(); + + // Настраиваем базовые методы + _donationServiceMock + .Setup(x => x.GetOrCreateUserAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new Data.Models.Users + { + Id = 123, + Username = "testuser", + FirstName = "Test", + LastName = "User" + })); // Исправлено - возвращаем User + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(It.IsAny())) + .ReturnsAsync(false); + + _commandHandlerMock + .Setup(x => x.HandleCommandAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + _paymentHandlerMock + .Setup(x => x.HandleSuccessfulPaymentAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + _paymentHandlerMock + .Setup(x => x.HandleCustomAmountInputAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + _adminHandlerMock + .Setup(x => x.HandleAdminGoalCreationAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + _handler = new MessageHandler( + _loggerMock.Object, + _donationServiceMock.Object, + _goalServiceMock.Object, + _commandHandlerMock.Object, + _paymentHandlerMock.Object, + _userStateServiceMock.Object, + _adminHandlerMock.Object, + _adminStateServiceMock.Object); + } + + [Test] + public void CanHandle_WithMessage_ReturnsTrue() + { + // Arrange + var update = new Update { Message = new Message() }; + + // Act + var result = _handler.CanHandle(update); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void CanHandle_WithoutMessage_ReturnsFalse() + { + // Arrange + var update = new Update { Message = null }; + + // Act + var result = _handler.CanHandle(update); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public async Task HandleAsync_MessageWithNullUser_LogsWarningAndReturns() + { + // Arrange + var message = new Message { From = null, Chat = new Chat { Id = 123 } }; + var update = new Update { Message = message }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Received message with null user information")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + + // Проверяем, что бизнес-логика не вызывалась + _donationServiceMock.Verify( + x => x.GetOrCreateUserAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task HandleAsync_ValidMessage_RegistersOrUpdatesUser() + { + // Arrange + var user = new User { Id = 123, Username = "testuser", FirstName = "Test", LastName = "User" }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "test" }; + var update = new Update { Message = message }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _donationServiceMock.Verify( + x => x.GetOrCreateUserAsync(123, "testuser", "Test", "User"), + Times.Once); + } + + [Test] + public async Task HandleAsync_SuccessfulPayment_ProcessesPayment() + { + // Arrange + var user = new User { Id = 123 }; + var successfulPayment = new SuccessfulPayment(); + var message = new Message { From = user, Chat = new Chat { Id = 456 }, SuccessfulPayment = successfulPayment }; + var update = new Update { Message = message }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _paymentHandlerMock.Verify( + x => x.HandleSuccessfulPaymentAsync(_botClientMock.Object, message, cancellationToken), + Times.Once); + + // Проверяем, что дальнейшая обработка не происходит + _commandHandlerMock.Verify( + x => x.HandleCommandAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task HandleAsync_UserWaitingForAmount_ProcessesCustomAmount() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "500" }; + var update = new Update { Message = message }; + var cancellationToken = new CancellationToken(); + + _userStateServiceMock + .Setup(x => x.IsWaitingForAmount(123, 456)) + .Returns(true); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _paymentHandlerMock.Verify( + x => x.HandleCustomAmountInputAsync(_botClientMock.Object, message, cancellationToken), + Times.Once); + + // Проверяем, что обычная обработка команд не происходит + _commandHandlerMock.Verify( + x => x.HandleCommandAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task HandleAsync_AdminCreatingGoal_ProcessesGoalCreation() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "Новая цель" }; + var update = new Update { Message = message }; + var cancellationToken = new CancellationToken(); + + _userStateServiceMock + .Setup(x => x.IsWaitingForAmount(123, 456)) + .Returns(false); + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(123)) + .ReturnsAsync(true); + + _adminStateServiceMock + .Setup(x => x.IsUserCreatingGoal(123)) + .Returns(true); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _adminHandlerMock.Verify( + x => x.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken), + Times.Once); + + // Проверяем, что обычная обработка команд не происходит + _commandHandlerMock.Verify( + x => x.HandleCommandAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task HandleAsync_RegularTextMessage_ProcessesCommand() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; + var update = new Update { Message = message }; + var cancellationToken = new CancellationToken(); + + _userStateServiceMock + .Setup(x => x.IsWaitingForAmount(123, 456)) + .Returns(false); + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(123)) + .ReturnsAsync(false); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _commandHandlerMock.Verify( + x => x.HandleCommandAsync(_botClientMock.Object, message, cancellationToken), + Times.Once); + } + + [Test] + public async Task HandleAsync_NonTextMessage_DoesNotProcessCommand() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = null }; // Не текстовое сообщение + var update = new Update { Message = message }; + var cancellationToken = new CancellationToken(); + + _userStateServiceMock + .Setup(x => x.IsWaitingForAmount(123, 456)) + .Returns(false); + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(123)) + .ReturnsAsync(false); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _commandHandlerMock.Verify( + x => x.HandleCommandAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task HandleAsync_UserRegistrationFails_LogsErrorButContinues() + { + // Arrange + var user = new User { Id = 123, Username = "testuser", FirstName = "Test", LastName = "User" }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; + var update = new Update { Message = message }; + var cancellationToken = new CancellationToken(); + + _donationServiceMock + .Setup(x => x.GetOrCreateUserAsync(123, "testuser", "Test", "User")) + .ThrowsAsync(new Exception("Database error")); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Failed to register/update user")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + + // Проверяем, что обработка команды все равно происходит + _commandHandlerMock.Verify( + x => x.HandleCommandAsync(_botClientMock.Object, message, cancellationToken), + Times.Once); + } + + [Test] + public async Task HandleAsync_CommandHandlerThrowsException_LogsError() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; + var update = new Update { Message = message }; + var cancellationToken = new CancellationToken(); + + _userStateServiceMock + .Setup(x => x.IsWaitingForAmount(123, 456)) + .Returns(false); + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(123)) + .ReturnsAsync(false); + + _commandHandlerMock + .Setup(x => x.HandleCommandAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("Command processing failed")); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error processing text message")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleAsync_AdminNotCreatingGoal_ProcessesAsRegularCommand() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; + var update = new Update { Message = message }; + var cancellationToken = new CancellationToken(); + + _userStateServiceMock + .Setup(x => x.IsWaitingForAmount(123, 456)) + .Returns(false); + + _goalServiceMock + .Setup(x => x.IsUserAdminAsync(123)) + .ReturnsAsync(true); // Пользователь - админ + + _adminStateServiceMock + .Setup(x => x.IsUserCreatingGoal(123)) + .Returns(false); // Но не создает цель + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _adminHandlerMock.Verify( + x => x.HandleAdminGoalCreationAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + + _commandHandlerMock.Verify( + x => x.HandleCommandAsync(_botClientMock.Object, message, cancellationToken), + Times.Once); + } +} \ No newline at end of file diff --git a/TestBot/Bot/Bot.Hadler/PaymentHandlerTests.cs b/TestBot/Bot/Bot.Hadler/PaymentHandlerTests.cs new file mode 100755 index 0000000..34359da --- /dev/null +++ b/TestBot/Bot/Bot.Hadler/PaymentHandlerTests.cs @@ -0,0 +1,487 @@ +using Bot.Handlers; +using Bot.Services; +using Configurations; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Services; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Payments; +using Data.Models; + +namespace Bot.Tests.Handlers; + +[TestFixture] +public class PaymentHandlerTests +{ + private Mock> _loggerMock; + private Mock _goalServiceMock; + private Mock _donationServiceMock; + private Mock _userStateServiceMock; + private Mock _botClientMock; + private PaymentHandler _handler; + private BotConfig _botConfig; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock>(); + _goalServiceMock = new Mock(); + _donationServiceMock = new Mock(); + + // Создаем мок для UserStateService с правильным конструктором + var userStateServiceLoggerMock = new Mock>(); + _userStateServiceMock = new Mock(userStateServiceLoggerMock.Object); + + _botClientMock = new Mock(); + + // Настраиваем конфигурацию бота + _botConfig = new BotConfig { PaymentProviderToken = "test-payment-token" }; + var botConfigOptions = Options.Create(_botConfig); + + _handler = new PaymentHandler( + _loggerMock.Object, + _goalServiceMock.Object, + _donationServiceMock.Object, + _userStateServiceMock.Object, + botConfigOptions); + } + + [Test] + public async Task HandleCustomAmountInputAsync_NullUser_LogsWarningAndReturns() + { + // Arrange + var message = new Message { From = null, Chat = new Chat { Id = 123 }, Text = "500" }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleCustomAmountInputAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Received custom amount input from null user")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleCustomAmountInputAsync_EmptyText_RemovesStateAndSendsErrorMessage() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = null }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleCustomAmountInputAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _userStateServiceMock.Verify(x => x.RemoveWaitingForAmount(123), Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("sent empty custom amount")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleCustomAmountInputAsync_InvalidNumber_RemovesStateAndSendsErrorMessage() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "invalid" }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleCustomAmountInputAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _userStateServiceMock.Verify(x => x.RemoveWaitingForAmount(123), Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("sent invalid custom amount")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleCustomAmountInputAsync_ValidAmount_CallsCreateDonationInvoice() + { + // Arrange + var user = new User { Id = 123 }; + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "500" }; + var cancellationToken = new CancellationToken(); + + var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(activeGoal); + + // Act + await _handler.HandleCustomAmountInputAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _userStateServiceMock.Verify(x => x.RemoveWaitingForAmount(123), Times.Once); + } + + [Test] + public async Task CreateDonationInvoice_NoActiveGoal_SendsErrorMessage() + { + // Arrange + var chatId = 456L; + var userId = 123L; + var amount = 500; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync((DonationGoal)null); + + // Act + await _handler.CreateDonationInvoice(_botClientMock.Object, chatId, userId, amount, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("No active goal found for donation")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task CreateDonationInvoice_AmountBelowMinimum_SendsValidationMessage() + { + // Arrange + var chatId = 456L; + var userId = 123L; + var amount = 50; // Ниже минимума 60 + var cancellationToken = new CancellationToken(); + var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; + + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(activeGoal); + + // Act + await _handler.CreateDonationInvoice(_botClientMock.Object, chatId, userId, amount, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("below minimum limit")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task CreateDonationInvoice_AmountAboveMaximum_SendsValidationMessage() + { + // Arrange + var chatId = 456L; + var userId = 123L; + var amount = 100001; // Выше максимума 100000 + var cancellationToken = new CancellationToken(); + var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; + + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(activeGoal); + + // Act + await _handler.CreateDonationInvoice(_botClientMock.Object, chatId, userId, amount, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("exceeds maximum limit")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task CreateDonationInvoice_ValidAmount_LogsSuccess() + { + // Arrange + var chatId = 456L; + var userId = 123L; + var amount = 500; + var cancellationToken = new CancellationToken(); + var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; + + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(activeGoal); + + // Act + await _handler.CreateDonationInvoice(_botClientMock.Object, chatId, userId, amount, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Creating donation invoice")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task CreateDonationInvoice_GoalServiceThrows_LogsErrorAndSendsErrorMessage() + { + // Arrange + var chatId = 456L; + var userId = 123L; + var amount = 500; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ThrowsAsync(new Exception("Database error")); + + // Act + await _handler.CreateDonationInvoice(_botClientMock.Object, chatId, userId, amount, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error creating donation invoice")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleSuccessfulPaymentAsync_NullPaymentOrUser_LogsWarningAndReturns() + { + // Arrange + var message = new Message { From = null, Chat = new Chat { Id = 123 }, SuccessfulPayment = null }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleSuccessfulPaymentAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Received successful payment with null payment or user information")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleSuccessfulPaymentAsync_ValidPayment_ProcessesDonation() + { + // Arrange + var user = new User { Id = 123 }; + var successfulPayment = new SuccessfulPayment + { + TelegramPaymentChargeId = "charge_123", + TotalAmount = 50000, // 500.00 RUB + Currency = "RUB" + }; + var message = new Message + { + From = user, + Chat = new Chat { Id = 456 }, + SuccessfulPayment = successfulPayment, + }; + var cancellationToken = new CancellationToken(); + + _donationServiceMock + .Setup(x => x.ProcessDonationAsync(123, 500, "RUB", "charge_123")) + .ReturnsAsync(true); + + // Act + await _handler.HandleSuccessfulPaymentAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _donationServiceMock.Verify( + x => x.ProcessDonationAsync(123, 500, "RUB", "charge_123"), + Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Successfully processed donation")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleSuccessfulPaymentAsync_DonationServiceReturnsFalse_LogsErrorAndSendsErrorMessage() + { + // Arrange + var user = new User { Id = 123 }; + var successfulPayment = new SuccessfulPayment + { + TelegramPaymentChargeId = "charge_123", + TotalAmount = 50000, + Currency = "RUB" + }; + var message = new Message + { + From = user, + Chat = new Chat { Id = 456 }, + SuccessfulPayment = successfulPayment, + }; + var cancellationToken = new CancellationToken(); + + _donationServiceMock + .Setup(x => x.ProcessDonationAsync(123, 500, "RUB", "charge_123")) + .ReturnsAsync(false); + + // Act + await _handler.HandleSuccessfulPaymentAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Failed to process donation")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleSuccessfulPaymentAsync_DonationServiceThrows_LogsErrorAndSendsErrorMessage() + { + // Arrange + var user = new User { Id = 123 }; + var successfulPayment = new SuccessfulPayment + { + TelegramPaymentChargeId = "charge_123", + TotalAmount = 50000, + Currency = "RUB" + }; + var message = new Message + { + From = user, + Chat = new Chat { Id = 456 }, + SuccessfulPayment = successfulPayment, + }; + var cancellationToken = new CancellationToken(); + + _donationServiceMock + .Setup(x => x.ProcessDonationAsync(123, 500, "RUB", "charge_123")) + .ThrowsAsync(new Exception("Database error")); + + // Act + await _handler.HandleSuccessfulPaymentAsync(_botClientMock.Object, message, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error handling successful payment")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleSuccessfulPaymentAsync_ValidPayment_DeletesInvoiceMessage() + { + // Arrange + var user = new User { Id = 123 }; + var successfulPayment = new SuccessfulPayment + { + TelegramPaymentChargeId = "charge_123", + TotalAmount = 50000, + Currency = "RUB" + }; + var message = new Message + { + From = user, + Chat = new Chat { Id = 456 }, + //MessageId = 789, + SuccessfulPayment = successfulPayment + }; + var cancellationToken = new CancellationToken(); + + _donationServiceMock + .Setup(x => x.ProcessDonationAsync(123, 500, "RUB", "charge_123")) + .ReturnsAsync(true); + + // Act + await _handler.HandleSuccessfulPaymentAsync(_botClientMock.Object, message, cancellationToken); + } + + [Test] + public async Task CreateDonationInvoice_AmountBoundaryValues_ValidatesCorrectly() + { + // Arrange + var chatId = 456L; + var userId = 123L; + var cancellationToken = new CancellationToken(); + var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; + + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(activeGoal); + + // Test cases: amount, shouldBeValid + var testCases = new[] + { + new { Amount = 59, ShouldBeValid = false }, + new { Amount = 60, ShouldBeValid = true }, + new { Amount = 100000, ShouldBeValid = true }, + new { Amount = 100001, ShouldBeValid = false }, + new { Amount = 500, ShouldBeValid = true }, + new { Amount = 0, ShouldBeValid = false }, + new { Amount = -100, ShouldBeValid = false } + }; + + foreach (var testCase in testCases) + { + // Act + await _handler.CreateDonationInvoice(_botClientMock.Object, chatId, userId, testCase.Amount, cancellationToken); + + // Assert + if (!testCase.ShouldBeValid) + { + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("provided invalid donation amount")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.AtLeastOnce); + } + } + } +} \ No newline at end of file diff --git a/TestBot/Bot/Bot.Hadler/PreCheckoutQueryHandlerTests.cs b/TestBot/Bot/Bot.Hadler/PreCheckoutQueryHandlerTests.cs new file mode 100755 index 0000000..18091b8 --- /dev/null +++ b/TestBot/Bot/Bot.Hadler/PreCheckoutQueryHandlerTests.cs @@ -0,0 +1,272 @@ +using Bot.Handlers; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Services; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Payments; +using Data.Models; + +namespace Bot.Tests.Handlers; + +[TestFixture] +public class PreCheckoutQueryHandlerTests +{ + private Mock> _loggerMock; + private Mock _goalServiceMock; + private Mock _botClientMock; + private PreCheckoutQueryHandler _handler; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock>(); + _goalServiceMock = new Mock(); + _botClientMock = new Mock(); + + _handler = new PreCheckoutQueryHandler( + _loggerMock.Object, + _goalServiceMock.Object); + } + + [Test] + public void CanHandle_WithPreCheckoutQuery_ReturnsTrue() + { + // Arrange + var update = new Update { PreCheckoutQuery = new PreCheckoutQuery() }; + + // Act + var result = _handler.CanHandle(update); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void CanHandle_WithoutPreCheckoutQuery_ReturnsFalse() + { + // Arrange + var update = new Update { PreCheckoutQuery = null }; + + // Act + var result = _handler.CanHandle(update); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public async Task HandleAsync_PreCheckoutQueryWithNullUser_LogsWarningAndReturns() + { + // Arrange + var preCheckoutQuery = new PreCheckoutQuery + { + Id = "test_query_id", + From = null, // Null user + InvoicePayload = "test_payload", + TotalAmount = 10000, // 100.00 RUB + Currency = "RUB" + }; + var update = new Update { PreCheckoutQuery = preCheckoutQuery }; + var cancellationToken = new CancellationToken(); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Received pre-checkout query with null user information")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + + // Проверяем, что бизнес-логика не вызывалась + _goalServiceMock.Verify( + x => x.GetActiveGoalAsync(), + Times.Never); + } + + [Test] + public async Task HandleAsync_WithActiveGoal_ApprovesPreCheckoutQuery() + { + // Arrange + var user = new User { Id = 123 }; + var preCheckoutQuery = new PreCheckoutQuery + { + Id = "test_query_id", + From = user, + InvoicePayload = "test_payload", + TotalAmount = 10000, // 100.00 RUB + Currency = "RUB" + }; + var update = new Update { PreCheckoutQuery = preCheckoutQuery }; + var cancellationToken = new CancellationToken(); + + var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal", TargetAmount = 100000 }; + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(activeGoal); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Approved pre-checkout query")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleAsync_WithoutActiveGoal_RejectsPreCheckoutQuery() + { + // Arrange + var user = new User { Id = 123 }; + var preCheckoutQuery = new PreCheckoutQuery + { + Id = "test_query_id", + From = user, + InvoicePayload = "test_payload", + TotalAmount = 10000, + Currency = "RUB" + }; + var update = new Update { PreCheckoutQuery = preCheckoutQuery }; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync((DonationGoal)null); // Нет активной цели + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Rejected pre-checkout query") && v.ToString().Contains("no active goal found")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleAsync_GoalServiceThrowsException_LogsErrorAndCallsSafeAnswer() + { + // Arrange + var user = new User { Id = 123 }; + var preCheckoutQuery = new PreCheckoutQuery + { + Id = "test_query_id", + From = user, + InvoicePayload = "test_payload", + TotalAmount = 10000, + Currency = "RUB" + }; + var update = new Update { PreCheckoutQuery = preCheckoutQuery }; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ThrowsAsync(new Exception("Database connection failed")); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error handling pre-checkout query")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleAsync_WithActiveGoal_LogsCorrectDetails() + { + // Arrange + var user = new User { Id = 123 }; + var preCheckoutQuery = new PreCheckoutQuery + { + Id = "test_query_id", + From = user, + InvoicePayload = "donation_500", + TotalAmount = 50000, // 500.00 RUB + Currency = "RUB" + }; + var update = new Update { PreCheckoutQuery = preCheckoutQuery }; + var cancellationToken = new CancellationToken(); + + var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal", TargetAmount = 100000 }; + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(activeGoal); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert - проверяем, что логи содержат правильные детали + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => + v.ToString().Contains("Approved pre-checkout query") && + v.ToString().Contains("123") && // user ID + v.ToString().Contains("donation_500") && // payload + v.ToString().Contains("500") && // amount + v.ToString().Contains("RUB")), // currency + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleAsync_WithoutActiveGoal_LogsRejectionDetails() + { + // Arrange + var user = new User { Id = 123 }; + var preCheckoutQuery = new PreCheckoutQuery + { + Id = "test_query_id", + From = user, + InvoicePayload = "donation_500", + TotalAmount = 50000, + Currency = "RUB" + }; + var update = new Update { PreCheckoutQuery = preCheckoutQuery }; + var cancellationToken = new CancellationToken(); + + _goalServiceMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync((DonationGoal)null); + + // Act + await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + + // Assert - проверяем, что логи содержат правильные детали отклонения + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => + v.ToString().Contains("Rejected pre-checkout query") && + v.ToString().Contains("123") && // user ID + v.ToString().Contains("no active goal found")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } +} \ No newline at end of file diff --git a/TestBot/Bot/Bot.Service/AdminStateServiceTests.cs b/TestBot/Bot/Bot.Service/AdminStateServiceTests.cs new file mode 100755 index 0000000..cbbc54b --- /dev/null +++ b/TestBot/Bot/Bot.Service/AdminStateServiceTests.cs @@ -0,0 +1,455 @@ +using Bot.Services; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using static Bot.Services.AdminStateService; + +namespace Bot.Tests.Services; + +[TestFixture] +public class AdminStateServiceTests +{ + private Mock> _loggerMock; + private AdminStateService _adminStateService; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock>(); + _adminStateService = new AdminStateService(_loggerMock.Object); + } + + [Test] + public void StartGoalCreation_NewUser_SetsInitialState() + { + // Arrange + var userId = 123L; + var chatId = 456L; + + // Act + _adminStateService.StartGoalCreation(userId, chatId); + + // Assert + var state = _adminStateService.GetState(userId); + Assert.That(state, Is.Not.Null); + Assert.That(state.ChatId, Is.EqualTo(chatId)); + Assert.That(state.CurrentStep, Is.EqualTo(AdminGoalStep.WaitingForTitle)); + Assert.That(state.Title, Is.Null); + Assert.That(state.Description, Is.Null); + Assert.That(state.TargetAmount, Is.Null); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Started goal creation for admin user")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void StartGoalCreation_ExistingUser_OverwritesState() + { + // Arrange + var userId = 123L; + var chatId = 456L; + _adminStateService.StartGoalCreation(userId, chatId); + + // Act - запускаем создание цели заново + _adminStateService.StartGoalCreation(userId, 789L); // Новый chatId + + // Assert + var state = _adminStateService.GetState(userId); + Assert.That(state.ChatId, Is.EqualTo(789L)); + Assert.That(state.CurrentStep, Is.EqualTo(AdminGoalStep.WaitingForTitle)); + } + + [Test] + public void GetState_NonExistentUser_ReturnsNull() + { + // Arrange + var userId = 999L; // Несуществующий пользователь + + // Act + var state = _adminStateService.GetState(userId); + + // Assert + Assert.That(state, Is.Null); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("No state found for user")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void GetState_ExistingUser_ReturnsState() + { + // Arrange + var userId = 123L; + var chatId = 456L; + _adminStateService.StartGoalCreation(userId, chatId); + + // Act + var state = _adminStateService.GetState(userId); + + // Assert + Assert.That(state, Is.Not.Null); + Assert.That(state.ChatId, Is.EqualTo(chatId)); + } + + [Test] + public void SetTitle_ExistingUser_SetsTitleAndAdvancesStep() + { + // Arrange + var userId = 123L; + var chatId = 456L; + var title = "Новая цель"; + _adminStateService.StartGoalCreation(userId, chatId); + + // Act + _adminStateService.SetTitle(userId, title); + + // Assert + var state = _adminStateService.GetState(userId); + Assert.That(state.Title, Is.EqualTo(title)); + Assert.That(state.CurrentStep, Is.EqualTo(AdminGoalStep.WaitingForDescription)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Set title for user") && v.ToString().Contains(title)), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void SetTitle_NonExistentUser_LogsWarning() + { + // Arrange + var userId = 999L; // Несуществующий пользователь + var title = "Новая цель"; + + // Act + _adminStateService.SetTitle(userId, title); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Attempted to set title for non-existent user state")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void SetDescription_ExistingUser_SetsDescriptionAndAdvancesStep() + { + // Arrange + var userId = 123L; + var chatId = 456L; + var description = "Описание цели"; + _adminStateService.StartGoalCreation(userId, chatId); + _adminStateService.SetTitle(userId, "Название"); + + // Act + _adminStateService.SetDescription(userId, description); + + // Assert + var state = _adminStateService.GetState(userId); + Assert.That(state.Description, Is.EqualTo(description)); + Assert.That(state.CurrentStep, Is.EqualTo(AdminGoalStep.WaitingForAmount)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Set description for user")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void SetDescription_NonExistentUser_LogsWarning() + { + // Arrange + var userId = 999L; // Несуществующий пользователь + var description = "Описание цели"; + + // Act + _adminStateService.SetDescription(userId, description); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Attempted to set description for non-existent user state")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void SetAmount_ExistingUser_SetsAmountAndCompletesProcess() + { + // Arrange + var userId = 123L; + var chatId = 456L; + var amount = 5000m; + _adminStateService.StartGoalCreation(userId, chatId); + _adminStateService.SetTitle(userId, "Название"); + _adminStateService.SetDescription(userId, "Описание"); + + // Act + _adminStateService.SetAmount(userId, amount); + + // Assert + var state = _adminStateService.GetState(userId); + Assert.That(state.TargetAmount, Is.EqualTo(amount)); + Assert.That(state.CurrentStep, Is.EqualTo(AdminGoalStep.None)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Set amount for user") && v.ToString().Contains(amount.ToString())), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void SetAmount_NonExistentUser_LogsWarning() + { + // Arrange + var userId = 999L; // Несуществующий пользователь + var amount = 5000m; + + // Act + _adminStateService.SetAmount(userId, amount); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Attempted to set amount for non-existent user state")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void CancelGoalCreation_ExistingUser_RemovesState() + { + // Arrange + var userId = 123L; + var chatId = 456L; + _adminStateService.StartGoalCreation(userId, chatId); + + // Act + _adminStateService.CancelGoalCreation(userId); + + // Assert + var state = _adminStateService.GetState(userId); + Assert.That(state, Is.Null); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Canceled goal creation for user")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void CancelGoalCreation_NonExistentUser_LogsDebug() + { + // Arrange + var userId = 999L; // Несуществующий пользователь + + // Act + _adminStateService.CancelGoalCreation(userId); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Attempted to cancel non-existent goal creation for user")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void IsUserCreatingGoal_UserInProcess_ReturnsTrue() + { + // Arrange + var userId = 123L; + var chatId = 456L; + _adminStateService.StartGoalCreation(userId, chatId); + + // Act + var isCreating = _adminStateService.IsUserCreatingGoal(userId); + + // Assert + Assert.That(isCreating, Is.True); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("User") && v.ToString().Contains("goal creation status: True")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void IsUserCreatingGoal_UserNotInProcess_ReturnsFalse() + { + // Arrange + var userId = 123L; // Пользователь не начинал создание цели + + // Act + var isCreating = _adminStateService.IsUserCreatingGoal(userId); + + // Assert + Assert.That(isCreating, Is.False); + } + + [Test] + public void IsUserCreatingGoal_UserCompletedProcess_ReturnsFalse() + { + // Arrange + var userId = 123L; + var chatId = 456L; + _adminStateService.StartGoalCreation(userId, chatId); + _adminStateService.SetTitle(userId, "Название"); + _adminStateService.SetDescription(userId, "Описание"); + _adminStateService.SetAmount(userId, 5000m); // Завершаем процесс + + // Act + var isCreating = _adminStateService.IsUserCreatingGoal(userId); + + // Assert + Assert.That(isCreating, Is.False); + } + + [Test] + public void IsUserCreatingGoal_UserCancelledProcess_ReturnsFalse() + { + // Arrange + var userId = 123L; + var chatId = 456L; + _adminStateService.StartGoalCreation(userId, chatId); + _adminStateService.CancelGoalCreation(userId); // Отменяем процесс + + // Act + var isCreating = _adminStateService.IsUserCreatingGoal(userId); + + // Assert + Assert.That(isCreating, Is.False); + } + + [Test] + public void GetActiveStateCount_NoUsers_ReturnsZero() + { + // Arrange - нет активных пользователей + + // Act + var count = _adminStateService.GetActiveStateCount(); + + // Assert + Assert.That(count, Is.EqualTo(0)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Trace, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Current active admin states: 0")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void GetActiveStateCount_MultipleUsers_ReturnsCorrectCount() + { + // Arrange + _adminStateService.StartGoalCreation(123L, 456L); + _adminStateService.StartGoalCreation(124L, 457L); + _adminStateService.StartGoalCreation(125L, 458L); + + // Act + var count = _adminStateService.GetActiveStateCount(); + + // Assert + Assert.That(count, Is.EqualTo(3)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Trace, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Current active admin states: 3")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void GetActiveStateCount_AfterCancellation_ReturnsUpdatedCount() + { + // Arrange + _adminStateService.StartGoalCreation(123L, 456L); + _adminStateService.StartGoalCreation(124L, 457L); + _adminStateService.StartGoalCreation(125L, 458L); + + // Act - отменяем одного пользователя + _adminStateService.CancelGoalCreation(124L); + var count = _adminStateService.GetActiveStateCount(); + + // Assert + Assert.That(count, Is.EqualTo(2)); + } + + [Test] + public void MultipleUsers_IndependentStates() + { + // Arrange + var user1 = 123L; + var user2 = 124L; + var chat1 = 456L; + var chat2 = 457L; + + // Act + _adminStateService.StartGoalCreation(user1, chat1); + _adminStateService.StartGoalCreation(user2, chat2); + + _adminStateService.SetTitle(user1, "Цель пользователя 1"); + _adminStateService.SetTitle(user2, "Цель пользователя 2"); + + // Assert + var state1 = _adminStateService.GetState(user1); + var state2 = _adminStateService.GetState(user2); + + Assert.That(state1.Title, Is.EqualTo("Цель пользователя 1")); + Assert.That(state2.Title, Is.EqualTo("Цель пользователя 2")); + Assert.That(state1.ChatId, Is.EqualTo(chat1)); + Assert.That(state2.ChatId, Is.EqualTo(chat2)); + } +} \ No newline at end of file diff --git a/TestBot/Bot/Bot.Service/UserStateServiceTests.cs b/TestBot/Bot/Bot.Service/UserStateServiceTests.cs new file mode 100755 index 0000000..ffefdae --- /dev/null +++ b/TestBot/Bot/Bot.Service/UserStateServiceTests.cs @@ -0,0 +1,446 @@ +using Bot.Services; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; + +namespace Bot.Tests.Services; + +[TestFixture] +public class UserStateServiceTests +{ + private Mock> _loggerMock; + private UserStateService _userStateService; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock>(); + _userStateService = new UserStateService(_loggerMock.Object); + } + + [Test] + public void SetWaitingForAmount_NewUser_SetsState() + { + // Arrange + var userId = 123L; + var chatId = 456L; + + // Act + _userStateService.SetWaitingForAmount(userId, chatId); + + // Assert + var isWaiting = _userStateService.IsWaitingForAmount(userId, chatId); + Assert.That(isWaiting, Is.True); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("User") && v.ToString().Contains("set to waiting for amount input")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void SetWaitingForAmount_ExistingUser_UpdatesState() + { + // Arrange + var userId = 123L; + _userStateService.SetWaitingForAmount(userId, 456L); + + // Act - обновляем chatId для того же пользователя + _userStateService.SetWaitingForAmount(userId, 789L); + + // Assert + var isWaitingOldChat = _userStateService.IsWaitingForAmount(userId, 456L); + var isWaitingNewChat = _userStateService.IsWaitingForAmount(userId, 789L); + + Assert.That(isWaitingOldChat, Is.False); + Assert.That(isWaitingNewChat, Is.True); + } + + [Test] + public void IsWaitingForAmount_UserNotWaiting_ReturnsFalse() + { + // Arrange + var userId = 123L; + var chatId = 456L; + + // Act + var isWaiting = _userStateService.IsWaitingForAmount(userId, chatId); + + // Assert + Assert.That(isWaiting, Is.False); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Checked waiting for amount status") && v.ToString().Contains("False")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void IsWaitingForAmount_UserWaitingInDifferentChat_ReturnsFalse() + { + // Arrange + var userId = 123L; + _userStateService.SetWaitingForAmount(userId, 456L); + + // Act + var isWaiting = _userStateService.IsWaitingForAmount(userId, 789L); // Другой chatId + + // Assert + Assert.That(isWaiting, Is.False); + } + + [Test] + public void IsWaitingForAmount_UserWaitingInSameChat_ReturnsTrue() + { + // Arrange + var userId = 123L; + var chatId = 456L; + _userStateService.SetWaitingForAmount(userId, chatId); + + // Act + var isWaiting = _userStateService.IsWaitingForAmount(userId, chatId); + + // Assert + Assert.That(isWaiting, Is.True); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Checked waiting for amount status") && v.ToString().Contains("True")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void RemoveWaitingForAmount_ExistingUser_RemovesState() + { + // Arrange + var userId = 123L; + var chatId = 456L; + _userStateService.SetWaitingForAmount(userId, chatId); + + // Act + _userStateService.RemoveWaitingForAmount(userId); + + // Assert + var isWaiting = _userStateService.IsWaitingForAmount(userId, chatId); + Assert.That(isWaiting, Is.False); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Removed user") && v.ToString().Contains("from waiting for amount state")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void RemoveWaitingForAmount_NonExistentUser_LogsDebug() + { + // Arrange + var userId = 999L; // Несуществующий пользователь + + // Act + _userStateService.RemoveWaitingForAmount(userId); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Attempted to remove non-existent waiting state for user")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void GetWaitingUsersCount_NoUsers_ReturnsZero() + { + // Arrange - нет пользователей в состоянии ожидания + + // Act + var count = _userStateService.GetWaitingUsersCount(); + + // Assert + Assert.That(count, Is.EqualTo(0)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Trace, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Current users waiting for amount input: 0")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void GetWaitingUsersCount_MultipleUsers_ReturnsCorrectCount() + { + // Arrange + _userStateService.SetWaitingForAmount(123L, 456L); + _userStateService.SetWaitingForAmount(124L, 457L); + _userStateService.SetWaitingForAmount(125L, 458L); + + // Act + var count = _userStateService.GetWaitingUsersCount(); + + // Assert + Assert.That(count, Is.EqualTo(3)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Trace, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Current users waiting for amount input: 3")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void GetWaitingUsersCount_AfterRemoval_ReturnsUpdatedCount() + { + // Arrange + _userStateService.SetWaitingForAmount(123L, 456L); + _userStateService.SetWaitingForAmount(124L, 457L); + _userStateService.SetWaitingForAmount(125L, 458L); + + // Act - удаляем одного пользователя + _userStateService.RemoveWaitingForAmount(124L); + var count = _userStateService.GetWaitingUsersCount(); + + // Assert + Assert.That(count, Is.EqualTo(2)); + } + + [Test] + public void ClearAllWaitingStates_WithUsers_ClearsAllStates() + { + // Arrange + _userStateService.SetWaitingForAmount(123L, 456L); + _userStateService.SetWaitingForAmount(124L, 457L); + _userStateService.SetWaitingForAmount(125L, 458L); + + // Act + _userStateService.ClearAllWaitingStates(); + + // Assert + var count = _userStateService.GetWaitingUsersCount(); + Assert.That(count, Is.EqualTo(0)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Cleared all waiting states, affected 3 users")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void ClearAllWaitingStates_NoUsers_LogsZero() + { + // Arrange - нет пользователей + + // Act + _userStateService.ClearAllWaitingStates(); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Cleared all waiting states, affected 0 users")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void RemoveMultipleWaitingStates_AllUsersExist_RemovesAll() + { + // Arrange + _userStateService.SetWaitingForAmount(123L, 456L); + _userStateService.SetWaitingForAmount(124L, 457L); + _userStateService.SetWaitingForAmount(125L, 458L); + + var userIdsToRemove = new long[] { 123L, 124L, 125L }; + + // Act + var removedCount = _userStateService.RemoveMultipleWaitingStates(userIdsToRemove); + + // Assert + Assert.That(removedCount, Is.EqualTo(3)); + + var remainingCount = _userStateService.GetWaitingUsersCount(); + Assert.That(remainingCount, Is.EqualTo(0)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Removed waiting states for 3 out of 3 users")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void RemoveMultipleWaitingStates_SomeUsersExist_RemovesOnlyExisting() + { + // Arrange + _userStateService.SetWaitingForAmount(123L, 456L); + _userStateService.SetWaitingForAmount(124L, 457L); + // 125L не добавляли + + var userIdsToRemove = new long[] { 123L, 124L, 125L }; + + // Act + var removedCount = _userStateService.RemoveMultipleWaitingStates(userIdsToRemove); + + // Assert + Assert.That(removedCount, Is.EqualTo(2)); + + var remainingCount = _userStateService.GetWaitingUsersCount(); + Assert.That(remainingCount, Is.EqualTo(0)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Removed waiting states for 2 out of 3 users")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void RemoveMultipleWaitingStates_NoUsersExist_ReturnsZero() + { + // Arrange + var userIdsToRemove = new long[] { 123L, 124L, 125L }; + + // Act + var removedCount = _userStateService.RemoveMultipleWaitingStates(userIdsToRemove); + + // Assert + Assert.That(removedCount, Is.EqualTo(0)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Removed waiting states for 0 out of 3 users")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void RemoveMultipleWaitingStates_EmptyList_ReturnsZero() + { + // Arrange + var userIdsToRemove = Array.Empty(); + + // Act + var removedCount = _userStateService.RemoveMultipleWaitingStates(userIdsToRemove); + + // Assert + Assert.That(removedCount, Is.EqualTo(0)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Removed waiting states for 0 out of 0 users")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void MultipleOperations_ComplexScenario_WorksCorrectly() + { + // Arrange & Act - комплексный сценарий + _userStateService.SetWaitingForAmount(123L, 456L); + _userStateService.SetWaitingForAmount(124L, 457L); + + var countAfterAdd = _userStateService.GetWaitingUsersCount(); + Assert.That(countAfterAdd, Is.EqualTo(2)); + + // Проверяем состояния + var user123Waiting = _userStateService.IsWaitingForAmount(123L, 456L); + var user124Waiting = _userStateService.IsWaitingForAmount(124L, 457L); + Assert.That(user123Waiting, Is.True); + Assert.That(user124Waiting, Is.True); + + // Обновляем состояние одного пользователя + _userStateService.SetWaitingForAmount(123L, 789L); + + var user123OldChat = _userStateService.IsWaitingForAmount(123L, 456L); + var user123NewChat = _userStateService.IsWaitingForAmount(123L, 789L); + Assert.That(user123OldChat, Is.False); + Assert.That(user123NewChat, Is.True); + + // Удаляем одного пользователя + _userStateService.RemoveWaitingForAmount(124L); + + var countAfterRemove = _userStateService.GetWaitingUsersCount(); + Assert.That(countAfterRemove, Is.EqualTo(1)); + + // Очищаем все + _userStateService.ClearAllWaitingStates(); + + var finalCount = _userStateService.GetWaitingUsersCount(); + Assert.That(finalCount, Is.EqualTo(0)); + } + + [Test] + public void IndependentUsers_DoNotInterfere() + { + // Arrange + var user1 = 123L; + var user2 = 124L; + var chat1 = 456L; + var chat2 = 457L; + + // Act + _userStateService.SetWaitingForAmount(user1, chat1); + _userStateService.SetWaitingForAmount(user2, chat2); + + // Assert - проверяем, что состояния независимы + var user1InChat1 = _userStateService.IsWaitingForAmount(user1, chat1); + var user1InChat2 = _userStateService.IsWaitingForAmount(user1, chat2); + var user2InChat1 = _userStateService.IsWaitingForAmount(user2, chat1); + var user2InChat2 = _userStateService.IsWaitingForAmount(user2, chat2); + + Assert.That(user1InChat1, Is.True); + Assert.That(user1InChat2, Is.False); + Assert.That(user2InChat1, Is.False); + Assert.That(user2InChat2, Is.True); + + // Удаляем только user1 + _userStateService.RemoveWaitingForAmount(user1); + + var user1AfterRemove = _userStateService.IsWaitingForAmount(user1, chat1); + var user2AfterRemove = _userStateService.IsWaitingForAmount(user2, chat2); + + Assert.That(user1AfterRemove, Is.False); + Assert.That(user2AfterRemove, Is.True); + } +} \ No newline at end of file diff --git a/TestBot/Bot/UpdateHandlerTests.cs b/TestBot/Bot/UpdateHandlerTests.cs new file mode 100755 index 0000000..afda669 --- /dev/null +++ b/TestBot/Bot/UpdateHandlerTests.cs @@ -0,0 +1,202 @@ +using Bot; +using Bot.Handlers; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Telegram.Bot; +using Telegram.Bot.Exceptions; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegram.Bot.Types.Payments; + +namespace Bot.Tests; + +[TestFixture] +public class UpdateHandlerTests +{ + private Mock> _loggerMock; + private Mock _botClientMock; + private List> _handlerMocks; + private UpdateHandler _updateHandler; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock>(); + _botClientMock = new Mock(); + + // Создаем моки обработчиков + _handlerMocks = new List>(); + + var handlers = new List(); + _updateHandler = new UpdateHandler(_loggerMock.Object, handlers); + } + + private void AddHandler(Mock handlerMock) + { + _handlerMocks.Add(handlerMock); + var handlers = _handlerMocks.Select(m => m.Object).ToList(); + _updateHandler = new UpdateHandler(_loggerMock.Object, handlers); + } + + [Test] + public async Task HandleUpdateAsync_NoHandlers_LogsWarning() + { + // Arrange + var update = new Update { Id = 123 }; + var cancellationToken = new CancellationToken(); + + // Act + await _updateHandler.HandleUpdateAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("No handler found for update")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleUpdateAsync_FirstMatchingHandlerUsed_DoesNotCheckOtherHandlers() + { + // Arrange + var update = new Update { Id = 123, CallbackQuery = new CallbackQuery() }; + var cancellationToken = new CancellationToken(); + + var firstHandlerMock = new Mock(); + firstHandlerMock.Setup(h => h.CanHandle(update)).Returns(true); + firstHandlerMock.Setup(h => h.HandleAsync(_botClientMock.Object, update, cancellationToken)) + .Returns(Task.CompletedTask); + + var secondHandlerMock = new Mock(); + secondHandlerMock.Setup(h => h.CanHandle(update)).Returns(true); // Этот тоже подходит, но не должен быть вызван + + AddHandler(firstHandlerMock); + AddHandler(secondHandlerMock); + + // Act + await _updateHandler.HandleUpdateAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + firstHandlerMock.Verify(h => h.CanHandle(update), Times.Once); + firstHandlerMock.Verify(h => h.HandleAsync(_botClientMock.Object, update, cancellationToken), Times.Once); + secondHandlerMock.Verify(h => h.CanHandle(update), Times.Never); // Не должен проверяться + } + + [Test] + public async Task HandleUpdateAsync_HandlerThrowsException_LogsError() + { + // Arrange + var update = new Update { Id = 123, Message = new Message() }; + var cancellationToken = new CancellationToken(); + var exception = new Exception("Handler failed"); + + var handlerMock = new Mock(); + handlerMock.Setup(h => h.CanHandle(update)).Returns(true); + handlerMock.Setup(h => h.HandleAsync(_botClientMock.Object, update, cancellationToken)) + .ThrowsAsync(exception); + + AddHandler(handlerMock); + + // Act + await _updateHandler.HandleUpdateAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error processing update")), + exception, + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleUpdateAsync_LogsDebugOnReceipt() + { + // Arrange + var update = new Update { Id = 123 }; + var cancellationToken = new CancellationToken(); + + // Act + await _updateHandler.HandleUpdateAsync(_botClientMock.Object, update, cancellationToken); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Received update") && v.ToString().Contains("123")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void Constructor_WithHandlers_LogsHandlerCount() + { + // Arrange + var handlers = new List + { + new Mock().Object, + new Mock().Object, + new Mock().Object + }; + + // Act + var handler = new UpdateHandler(_loggerMock.Object, handlers); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("UpdateHandler initialized with 3 handlers")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task HandleUpdateAsync_MultipleHandlerTypes_RoutesCorrectly() + { + // Arrange + var messageUpdate = new Update { Id = 1, Message = new Message() }; + var callbackUpdate = new Update { Id = 2, CallbackQuery = new CallbackQuery() }; + var preCheckoutUpdate = new Update { Id = 3, PreCheckoutQuery = new PreCheckoutQuery() }; + var cancellationToken = new CancellationToken(); + + var messageHandlerMock = new Mock(); + messageHandlerMock.Setup(h => h.CanHandle(It.Is(u => u.Message != null))).Returns(true); + + var callbackHandlerMock = new Mock(); + callbackHandlerMock.Setup(h => h.CanHandle(It.Is(u => u.CallbackQuery != null))).Returns(true); + + var preCheckoutHandlerMock = new Mock(); + preCheckoutHandlerMock.Setup(h => h.CanHandle(It.Is(u => u.PreCheckoutQuery != null))).Returns(true); + + AddHandler(messageHandlerMock); + AddHandler(callbackHandlerMock); + AddHandler(preCheckoutHandlerMock); + + // Act & Assert - Message update + await _updateHandler.HandleUpdateAsync(_botClientMock.Object, messageUpdate, cancellationToken); + messageHandlerMock.Verify(h => h.HandleAsync(_botClientMock.Object, messageUpdate, cancellationToken), Times.Once); + callbackHandlerMock.Verify(h => h.HandleAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + preCheckoutHandlerMock.Verify(h => h.HandleAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + + // Act & Assert - Callback update + await _updateHandler.HandleUpdateAsync(_botClientMock.Object, callbackUpdate, cancellationToken); + callbackHandlerMock.Verify(h => h.HandleAsync(_botClientMock.Object, callbackUpdate, cancellationToken), Times.Once); + + // Act & Assert - PreCheckout update + await _updateHandler.HandleUpdateAsync(_botClientMock.Object, preCheckoutUpdate, cancellationToken); + preCheckoutHandlerMock.Verify(h => h.HandleAsync(_botClientMock.Object, preCheckoutUpdate, cancellationToken), Times.Once); + } +} \ No newline at end of file diff --git a/TestBot/Data/DonationServiceTests.cs b/TestBot/Data/DonationServiceTests.cs new file mode 100755 index 0000000..1c69294 --- /dev/null +++ b/TestBot/Data/DonationServiceTests.cs @@ -0,0 +1,467 @@ +using Data; +using Data.Models; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Services; + +namespace Services.Tests; + +[TestFixture] +public class DonationServiceTests +{ + private Mock _repositoryMock; + private Mock> _loggerMock; + private DonationService _donationService; + + [SetUp] + public void Setup() + { + _repositoryMock = new Mock(); + _loggerMock = new Mock>(); + _donationService = new DonationService(_repositoryMock.Object, _loggerMock.Object); + } + + [Test] + public async Task GetOrCreateUserAsync_ExistingUser_ReturnsUser() + { + // Arrange + var telegramId = 123L; + var existingUser = new Users { Id = 1, TelegramId = telegramId, Username = "testuser", FirstName = "Test", LastName = "User" }; + + _repositoryMock + .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) + .ReturnsAsync(existingUser); + + // Act + var result = await _donationService.GetOrCreateUserAsync(telegramId, "testuser", "Test", "User"); + + // Assert + Assert.That(result, Is.EqualTo(existingUser)); + _repositoryMock.Verify(x => x.GetUserByTelegramIdAsync(telegramId), Times.Once); + _repositoryMock.Verify(x => x.CreateUserAsync(It.IsAny()), Times.Never); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Found existing user")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task GetOrCreateUserAsync_NewUser_CreatesUser() + { + // Arrange + var telegramId = 123L; + var newUser = new Users + { + Id = 1, + TelegramId = telegramId, + Username = "newuser", + FirstName = "New", + LastName = "User", + Admin = false + }; + + _repositoryMock + .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) + .ReturnsAsync((Users)null); + + _repositoryMock + .Setup(x => x.CreateUserAsync(It.Is(u => + u.TelegramId == telegramId && + u.Username == "newuser" && + u.FirstName == "New" && + u.LastName == "User" && + u.Admin == false))) + .ReturnsAsync(newUser); + + // Act + var result = await _donationService.GetOrCreateUserAsync(telegramId, "newuser", "New", "User"); + + // Assert + Assert.That(result, Is.EqualTo(newUser)); + _repositoryMock.Verify(x => x.GetUserByTelegramIdAsync(telegramId), Times.Once); + _repositoryMock.Verify(x => x.CreateUserAsync(It.IsAny()), Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Creating new user") && v.ToString().Contains("newuser")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task GetOrCreateUserAsync_NullNames_HandlesCorrectly() + { + // Arrange + var telegramId = 123L; + var newUser = new Users { Id = 1, TelegramId = telegramId, Username = null, FirstName = null, LastName = null }; + + _repositoryMock + .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) + .ReturnsAsync((Users)null); + + _repositoryMock + .Setup(x => x.CreateUserAsync(It.Is(u => + u.TelegramId == telegramId && + u.Username == null && + u.FirstName == null && + u.LastName == null))) + .ReturnsAsync(newUser); + + // Act + var result = await _donationService.GetOrCreateUserAsync(telegramId, null, null, null); + + // Assert + Assert.That(result, Is.EqualTo(newUser)); + } + + [Test] + public async Task GetOrCreateUserAsync_RepositoryThrows_LogsErrorAndThrows() + { + // Arrange + var telegramId = 123L; + var exception = new Exception("Database error"); + + _repositoryMock + .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) + .ThrowsAsync(exception); + + // Act & Assert + Assert.ThrowsAsync(() => + _donationService.GetOrCreateUserAsync(telegramId, "testuser", "Test", "User")); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error getting or creating user")), + exception, + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task ProcessDonationAsync_SuccessfulDonation_ReturnsTrue() + { + // Arrange + var userId = 123L; + var amount = 500m; + var currency = "RUB"; + var donationId = "donation_123"; + var goal = new DonationGoal { Id = 1, Title = "Test Goal", TargetAmount = 10000, CurrentAmount = 0 }; + var donation = new Donation { Id = 1, UserTelegramId = userId, GoalId = goal.Id, Amount = amount, Currency = currency, ProviderPaymentId = donationId }; + + _repositoryMock + .Setup(x => x.GetDonationAsync(donationId)) + .ReturnsAsync((Donation)null); + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(goal); + + _repositoryMock + .Setup(x => x.CreateDonationAsync(It.Is(d => + d.UserTelegramId == userId && + d.GoalId == goal.Id && + d.Amount == amount && + d.Currency == currency && + d.ProviderPaymentId == donationId && + d.Status == "completed"))) + .ReturnsAsync(donation); + + _repositoryMock + .Setup(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount)) + .Returns(Task.CompletedTask); + + // Act + var result = await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + + // Assert + Assert.That(result, Is.True); + + _repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); + _repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + _repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount), Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Successfully processed donation")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task ProcessDonationAsync_DuplicateDonation_ReturnsTrue() + { + // Arrange + var userId = 123L; + var amount = 500m; + var goalId = 1; + var currency = "RUB"; + var donationId = "donation_123"; + var existingDonation = new Donation { Id = 1, UserTelegramId = userId, GoalId = goalId, Amount = amount, CreatedAt = DateTime.UtcNow }; + + _repositoryMock + .Setup(x => x.GetDonationAsync(donationId)) + .ReturnsAsync(existingDonation); + + // Act + var result = await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + + // Assert + Assert.That(result, Is.True); + + _repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); + _repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Never); + _repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(It.IsAny(), It.IsAny()), Times.Never); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("has already been processed")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task ProcessDonationAsync_NoActiveGoal_ReturnsFalse() + { + // Arrange + var userId = 123L; + var amount = 500m; + var currency = "RUB"; + var donationId = "donation_123"; + + _repositoryMock + .Setup(x => x.GetDonationAsync(donationId)) + .ReturnsAsync((Donation)null); + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync((DonationGoal)null); + + // Act + var result = await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + + // Assert + Assert.That(result, Is.False); + + _repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); + _repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + _repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(It.IsAny(), It.IsAny()), Times.Never); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("No active goal found for donation")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task ProcessDonationAsync_CreateDonationThrows_ReturnsFalse() + { + // Arrange + var userId = 123L; + var amount = 500m; + var currency = "RUB"; + var donationId = "donation_123"; + var goal = new DonationGoal { Id = 1, Title = "Test Goal" }; + var exception = new Exception("Database error"); + + _repositoryMock + .Setup(x => x.GetDonationAsync(donationId)) + .ReturnsAsync((Donation)null); + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(goal); + + _repositoryMock + .Setup(x => x.CreateDonationAsync(It.IsAny())) + .ThrowsAsync(exception); + + // Act + var result = await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + + // Assert + Assert.That(result, Is.False); + + _repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); + _repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + _repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(It.IsAny(), It.IsAny()), Times.Never); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error processing donation")), + exception, + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task ProcessDonationAsync_UpdateGoalThrows_ReturnsFalse() + { + // Arrange + var userId = 123L; + var amount = 500m; + var currency = "RUB"; + var donationId = "donation_123"; + var goal = new DonationGoal { Id = 1, Title = "Test Goal" }; + var donation = new Donation { Id = 1, UserTelegramId = userId, GoalId = goal.Id, Amount = amount }; + var exception = new Exception("Database error"); + + _repositoryMock + .Setup(x => x.GetDonationAsync(donationId)) + .ReturnsAsync((Donation)null); + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(goal); + + _repositoryMock + .Setup(x => x.CreateDonationAsync(It.IsAny())) + .ReturnsAsync(donation); + + _repositoryMock + .Setup(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount)) + .ThrowsAsync(exception); + + // Act + var result = await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + + // Assert + Assert.That(result, Is.False); + + _repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); + _repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + _repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount), Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error processing donation")), + exception, + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task ProcessDonationAsync_ValidatesDonationStatus() + { + // Arrange + var userId = 123L; + var amount = 500m; + var currency = "RUB"; + var donationId = "donation_123"; + var goal = new DonationGoal { Id = 1, Title = "Test Goal" }; + var donation = new Donation { Id = 1, UserTelegramId = userId, GoalId = goal.Id, Amount = amount }; + + _repositoryMock + .Setup(x => x.GetDonationAsync(donationId)) + .ReturnsAsync((Donation)null); + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(goal); + + _repositoryMock + .Setup(x => x.CreateDonationAsync(It.Is(d => d.Status == "completed"))) + .ReturnsAsync(donation); + + _repositoryMock + .Setup(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount)) + .Returns(Task.CompletedTask); + + // Act + var result = await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + + // Assert + Assert.That(result, Is.True); + + // Проверяем, что статус доната установлен в "completed" + _repositoryMock.Verify(x => x.CreateDonationAsync(It.Is(d => d.Status == "completed")), Times.Once); + } + + [Test] + public async Task ProcessDonationAsync_LogsAppropriateMessages() + { + // Arrange + var userId = 123L; + var amount = 500m; + var currency = "RUB"; + var donationId = "donation_123"; + var goal = new DonationGoal { Id = 1, Title = "Test Goal" }; + var donation = new Donation { Id = 1, UserTelegramId = userId, GoalId = goal.Id, Amount = amount }; + + _repositoryMock + .Setup(x => x.GetDonationAsync(donationId)) + .ReturnsAsync((Donation)null); + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(goal); + + _repositoryMock + .Setup(x => x.CreateDonationAsync(It.IsAny())) + .ReturnsAsync(donation); + + _repositoryMock + .Setup(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount)) + .Returns(Task.CompletedTask); + + // Act + await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + + // Assert - проверяем последовательность логирования + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Processing donation")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Found active goal")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Created donation record")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } +} \ No newline at end of file diff --git a/TestBot/Data/GoalServiceTests.cs b/TestBot/Data/GoalServiceTests.cs new file mode 100755 index 0000000..11a6aad --- /dev/null +++ b/TestBot/Data/GoalServiceTests.cs @@ -0,0 +1,563 @@ +using Data; +using Data.Models; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Services; + +namespace Services.Tests; + +[TestFixture] +public class GoalServiceTests +{ + private Mock _repositoryMock; + private Mock> _loggerMock; + private GoalService _goalService; + + [SetUp] + public void Setup() + { + _repositoryMock = new Mock(); + _loggerMock = new Mock>(); + _goalService = new GoalService(_repositoryMock.Object, _loggerMock.Object); + } + + [Test] + public async Task IsUserAdminAsync_AdminUser_ReturnsTrue() + { + // Arrange + var telegramId = 123L; + var adminUser = new Users { Id = 1, TelegramId = telegramId, Admin = true }; + + _repositoryMock + .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) + .ReturnsAsync(adminUser); + + // Act + var result = await _goalService.IsUserAdminAsync(telegramId); + + // Assert + Assert.That(result, Is.True); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("User") && v.ToString().Contains("admin status: True")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task IsUserAdminAsync_NonAdminUser_ReturnsFalse() + { + // Arrange + var telegramId = 123L; + var regularUser = new Users { Id = 1, TelegramId = telegramId, Admin = false }; + + _repositoryMock + .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) + .ReturnsAsync(regularUser); + + // Act + var result = await _goalService.IsUserAdminAsync(telegramId); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public async Task IsUserAdminAsync_UserNotFound_ReturnsFalse() + { + // Arrange + var telegramId = 999L; + + _repositoryMock + .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) + .ReturnsAsync((Users)null); + + // Act + var result = await _goalService.IsUserAdminAsync(telegramId); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public async Task IsUserAdminAsync_RepositoryThrows_ReturnsFalseAndLogsError() + { + // Arrange + var telegramId = 123L; + var exception = new Exception("Database error"); + + _repositoryMock + .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) + .ThrowsAsync(exception); + + // Act + var result = await _goalService.IsUserAdminAsync(telegramId); + + // Assert + Assert.That(result, Is.False); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error checking admin status")), + exception, + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task GetActiveGoalAsync_GoalExists_ReturnsGoal() + { + // Arrange + var goal = new DonationGoal { Id = 1, Title = "Test Goal", TargetAmount = 10000, CurrentAmount = 5000 }; + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(goal); + + // Act + var result = await _goalService.GetActiveGoalAsync(); + + // Assert + Assert.That(result, Is.EqualTo(goal)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Active goal retrieval succeeded")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task GetActiveGoalAsync_NoGoal_ReturnsNull() + { + // Arrange + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync((DonationGoal)null); + + // Act + var result = await _goalService.GetActiveGoalAsync(); + + // Assert + Assert.That(result, Is.Null); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Active goal retrieval failed - no active goal")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task GetActiveGoalAsync_RepositoryThrows_ReturnsNullAndLogsError() + { + // Arrange + var exception = new Exception("Database error"); + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ThrowsAsync(exception); + + // Act + var result = await _goalService.GetActiveGoalAsync(); + + // Assert + Assert.That(result, Is.Null); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error retrieving active goal")), + exception, + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task GetGoalStatsAsync_WithActiveGoal_ReturnsFormattedStats() + { + // Arrange + var goal = new DonationGoal + { + Id = 1, + Title = "Test Goal", + Description = "Test Description", + TargetAmount = 10000, + CurrentAmount = 5000, + CreatedAt = new DateTime(2024, 1, 1) + }; + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(goal); + + _repositoryMock + .Setup(x => x.GetCountUsersForActiveGoals()) + .ReturnsAsync(25); + + _repositoryMock + .Setup(x => x.GetCountDonationsForActiveGoals()) + .ReturnsAsync(50); + + // Act + var result = await _goalService.GetGoalStatsAsync(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Contains.Substring("Test Goal")); + Assert.That(result, Contains.Substring("Test Description")); + Assert.That(result, Contains.Substring("10\u00A0000₽")); + Assert.That(result, Contains.Substring("5\u00A0000₽")); + Assert.That(result, Contains.Substring("50,0%")); + Assert.That(result, Contains.Substring("25")); + Assert.That(result, Contains.Substring("50")); + Assert.That(result, Contains.Substring("01.01.2024")); + Assert.That(result, Contains.Substring("[■■■■■□□□□□]")); // 50% progress bar + + _repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + _repositoryMock.Verify(x => x.GetCountUsersForActiveGoals(), Times.Once); + _repositoryMock.Verify(x => x.GetCountDonationsForActiveGoals(), Times.Once); + } + + [Test] + public async Task GetGoalStatsAsync_NoActiveGoal_ReturnsNoActiveGoalMessage() + { + // Arrange + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync((DonationGoal)null); + + // Act + var result = await _goalService.GetGoalStatsAsync(); + + // Assert + Assert.That(result, Is.EqualTo("🎯 На данный момент нет активных целей для сбора.")); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("No active goal found for statistics")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task GetGoalStatsAsync_ZeroTargetAmount_HandlesCorrectly() + { + // Arrange + var goal = new DonationGoal + { + Id = 1, + Title = "Test Goal", + TargetAmount = 0, + CurrentAmount = 5000 + }; + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(goal); + + _repositoryMock + .Setup(x => x.GetCountUsersForActiveGoals()) + .ReturnsAsync(10); + + _repositoryMock + .Setup(x => x.GetCountDonationsForActiveGoals()) + .ReturnsAsync(20); + + // Act + var result = await _goalService.GetGoalStatsAsync(); + + // Assert + Assert.That(result, Is.Not.Null); + var today = DateTime.Now.ToString("dd.MM.yyyy"); + Assert.That(result, Contains.Substring( + $"🎯 **Test Goal** — 0₽ \n" + + $"📝 Описание: \n\n" + + $"📈 Количество пожертвований на текущую цель: 20\n" + + $"🧮 Количество пожертвовавших: 10 \n" + + $"⏳ Дата открытия сбора: {today}\n\n" + + $"Собрано: 5\u00A0000₽ (0,0%) \n" + + $"[□□□□□□□□□□]")); + } + + [Test] + public async Task GetGoalStatsAsync_FullProgress_ShowsFullProgressBar() + { + // Arrange + var goal = new DonationGoal + { + Id = 1, + Title = "Test Goal", + TargetAmount = 1000, + CurrentAmount = 1000 + }; + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(goal); + + _repositoryMock + .Setup(x => x.GetCountUsersForActiveGoals()) + .ReturnsAsync(10); + + _repositoryMock + .Setup(x => x.GetCountDonationsForActiveGoals()) + .ReturnsAsync(20); + + // Act + var result = await _goalService.GetGoalStatsAsync(); + + // Assert + Assert.That(result, Contains.Substring("[■■■■■■■■■■]")); // 100% progress bar + Assert.That(result, Contains.Substring("100,0%")); + } + + [Test] + public async Task GetGoalStatsAsync_RepositoryThrows_ReturnsErrorMessage() + { + // Arrange + var exception = new Exception("Database error"); + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ThrowsAsync(exception); + + // Act + var result = await _goalService.GetGoalStatsAsync(); + + // Assert + Assert.That(result, Is.EqualTo("🎯 На данный момент нет активных целей для сбора.")); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error retrieving active goal")), + exception, + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task GetStartStats_WithActiveGoal_ReturnsFormattedStats() + { + // Arrange + var goal = new DonationGoal + { + Id = 1, + Title = "Test Goal", + Description = "Test Description", + TargetAmount = 10000, + CurrentAmount = 7500 + }; + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(goal); + + // Act + var result = await _goalService.GetStartStats(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Contains.Substring("Test Goal")); + Assert.That(result, Contains.Substring("Test Description")); + Assert.That(result, Contains.Substring("10\u00A0000₽")); + Assert.That(result, Contains.Substring("7\u00A0500₽")); + Assert.That(result, Contains.Substring("75,0%")); + Assert.That(result, Contains.Substring("[■■■■■■■■□□]")); // 75% progress bar (rounded to 8 blocks out of 10) + } + + [Test] + public async Task GetStartStats_NoActiveGoal_ReturnsNoActiveGoalMessage() + { + // Arrange + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync((DonationGoal)null); + + // Act + var result = await _goalService.GetStartStats(); + + // Assert + Assert.That(result, Is.EqualTo("🎯 На данный момент нет активных целей для сбора.")); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("No active goal found for start statistics")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task GetStartStats_RepositoryThrows_ReturnsErrorMessage() + { + // Arrange + var exception = new Exception("Database error"); + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ThrowsAsync(exception); + + // Act + var result = await _goalService.GetStartStats(); + + // Assert + Assert.That(result, Is.EqualTo("🎯 На данный момент нет активных целей для сбора.")); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error retrieving active goal")), + exception, + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public async Task CreateGoalAsync_ValidGoal_CreatesAndReturnsGoal() + { + // Arrange + var title = "New Goal"; + var description = "New Description"; + var targetAmount = 5000m; + var createdGoal = new DonationGoal { Id = 1, Title = title, Description = description, TargetAmount = targetAmount }; + + _repositoryMock + .Setup(x => x.CreateGoalAsync(It.Is(g => + g.Title == title && + g.Description == description && + g.TargetAmount == targetAmount && + g.IsActive == true))) + .ReturnsAsync(createdGoal); + + // Act + var result = await _goalService.CreateGoalAsync(title, description, targetAmount); + + // Assert + Assert.That(result, Is.EqualTo(createdGoal)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Goal created successfully") && v.ToString().Contains(title)), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void CreateGoalAsync_RepositoryThrows_LogsErrorAndThrows() + { + // Arrange + var title = "New Goal"; + var description = "New Description"; + var targetAmount = 5000m; + var exception = new Exception("Database error"); + + _repositoryMock + .Setup(x => x.CreateGoalAsync(It.IsAny())) + .ThrowsAsync(exception); + + // Act & Assert + Assert.ThrowsAsync(() => + _goalService.CreateGoalAsync(title, description, targetAmount)); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Error creating goal") && v.ToString().Contains(title)), + exception, + It.Is>((v, t) => true)), + Times.Once); + } + + [Test] + public void CreateProgressBar_VariousPercentages_ReturnsCorrectBars() + { + // Arrange + var service = new GoalService(_repositoryMock.Object, _loggerMock.Object); + + // Use reflection to test private method + var method = typeof(GoalService).GetMethod("CreateProgressBar", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + // Act & Assert + var result0 = method?.Invoke(service, new object[] { 0.0 }) as string; + var result25 = method?.Invoke(service, new object[] { 25.0 }) as string; + var result50 = method?.Invoke(service, new object[] { 50.0 }) as string; + var result75 = method?.Invoke(service, new object[] { 75.0 }) as string; + var result99 = method?.Invoke(service, new object[] { 99.0 }) as string; + var result100 = method?.Invoke(service, new object[] { 100.0 }) as string; + var result110 = method?.Invoke(service, new object[] { 110.0 }) as string; + + Assert.That(result0, Is.EqualTo("[□□□□□□□□□□]")); + Assert.That(result25, Is.EqualTo("[■■□□□□□□□□]")); + Assert.That(result50, Is.EqualTo("[■■■■■□□□□□]")); + Assert.That(result75, Is.EqualTo("[■■■■■■■■□□]")); + Assert.That(result99, Is.EqualTo("[■■■■■■■■■■]")); // 99% rounds up to full bar + Assert.That(result100, Is.EqualTo("[■■■■■■■■■■]")); + Assert.That(result110, Is.EqualTo("[■■■■■■■■■■]")); // Over 100% still shows full bar + } + + [Test] + public async Task GetGoalStatsAsync_PartialProgressBar_RoundsCorrectly() + { + // Arrange + var goal = new DonationGoal + { + Id = 1, + Title = "Test Goal", + TargetAmount = 1000, + CurrentAmount = 123 // 12.3% + }; + + _repositoryMock + .Setup(x => x.GetActiveGoalAsync()) + .ReturnsAsync(goal); + + _repositoryMock + .Setup(x => x.GetCountUsersForActiveGoals()) + .ReturnsAsync(5); + + _repositoryMock + .Setup(x => x.GetCountDonationsForActiveGoals()) + .ReturnsAsync(10); + + // Act + var result = await _goalService.GetGoalStatsAsync(); + + var today = DateTime.Now.ToString("dd.MM.yyyy"); + Assert.That(result, Contains.Substring( + $"🎯 **Test Goal** — 1 000₽ \n" + + $"📝 Описание: \n\n" + + $"📈 Количество пожертвований на текущую цель: 10\n" + + $"🧮 Количество пожертвовавших: 5 \n" + + $"⏳ Дата открытия сбора: {today}\n\n" + + $"Собрано: 123₽ (12,3%) \n" + + $"[■□□□□□□□□□]")); + } +} \ No newline at end of file diff --git a/TestBot/TestBot.csproj b/TestBot/TestBot.csproj new file mode 100755 index 0000000..f25913a --- /dev/null +++ b/TestBot/TestBot.csproj @@ -0,0 +1,35 @@ + + + + net9.0 + latest + enable + enable + false + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100755 index 0000000..a2723f6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +#version: '3.8' + +services: + postgres: + image: postgres:17 + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./Data/Scripts/create_tables.sql:/docker-entrypoint-initdb.d/01-create-tables.sql + # настрой /var/lib/postgresql/data/pg_hba.conf под себя + ports: + - "5432:5432" + networks: + - donationbot-network + + bot: + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + environment: + - ASPNETCORE_ENVIRONMENT=Production + - BotConfig__BotToken=${BotConfig__BotToken} + - BotConfig__PaymentProviderToken=${BotConfig__PaymentProviderToken} + - DatabaseConfig__ConnectionString=${DatabaseConfig__ConnectionString} + depends_on: + - postgres + networks: + - donationbot-network + +networks: + donationbot-network: + +volumes: + postgres_data: From 020b2e83e109b4536ec09acf5fa84c51d816cfed Mon Sep 17 00:00:00 2001 From: kirill Date: Sat, 3 Jan 2026 03:57:06 +0300 Subject: [PATCH 2/2] Fixing StyleCop Warnings and problems with regional settings --- .editorconfig | 189 ++++++++ Bot/Hadlers/AdminHandler.cs | 6 +- Bot/Hadlers/CallbackQueryHandler.cs | 6 +- Bot/Hadlers/CommandHandler.cs | 6 +- Bot/Hadlers/IUpdateHandlerCommand.cs | 6 +- Bot/Hadlers/MessageHandler.cs | 6 +- Bot/Hadlers/PaymentHandler.cs | 12 +- Bot/Hadlers/PreCheckoutQueryHandler.cs | 13 +- Bot/IReceiverService.cs | 6 +- Bot/PollingService.cs | 34 +- Bot/Program.cs | 6 +- Bot/ReceiverService.cs | 6 +- Bot/Services/AdminGoalStep.cs | 6 +- Bot/Services/AdminStateService.cs | 68 +-- Bot/Services/KeyboardService.cs | 6 +- Bot/Services/UserStateService.cs | 6 +- Bot/UpdateHandler.cs | 10 +- Configurations/BotConfig.cs | 21 +- Configurations/DatabaseConfig.cs | 18 +- Data/DapperRepository.cs | 104 ++++- Data/IDapperRepository.cs | 6 +- Data/Models/Donation.cs | 59 ++- Data/Models/DonationGoal.cs | 51 ++- Data/Models/Users.cs | 30 +- Services/DonationService.cs | 73 ++- Services/GoalService.cs | 120 ++--- Services/IDonationService.cs | 6 +- Services/IGoalService.cs | 6 +- TestBot/Bot/Bot.Hadler/AdminHandlerTests.cs | 277 +++++++----- .../Bot.Hadler/CallbackQueryHandlerTests.cs | 221 ++++++---- TestBot/Bot/Bot.Hadler/CommandHandlerTests.cs | 395 ++++++++++------- TestBot/Bot/Bot.Hadler/MessageHandlerTests.cs | 320 ++++++++------ TestBot/Bot/Bot.Hadler/PaymentHandlerTests.cs | 327 ++++++++------ .../PreCheckoutQueryHandlerTests.cs | 184 ++++---- .../Bot/Bot.Service/AdminStateServiceTests.cs | 324 ++++++++------ .../Bot/Bot.Service/UserStateServiceTests.cs | 339 ++++++++------ TestBot/Bot/UpdateHandlerTests.cs | 417 ++++++++++-------- TestBot/Data/DonationServiceTests.cs | 319 ++++++++------ TestBot/Data/GoalServiceTests.cs | 338 +++++++++----- 39 files changed, 2800 insertions(+), 1547 deletions(-) diff --git a/.editorconfig b/.editorconfig index 2410a5b..68d26a1 100755 --- a/.editorconfig +++ b/.editorconfig @@ -2,3 +2,192 @@ # SA1101: Prefix local calls with this dotnet_diagnostic.SA1101.severity = none +[*.cs] +#### Стили именования #### + +# Правила именования + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Спецификации символов + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Стили именования + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +csharp_indent_labels = one_less_than_current +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_implicitly_typed_lambda_expression = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_space_around_binary_operators = before_and_after +dotnet_diagnostic.SA1200.severity = none + +# CA1848: Использовать делегаты LoggerMessage +dotnet_diagnostic.CA1848.severity = none +dotnet_diagnostic.SA1010.severity = none +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_elsewhere = false:silent +csharp_style_var_when_type_is_apparent = false:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion +csharp_using_directive_placement = outside_namespace:silent +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_static_anonymous_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent + +[*.vb] +#### Стили именования #### + +# Правила именования + +dotnet_naming_rule.interface_should_be_начинается_с_i.severity = suggestion +dotnet_naming_rule.interface_should_be_начинается_с_i.symbols = interface +dotnet_naming_rule.interface_should_be_начинается_с_i.style = начинается_с_i + +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.severity = suggestion +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.symbols = типы +dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.style = всечастиспрописнойбуквы + +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.severity = suggestion +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.symbols = не_являющиеся_полем_члены +dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.style = всечастиспрописнойбуквы + +# Спецификации символов + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.типы.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.типы.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.типы.required_modifiers = + +dotnet_naming_symbols.не_являющиеся_полем_члены.applicable_kinds = property, event, method +dotnet_naming_symbols.не_являющиеся_полем_члены.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.не_являющиеся_полем_члены.required_modifiers = + +# Стили именования + +dotnet_naming_style.начинается_с_i.required_prefix = I +dotnet_naming_style.начинается_с_i.required_suffix = +dotnet_naming_style.начинается_с_i.word_separator = +dotnet_naming_style.начинается_с_i.capitalization = pascal_case + +dotnet_naming_style.всечастиспрописнойбуквы.required_prefix = +dotnet_naming_style.всечастиспрописнойбуквы.required_suffix = +dotnet_naming_style.всечастиспрописнойбуквы.word_separator = +dotnet_naming_style.всечастиспрописнойбуквы.capitalization = pascal_case + +dotnet_naming_style.всечастиспрописнойбуквы.required_prefix = +dotnet_naming_style.всечастиспрописнойбуквы.required_suffix = +dotnet_naming_style.всечастиспрописнойбуквы.word_separator = +dotnet_naming_style.всечастиспрописнойбуквы.capitalization = pascal_case + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_namespace_match_folder = true:suggestion +end_of_line = crlf +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_event = false:silent +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_style_readonly_field = true:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent \ No newline at end of file diff --git a/Bot/Hadlers/AdminHandler.cs b/Bot/Hadlers/AdminHandler.cs index bb7fe0d..35fcbc4 100755 --- a/Bot/Hadlers/AdminHandler.cs +++ b/Bot/Hadlers/AdminHandler.cs @@ -1,4 +1,8 @@ -using Bot.Services; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot.Services; using Microsoft.Extensions.Logging; using Services; using Telegram.Bot; diff --git a/Bot/Hadlers/CallbackQueryHandler.cs b/Bot/Hadlers/CallbackQueryHandler.cs index b2e9cf8..d90d9e3 100755 --- a/Bot/Hadlers/CallbackQueryHandler.cs +++ b/Bot/Hadlers/CallbackQueryHandler.cs @@ -1,4 +1,8 @@ -using Bot.Services; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot.Services; using Microsoft.Extensions.Logging; using Services; using Telegram.Bot; diff --git a/Bot/Hadlers/CommandHandler.cs b/Bot/Hadlers/CommandHandler.cs index bf752e9..ca666ca 100755 --- a/Bot/Hadlers/CommandHandler.cs +++ b/Bot/Hadlers/CommandHandler.cs @@ -1,4 +1,8 @@ -using Bot.Services; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot.Services; using Data.Models; using Microsoft.Extensions.Logging; using Services; diff --git a/Bot/Hadlers/IUpdateHandlerCommand.cs b/Bot/Hadlers/IUpdateHandlerCommand.cs index 6a89916..5b9abf8 100755 --- a/Bot/Hadlers/IUpdateHandlerCommand.cs +++ b/Bot/Hadlers/IUpdateHandlerCommand.cs @@ -1,4 +1,8 @@ -using Telegram.Bot; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Telegram.Bot; using Telegram.Bot.Types; namespace Bot.Handlers; diff --git a/Bot/Hadlers/MessageHandler.cs b/Bot/Hadlers/MessageHandler.cs index 31a9584..ec0afc0 100755 --- a/Bot/Hadlers/MessageHandler.cs +++ b/Bot/Hadlers/MessageHandler.cs @@ -1,4 +1,8 @@ -using Bot.Services; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot.Services; using Data.Models; using Microsoft.Extensions.Logging; using Services; diff --git a/Bot/Hadlers/PaymentHandler.cs b/Bot/Hadlers/PaymentHandler.cs index 94304ec..de91005 100755 --- a/Bot/Hadlers/PaymentHandler.cs +++ b/Bot/Hadlers/PaymentHandler.cs @@ -1,4 +1,8 @@ -using Bot.Services; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot.Services; using Configurations; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -15,15 +19,15 @@ namespace Bot.Handlers; /// public class PaymentHandler { + private const int MinimumDonationAmount = 60; + private const int MaximumDonationAmount = 100000; + private readonly ILogger logger; private readonly IGoalService goalService; private readonly IDonationService donationService; private readonly UserStateService userStateService; private readonly BotConfig botConfig; - private const int MinimumDonationAmount = 60; - private const int MaximumDonationAmount = 100000; - /// /// Initializes a new instance of the class. /// diff --git a/Bot/Hadlers/PreCheckoutQueryHandler.cs b/Bot/Hadlers/PreCheckoutQueryHandler.cs index eb742bd..9c1e37a 100755 --- a/Bot/Hadlers/PreCheckoutQueryHandler.cs +++ b/Bot/Hadlers/PreCheckoutQueryHandler.cs @@ -1,4 +1,8 @@ -using Bot.Services; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot.Services; using Microsoft.Extensions.Logging; using Services; using Telegram.Bot; @@ -68,8 +72,11 @@ await botClient.AnswerPreCheckoutQuery( cancellationToken: cancellationToken); logger.LogInformation( - "Approved pre-checkout query for user {UserId}, payload: {InvoicePayload}, amount: {Amount} {Currency}", - preCheckoutQuery.From.Id, preCheckoutQuery.InvoicePayload, preCheckoutQuery.TotalAmount / 100m, preCheckoutQuery.Currency); + "Approved pre-checkout query for user {UserId}, payload: {InvoicePayload}, amount: {Amount} {Currency}", + preCheckoutQuery.From.Id, + preCheckoutQuery.InvoicePayload, + preCheckoutQuery.TotalAmount / 100m, + preCheckoutQuery.Currency); } else { diff --git a/Bot/IReceiverService.cs b/Bot/IReceiverService.cs index 01c0f83..e499c19 100755 --- a/Bot/IReceiverService.cs +++ b/Bot/IReceiverService.cs @@ -1,4 +1,8 @@ -using System; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/Bot/PollingService.cs b/Bot/PollingService.cs index 272690b..aa0cc45 100755 --- a/Bot/PollingService.cs +++ b/Bot/PollingService.cs @@ -1,4 +1,8 @@ -using Microsoft.Extensions.DependencyInjection; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Telegram.Bot; @@ -28,6 +32,20 @@ public PollingService(IServiceProvider serviceProvider, ILogger this.logger = logger; } + /// + /// Triggered when the application host is performing a graceful shutdown. + /// + /// Cancellation token indicating that shutdown should be no longer graceful. + /// A representing the asynchronous operation. + public override async Task StopAsync(CancellationToken cancellationToken) + { + logger.LogInformation("Polling service is stopping gracefully"); + + await base.StopAsync(cancellationToken); + + logger.LogInformation("Polling service stopped gracefully"); + } + /// /// Executes the polling service as a background task. /// @@ -107,18 +125,4 @@ private async Task HandlePollingErrorAsync(Exception ex, CancellationToken stopp logger.LogDebug("Cooldown delay was cancelled"); } } - - /// - /// Triggered when the application host is performing a graceful shutdown. - /// - /// Cancellation token indicating that shutdown should be no longer graceful. - /// A representing the asynchronous operation. - public override async Task StopAsync(CancellationToken cancellationToken) - { - logger.LogInformation("Polling service is stopping gracefully"); - - await base.StopAsync(cancellationToken); - - logger.LogInformation("Polling service stopped gracefully"); - } } \ No newline at end of file diff --git a/Bot/Program.cs b/Bot/Program.cs index ee6b5d3..813ab9b 100755 --- a/Bot/Program.cs +++ b/Bot/Program.cs @@ -1,4 +1,8 @@ -using Bot; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot; using Bot.Handlers; using Bot.Services; using Configurations; diff --git a/Bot/ReceiverService.cs b/Bot/ReceiverService.cs index 4fba2fb..b5f936b 100755 --- a/Bot/ReceiverService.cs +++ b/Bot/ReceiverService.cs @@ -1,4 +1,8 @@ -using Microsoft.Extensions.Logging; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Microsoft.Extensions.Logging; using Telegram.Bot; using Telegram.Bot.Polling; using Telegram.Bot.Types.Enums; diff --git a/Bot/Services/AdminGoalStep.cs b/Bot/Services/AdminGoalStep.cs index 5742ed4..fbf88f5 100755 --- a/Bot/Services/AdminGoalStep.cs +++ b/Bot/Services/AdminGoalStep.cs @@ -1,4 +1,8 @@ -namespace Bot.Services; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace Bot.Services; public partial class AdminStateService { diff --git a/Bot/Services/AdminStateService.cs b/Bot/Services/AdminStateService.cs index 75bb4e5..402e170 100755 --- a/Bot/Services/AdminStateService.cs +++ b/Bot/Services/AdminStateService.cs @@ -1,4 +1,8 @@ -using Microsoft.Extensions.Logging; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Microsoft.Extensions.Logging; namespace Bot.Services; @@ -10,37 +14,6 @@ public partial class AdminStateService private readonly ILogger logger; private readonly Dictionary adminStates = []; - /// - /// Represents the state of an admin user during goal creation process. - /// - public class AdminGoalCreationState - { - /// - /// Gets or sets the chat identifier where goal creation is taking place. - /// - public long ChatId { get; set; } - - /// - /// Gets or sets the goal title. - /// - public string? Title { get; set; } - - /// - /// Gets or sets the goal description. - /// - public string? Description { get; set; } - - /// - /// Gets or sets the target amount for the goal. - /// - public decimal? TargetAmount { get; set; } - - /// - /// Gets or sets the current step in the goal creation process. - /// - public AdminGoalStep CurrentStep { get; set; } = AdminGoalStep.None; - } - /// /// Initializes a new instance of the class. /// @@ -187,4 +160,35 @@ public int GetActiveStateCount() logger.LogTrace("Current active admin states: {Count}", count); return count; } + + /// + /// Represents the state of an admin user during goal creation process. + /// + public class AdminGoalCreationState + { + /// + /// Gets or sets the chat identifier where goal creation is taking place. + /// + public long ChatId { get; set; } + + /// + /// Gets or sets the goal title. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the goal description. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the target amount for the goal. + /// + public decimal? TargetAmount { get; set; } + + /// + /// Gets or sets the current step in the goal creation process. + /// + public AdminGoalStep CurrentStep { get; set; } = AdminGoalStep.None; + } } \ No newline at end of file diff --git a/Bot/Services/KeyboardService.cs b/Bot/Services/KeyboardService.cs index 247c8ba..f2d91e3 100755 --- a/Bot/Services/KeyboardService.cs +++ b/Bot/Services/KeyboardService.cs @@ -1,4 +1,8 @@ -using Microsoft.Extensions.Logging; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Microsoft.Extensions.Logging; using Telegram.Bot.Types.ReplyMarkups; namespace Bot.Services; diff --git a/Bot/Services/UserStateService.cs b/Bot/Services/UserStateService.cs index 7350200..d69abf0 100755 --- a/Bot/Services/UserStateService.cs +++ b/Bot/Services/UserStateService.cs @@ -1,4 +1,8 @@ -using Microsoft.Extensions.Logging; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Microsoft.Extensions.Logging; namespace Bot.Services; diff --git a/Bot/UpdateHandler.cs b/Bot/UpdateHandler.cs index 2e78f0c..22acd4f 100755 --- a/Bot/UpdateHandler.cs +++ b/Bot/UpdateHandler.cs @@ -1,4 +1,8 @@ -using Bot.Handlers; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot.Handlers; using Microsoft.Extensions.Logging; using Telegram.Bot; using Telegram.Bot.Exceptions; @@ -81,9 +85,7 @@ RequestException requestException _ => exception.Message }; - logger.LogError( - exception, - "Error occurred from source {ErrorSource}. Message: {ErrorMessage}", errorSource, errorMessage); + logger.LogError(exception, "Error occurred from source {ErrorSource}. Message: {ErrorMessage}", errorSource, errorMessage); // Implement retry delay for network-related errors if (exception is HttpRequestException or RequestException) diff --git a/Configurations/BotConfig.cs b/Configurations/BotConfig.cs index 4e63ea9..f13e1e4 100755 --- a/Configurations/BotConfig.cs +++ b/Configurations/BotConfig.cs @@ -1,8 +1,27 @@ -namespace Configurations; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// +namespace Configurations; + +/// +/// Represents the configuration settings for the bot application. +/// public class BotConfig { + /// + /// Gets or sets the bot token used for authenticating with the Telegram Bot API. + /// + /// + /// The bot token string. This should be kept secure and not exposed publicly. + /// public string BotToken { get; set; } = string.Empty; + /// + /// Gets or sets the payment provider token for processing donations. + /// + /// + /// The payment provider token string. Used for integrating with payment systems. + /// public string PaymentProviderToken { get; set; } = string.Empty; } diff --git a/Configurations/DatabaseConfig.cs b/Configurations/DatabaseConfig.cs index de59865..e8ab896 100755 --- a/Configurations/DatabaseConfig.cs +++ b/Configurations/DatabaseConfig.cs @@ -1,4 +1,8 @@ -using System; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -6,7 +10,19 @@ namespace Configurations; +/// +/// Represents database configuration settings for the application. +/// public class DatabaseConfig { + /// + /// Gets or sets the connection string used to connect to the database. + /// + /// + /// The connection string containing server, database, and authentication information. + /// + /// + /// Server=localhost;Database=MyDb;UserId=myuser;Password=mypassword;. + /// public string ConnectionString { get; set; } = string.Empty; } diff --git a/Data/DapperRepository.cs b/Data/DapperRepository.cs index 5651106..efcf7d8 100755 --- a/Data/DapperRepository.cs +++ b/Data/DapperRepository.cs @@ -1,17 +1,30 @@ -using Data.Models; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Configurations; +using Dapper; +using Data.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Npgsql; -using Dapper; -using Configurations; namespace Data; +/// +/// Provides data access operations for the application using Dapper micro-ORM. +/// Implements the interface. +/// public class DapperRepository : IDapperRepository { private readonly string connectionString; private readonly ILogger logger; + /// + /// Initializes a new instance of the class. + /// + /// Database configuration containing the connection string. + /// Logger instance for tracking operations. public DapperRepository(IOptions config, ILogger logger) { connectionString = config.Value.ConnectionString; @@ -19,6 +32,13 @@ public DapperRepository(IOptions config, ILogger + /// Retrieves a user by their Telegram identifier. + /// + /// The Telegram user identifier to search for. + /// + /// A object if found; otherwise, null. + /// public async Task GetUserByTelegramIdAsync(long telegramId) { logger.LogDebug("Retrieving user by Telegram ID: {TelegramId}", telegramId); @@ -30,8 +50,7 @@ public DapperRepository(IOptions config, ILogger config, ILogger + /// Creates a new user in the database. + /// + /// The user object containing the data to insert. + /// + /// The created object with the generated identifier. + /// public async Task CreateUserAsync(Users user) { logger.LogDebug("Creating user with Telegram ID: {TelegramId}", user.TelegramId); @@ -55,8 +81,8 @@ INSERT INTO users (telegram_id, username, first_name, last_name) RETURNING *"; var createdUser = await connection.QueryFirstAsync(sql, user); - logger.LogInformation("User created successfully with ID: {UserId} for Telegram ID: {TelegramId}", - createdUser.Id, user.TelegramId); + logger.LogInformation( + "User created successfully with ID: {UserId} for Telegram ID: {TelegramId}", createdUser.Id, user.TelegramId); return createdUser; } @@ -67,6 +93,12 @@ INSERT INTO users (telegram_id, username, first_name, last_name) } } + /// + /// Retrieves the currently active donation goal. + /// + /// + /// An active object if found; otherwise, null. + /// public async Task GetActiveGoalAsync() { logger.LogDebug("Retrieving active donation goal"); @@ -89,8 +121,8 @@ ORDER BY created_at DESC LIMIT 1"; var goal = await connection.QueryFirstOrDefaultAsync(sql); - logger.LogDebug("Active goal retrieval {(Result)}", - goal != null ? $"succeeded - Goal ID: {goal.Id}" : "failed - no active goal"); + logger.LogDebug( + "Active goal retrieval {(Result)}", goal != null ? $"succeeded - Goal ID: {goal.Id}" : "failed - no active goal"); return goal; } @@ -101,6 +133,12 @@ ORDER BY created_at DESC } } + /// + /// Gets the total number of donations made to active goals. + /// + /// + /// The count of donations for active goals. + /// public async Task GetCountDonationsForActiveGoals() { logger.LogDebug("Retrieving count of donations for active goals"); @@ -126,6 +164,12 @@ FROM donations d } } + /// + /// Gets the number of unique users who have donated to active goals. + /// + /// + /// The count of unique users for active goals. + /// public async Task GetCountUsersForActiveGoals() { logger.LogDebug("Retrieving count of unique users for active goals"); @@ -151,6 +195,13 @@ FROM donations d } } + /// + /// Creates a new donation goal and deactivates any previously active goal. + /// + /// The donation goal object containing the data to insert. + /// + /// The created object with the generated identifier. + /// public async Task CreateGoalAsync(DonationGoal goal) { logger.LogInformation("Creating new donation goal: {GoalTitle}", goal.Title); @@ -166,8 +217,8 @@ INSERT INTO donation_goals (title, description, target_amount) RETURNING *"; var createdGoal = await connection.QueryFirstAsync(sql, goal); - logger.LogInformation("Goal created successfully with ID: {GoalId}, Target: {TargetAmount}", - createdGoal.Id, createdGoal.TargetAmount); + logger.LogInformation( + "Goal created successfully with ID: {GoalId}, Target: {TargetAmount}", createdGoal.Id, createdGoal.TargetAmount); return createdGoal; } @@ -178,6 +229,11 @@ INSERT INTO donation_goals (title, description, target_amount) } } + /// + /// Updates the current amount of a donation goal by adding the specified amount. + /// + /// The identifier of the goal to update. + /// The amount to add to the goal's current amount. public async Task UpdateGoalCurrentAmountAsync(int goalId, decimal amountToAdd) { logger.LogDebug("Updating goal {GoalId} current amount by {AmountToAdd}", goalId, amountToAdd); @@ -208,10 +264,17 @@ UPDATE donation_goals } } + /// + /// Creates a new donation record in the database. + /// + /// The donation object containing the data to insert. + /// + /// The created object with the generated identifier. + /// public async Task CreateDonationAsync(Donation donation) { - logger.LogDebug("Creating donation for user {UserTelegramId} with amount {Amount} {Currency}", - donation.UserTelegramId, donation.Amount, donation.Currency); + logger.LogDebug( + "Creating donation for user {UserTelegramId} with amount {Amount} {Currency}", donation.UserTelegramId, donation.Amount, donation.Currency); try { @@ -230,8 +293,8 @@ INSERT INTO donations (user_telegram_id, goal_id, amount, currency, provider_pay created_at AS CreatedAt"; var createdDonation = await connection.QueryFirstAsync(sql, donation); - logger.LogInformation("Donation created successfully with ID: {DonationId} for user {UserTelegramId}", - createdDonation.Id, donation.UserTelegramId); + logger.LogInformation( + "Donation created successfully with ID: {DonationId} for user {UserTelegramId}", createdDonation.Id, donation.UserTelegramId); return createdDonation; } @@ -242,6 +305,13 @@ INSERT INTO donations (user_telegram_id, goal_id, amount, currency, provider_pay } } + /// + /// Retrieves a donation by its payment provider identifier. + /// + /// The payment provider's donation identifier. + /// + /// A object if found; otherwise, null. + /// public async Task GetDonationAsync(string donationId) { logger.LogDebug("Retrieving donation by ID: {DonationId}", donationId); @@ -263,8 +333,8 @@ FROM donations WHERE provider_payment_id = @DonationId"; var donation = await connection.QueryFirstOrDefaultAsync(sql, new { DonationId = donationId }); - logger.LogDebug("Donation retrieval {(Result)} for ID: {DonationId}", - donation != null ? "succeeded" : "failed - not found", donationId); + logger.LogDebug( + "Donation retrieval {(Result)} for ID: {DonationId}", donation != null ? "succeeded" : "failed - not found", donationId); return donation; } diff --git a/Data/IDapperRepository.cs b/Data/IDapperRepository.cs index d4c3ad6..a68732f 100755 --- a/Data/IDapperRepository.cs +++ b/Data/IDapperRepository.cs @@ -1,4 +1,8 @@ -using Data.Models; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Data.Models; namespace Data; diff --git a/Data/Models/Donation.cs b/Data/Models/Donation.cs index a97671d..0a40b84 100755 --- a/Data/Models/Donation.cs +++ b/Data/Models/Donation.cs @@ -1,20 +1,75 @@ -namespace Data.Models; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// +namespace Data.Models; + +/// +/// Represents a donation made by a user towards a specific goal. +/// public class Donation { + /// + /// Gets or sets the unique identifier for the donation. + /// + /// + /// The donation identifier. + /// public int Id { get; set; } + /// + /// Gets or sets the Telegram user identifier of the donor. + /// + /// + /// The Telegram user identifier. + /// public long UserTelegramId { get; set; } + /// + /// Gets or sets the identifier of the goal this donation is for. + /// + /// + /// The goal identifier. + /// public int GoalId { get; set; } + /// + /// Gets or sets the amount of the donation. + /// + /// + /// The donation amount in the specified currency. + /// public decimal Amount { get; set; } + /// + /// Gets or sets the currency of the donation. Default is "RUB" (Russian Ruble). + /// + /// + /// The 3-letter currency code (ISO 4217). + /// public string Currency { get; set; } = "RUB"; + /// + /// Gets or sets the payment provider's transaction identifier. + /// + /// + /// The payment provider's unique transaction identifier, or null if not available. + /// public string? ProviderPaymentId { get; set; } + /// + /// Gets or sets the current status of the donation. Default is "pending". + /// + /// + /// The donation status (e.g., "pending", "completed", "failed", "refunded"). + /// public string Status { get; set; } = "pending"; + /// + /// Gets or sets the date and time when the donation was created. + /// + /// + /// The creation timestamp in UTC. + /// public DateTime CreatedAt { get; set; } = DateTime.UtcNow; -} +} \ No newline at end of file diff --git a/Data/Models/DonationGoal.cs b/Data/Models/DonationGoal.cs index 5a15431..e54efc5 100755 --- a/Data/Models/DonationGoal.cs +++ b/Data/Models/DonationGoal.cs @@ -1,18 +1,67 @@ -namespace Data.Models; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// +namespace Data.Models; + +/// +/// Represents a fundraising goal with a target amount and current progress. +/// public class DonationGoal { + /// + /// Gets or sets the unique identifier for the donation goal. + /// + /// + /// The goal identifier. + /// public int Id { get; set; } + /// + /// Gets or sets the title of the donation goal. + /// + /// + /// The goal title. + /// public string Title { get; set; } = string.Empty; + /// + /// Gets or sets the optional description providing more details about the goal. + /// + /// + /// The goal description, or null if not provided. + /// public string? Description { get; set; } + /// + /// Gets or sets the target amount to be raised for this goal. + /// + /// + /// The target amount in the base currency. + /// public decimal TargetAmount { get; set; } + /// + /// Gets or sets the current amount raised towards the target. + /// + /// + /// The current amount raised. + /// public decimal CurrentAmount { get; set; } + /// + /// Gets or sets a value indicating whether the goal is currently active and accepting donations. + /// + /// + /// true if the goal is active; otherwise, false. + /// public bool IsActive { get; set; } = true; + /// + /// Gets or sets the date and time when the goal was created. + /// + /// + /// The creation timestamp in UTC. + /// public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } diff --git a/Data/Models/Users.cs b/Data/Models/Users.cs index 588b4f2..abf0b9f 100755 --- a/Data/Models/Users.cs +++ b/Data/Models/Users.cs @@ -1,18 +1,46 @@ -namespace Data.Models; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// +namespace Data.Models; + +/// +/// Represents a user in the system. +/// public class Users { + /// + /// Gets or sets the unique identifier for the user. + /// public int Id { get; set; } + /// + /// Gets or sets a value indicating whether the user has administrative privileges. + /// public bool Admin { get; set; } + /// + /// Gets or sets the user's Telegram identifier. + /// public long TelegramId { get; set; } + /// + /// Gets or sets the user's Telegram username (optional). + /// public string? Username { get; set; } + /// + /// Gets or sets the user's first name (optional). + /// public string? FirstName { get; set; } + /// + /// Gets or sets the user's last name (optional). + /// public string? LastName { get; set; } + /// + /// Gets or sets the date and time when the user record was created. + /// public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } \ No newline at end of file diff --git a/Services/DonationService.cs b/Services/DonationService.cs index 749dbbd..17fbb0d 100755 --- a/Services/DonationService.cs +++ b/Services/DonationService.cs @@ -1,14 +1,27 @@ -using Data; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Data; using Data.Models; using Microsoft.Extensions.Logging; namespace Services; +/// +/// Provides services for managing user donations and user data operations. +/// Implements the interface. +/// public class DonationService : IDonationService { private readonly IDapperRepository repository; private readonly ILogger logger; + /// + /// Initializes a new instance of the class. + /// + /// Data repository for database operations. + /// Logger instance for tracking operations. public DonationService(IDapperRepository repository, ILogger logger) { this.repository = repository; @@ -16,6 +29,19 @@ public DonationService(IDapperRepository repository, ILogger lo this.logger.LogDebug("DonationService initialized"); } + /// + /// Retrieves an existing user by Telegram ID or creates a new user if not found. + /// + /// The Telegram user identifier. + /// The Telegram username (optional). + /// The user's first name (optional). + /// The user's last name (optional). + /// + /// The existing or newly created object. + /// + /// + /// This method implements a "get or create" pattern for user management. + /// public async Task GetOrCreateUserAsync(long telegramId, string? username, string? firstName, string? lastName) { logger.LogDebug("Getting or creating user with Telegram ID: {TelegramId}", telegramId); @@ -30,8 +56,8 @@ public async Task GetOrCreateUserAsync(long telegramId, string? username, return user; } - logger.LogInformation("Creating new user for Telegram ID: {TelegramId}, username: {Username}", - telegramId, username); + logger.LogInformation( + "Creating new user for Telegram ID: {TelegramId}, username: {Username}", telegramId, username); var newUser = new Users { @@ -43,8 +69,8 @@ public async Task GetOrCreateUserAsync(long telegramId, string? username, }; var createdUser = await repository.CreateUserAsync(newUser); - logger.LogInformation("Created new user {UserId} for Telegram ID: {TelegramId}", - createdUser.Id, telegramId); + logger.LogInformation( + "Created new user {UserId} for Telegram ID: {TelegramId}", createdUser.Id, telegramId); return createdUser; } @@ -55,18 +81,28 @@ public async Task GetOrCreateUserAsync(long telegramId, string? username, } } + /// + /// Processes a donation transaction and updates the relevant goal's progress. + /// + /// The Telegram identifier of the user making the donation. + /// The donation amount. + /// The currency code of the donation. + /// The unique identifier provided by the payment provider. + /// + /// true if the donation was processed successfully; otherwise, false. + /// public async Task ProcessDonationAsync(long userTelegramId, decimal amount, string currency, string donationId) { - logger.LogInformation("Processing donation {DonationId} for user {UserTelegramId}, amount: {Amount} {Currency}", - donationId, userTelegramId, amount, currency); + logger.LogInformation( + "Processing donation {DonationId} for user {UserTelegramId}, amount: {Amount} {Currency}", donationId, userTelegramId, amount, currency); try { var existingDonation = await repository.GetDonationAsync(donationId); if (existingDonation != null) { - logger.LogWarning("Donation {DonationId} has already been processed on {ProcessedDate}", - donationId, existingDonation.CreatedAt); + logger.LogWarning( + "Donation {DonationId} has already been processed on {ProcessedDate}", donationId, existingDonation.CreatedAt); return true; } @@ -90,22 +126,31 @@ public async Task ProcessDonationAsync(long userTelegramId, decimal amount }; var createdDonation = await repository.CreateDonationAsync(donation); - logger.LogDebug("Created donation record {DonationRecordId} for payment {DonationId}", - createdDonation.Id, donationId); + logger.LogDebug( + "Created donation record {DonationRecordId} for payment {DonationId}", createdDonation.Id, donationId); await repository.UpdateGoalCurrentAmountAsync(goal.Id, amount); logger.LogInformation( "Successfully processed donation {DonationId} for user {UserTelegramId}. " + "Amount: {Amount} {Currency} added to goal {GoalId}", - donationId, userTelegramId, amount, currency, goal.Id); + donationId, + userTelegramId, + amount, + currency, + goal.Id); return true; } catch (Exception ex) { - logger.LogError(ex, "Error processing donation {DonationId} for user {UserTelegramId}, amount: {Amount} {Currency}", - donationId, userTelegramId, amount, currency); + logger.LogError( + ex, + "Error processing donation {DonationId} for user {UserTelegramId}, amount: {Amount} {Currency}", + donationId, + userTelegramId, + amount, + currency); return false; } } diff --git a/Services/GoalService.cs b/Services/GoalService.cs index 54ad7a9..9e233f6 100755 --- a/Services/GoalService.cs +++ b/Services/GoalService.cs @@ -1,4 +1,8 @@ -using Data; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Data; using Data.Models; using Microsoft.Extensions.Logging; @@ -66,44 +70,6 @@ public async Task IsUserAdminAsync(long telegramId) } } - private async Task GetActiveUsersGoalAsync() - { - logger.LogDebug("Retrieving active users count for goal"); - - try - { - var count = await repository.GetCountUsersForActiveGoals(); - - logger.LogDebug("Retrieved {UserCount} active users for goal", count); - - return count; - } - catch (Exception ex) - { - logger.LogError(ex, "Error retrieving active users count for goal"); - return 0; - } - } - - private async Task GetActiveDonationsGoalAsync() - { - logger.LogDebug("Retrieving active donations count for goal"); - - try - { - var count = await repository.GetCountDonationsForActiveGoals(); - - logger.LogDebug("Retrieved {DonationCount} active donations for goal", count); - - return count; - } - catch (Exception ex) - { - logger.LogError(ex, "Error retrieving active donations count for goal"); - return 0; - } - } - /// public async Task GetGoalStatsAsync() { @@ -175,25 +141,6 @@ public async Task GetStartStats() } } - /// - /// Creates a visual progress bar representation. - /// - /// The completion percentage. - /// A string representing the progress bar. - private string CreateProgressBar(double percent) - { - if (percent >= 100) - { - return $"[{new string('■', 10)}]"; - } - else - { - var filled = (int)Math.Round(percent / 10); - var empty = 10 - filled; - return $"[{new string('■', filled)}{new string('□', empty)}]"; - } - } - /// public async Task CreateGoalAsync(string title, string description, decimal targetAmount) { @@ -221,4 +168,61 @@ public async Task CreateGoalAsync(string title, string description throw; } } + + private async Task GetActiveUsersGoalAsync() + { + logger.LogDebug("Retrieving active users count for goal"); + + try + { + var count = await repository.GetCountUsersForActiveGoals(); + + logger.LogDebug("Retrieved {UserCount} active users for goal", count); + + return count; + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving active users count for goal"); + return 0; + } + } + + private async Task GetActiveDonationsGoalAsync() + { + logger.LogDebug("Retrieving active donations count for goal"); + + try + { + var count = await repository.GetCountDonationsForActiveGoals(); + + logger.LogDebug("Retrieved {DonationCount} active donations for goal", count); + + return count; + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving active donations count for goal"); + return 0; + } + } + + /// + /// Creates a visual progress bar representation. + /// + /// The completion percentage. + /// A string representing the progress bar. + private string CreateProgressBar(double percent) + { + if (percent >= 100) + { + return $"[{new string('■', 10)}]"; + } + else + { + var filled = (int)Math.Round(percent / 10); + var empty = 10 - filled; + return $"[{new string('■', filled)}{new string('□', empty)}]"; + } + } } \ No newline at end of file diff --git a/Services/IDonationService.cs b/Services/IDonationService.cs index 66082fb..4a2ef3f 100755 --- a/Services/IDonationService.cs +++ b/Services/IDonationService.cs @@ -1,4 +1,8 @@ -using Data.Models; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Data.Models; namespace Services; diff --git a/Services/IGoalService.cs b/Services/IGoalService.cs index fe2b729..f820dc5 100755 --- a/Services/IGoalService.cs +++ b/Services/IGoalService.cs @@ -1,4 +1,8 @@ -using Data.Models; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Data.Models; namespace Services; diff --git a/TestBot/Bot/Bot.Hadler/AdminHandlerTests.cs b/TestBot/Bot/Bot.Hadler/AdminHandlerTests.cs index 6496ca0..aa3f1d6 100755 --- a/TestBot/Bot/Bot.Hadler/AdminHandlerTests.cs +++ b/TestBot/Bot/Bot.Hadler/AdminHandlerTests.cs @@ -1,4 +1,8 @@ -using Bot.Handlers; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot.Handlers; using Bot.Services; using Microsoft.Extensions.Logging; using Moq; @@ -10,80 +14,98 @@ namespace Bot.Tests.Handlers; +/// +/// Unit tests for the class. +/// [TestFixture] public class AdminHandlerTests { - private Mock> _loggerMock; - private Mock _goalServiceMock; - private Mock _adminStateServiceMock; - private Mock _botClientMock; - private AdminHandler _handler; - + private Mock> loggerMock; + private Mock goalServiceMock; + private Mock adminStateServiceMock; + private Mock botClientMock; + private AdminHandler handler; + + /// + /// Sets up the test environment before each test execution. + /// [SetUp] public void Setup() { - _loggerMock = new Mock>(); - _goalServiceMock = new Mock(); + loggerMock = new Mock>(); + goalServiceMock = new Mock(); // Создаем мок для AdminStateService с правильным конструктором var adminStateServiceLoggerMock = new Mock>(); - _adminStateServiceMock = new Mock(adminStateServiceLoggerMock.Object); + adminStateServiceMock = new Mock(adminStateServiceLoggerMock.Object); - _botClientMock = new Mock(); + botClientMock = new Mock(); - _handler = new AdminHandler( - _loggerMock.Object, - _goalServiceMock.Object, - _adminStateServiceMock.Object); + handler = new AdminHandler( + loggerMock.Object, + goalServiceMock.Object, + adminStateServiceMock.Object); } + /// + /// Tests that a message with null user triggers a warning log and returns without processing. + /// + /// A representing the asynchronous unit test. [Test] public async Task HandleAdminGoalCreationAsync_NullUser_LogsWarningAndReturns() { // Arrange var message = new Message { From = null, Chat = new Chat { Id = 123 }, Text = "Test" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + await handler.HandleAdminGoalCreationAsync(botClientMock.Object, message, cancellationToken); // Assert - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Received message with null user ID")), + It.Is((v, t) => v.ToString() !.Contains("Received message with null user ID")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that a message from a user without admin state triggers a warning log and returns. + /// + /// A representing the asynchronous unit test. [Test] public async Task HandleAdminGoalCreationAsync_NullState_LogsWarningAndReturns() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "Test" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _adminStateServiceMock + adminStateServiceMock .Setup(x => x.GetState(123)) - .Returns((AdminGoalCreationState)null); + .Returns(null as AdminGoalCreationState); // Act - await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + await handler.HandleAdminGoalCreationAsync(botClientMock.Object, message, cancellationToken); // Assert - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("No admin state found for user")), + It.Is((v, t) => v.ToString() !.Contains("No admin state found for user")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that a message with empty text triggers a warning log and returns. + /// + /// A representing the asynchronous unit test. [Test] public async Task HandleAdminGoalCreationAsync_EmptyMessageText_LogsWarningAndReturns() { @@ -91,26 +113,30 @@ public async Task HandleAdminGoalCreationAsync_EmptyMessageText_LogsWarningAndRe var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = null }; var state = new AdminGoalCreationState { CurrentStep = AdminGoalStep.WaitingForTitle }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _adminStateServiceMock + adminStateServiceMock .Setup(x => x.GetState(123)) .Returns(state); // Act - await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + await handler.HandleAdminGoalCreationAsync(botClientMock.Object, message, cancellationToken); // Assert - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Received empty message text")), + It.Is((v, t) => v.ToString() !.Contains("Received empty message text")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that a valid title is processed correctly during the title step. + /// + /// A representing the asynchronous unit test. [Test] public async Task HandleAdminGoalCreationAsync_WaitingForTitle_ValidTitle_ProcessesTitleStep() { @@ -118,53 +144,61 @@ public async Task HandleAdminGoalCreationAsync_WaitingForTitle_ValidTitle_Proces var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "Новая цель" }; var state = new AdminGoalCreationState { CurrentStep = AdminGoalStep.WaitingForTitle }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _adminStateServiceMock + adminStateServiceMock .Setup(x => x.GetState(123)) .Returns(state); // Act - await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + await handler.HandleAdminGoalCreationAsync(botClientMock.Object, message, cancellationToken); // Assert - _adminStateServiceMock.Verify( + adminStateServiceMock.Verify( x => x.SetTitle(123, "Новая цель"), Times.Once); } + /// + /// Tests that a title exceeding the length limit triggers cancellation of goal creation. + /// + /// A representing the asynchronous unit test. [Test] public async Task HandleAdminGoalCreationAsync_WaitingForTitle_TooLongTitle_CancelsCreation() { // Arrange var user = new User { Id = 123 }; - var longTitle = new string('a', 256); // 256 символов > 255 + var longTitle = new string('a', 256); var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = longTitle }; var state = new AdminGoalCreationState { CurrentStep = AdminGoalStep.WaitingForTitle }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _adminStateServiceMock + adminStateServiceMock .Setup(x => x.GetState(123)) .Returns(state); // Act - await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + await handler.HandleAdminGoalCreationAsync(botClientMock.Object, message, cancellationToken); // Assert - _adminStateServiceMock.Verify( + adminStateServiceMock.Verify( x => x.CancelGoalCreation(123), Times.Once); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("provided title that exceeds length limit")), + It.Is((v, t) => v.ToString() !.Contains("provided title that exceeds length limit")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that a valid description is processed correctly during the description step. + /// + /// A representing the asynchronous unit test. [Test] public async Task HandleAdminGoalCreationAsync_WaitingForDescription_ProcessesDescriptionStep() { @@ -172,21 +206,25 @@ public async Task HandleAdminGoalCreationAsync_WaitingForDescription_ProcessesDe var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "Описание цели" }; var state = new AdminGoalCreationState { CurrentStep = AdminGoalStep.WaitingForDescription }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _adminStateServiceMock + adminStateServiceMock .Setup(x => x.GetState(123)) .Returns(state); // Act - await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + await handler.HandleAdminGoalCreationAsync(botClientMock.Object, message, cancellationToken); // Assert - _adminStateServiceMock.Verify( + adminStateServiceMock.Verify( x => x.SetDescription(123, "Описание цели"), Times.Once); } + /// + /// Tests that an invalid amount triggers cancellation of goal creation. + /// + /// A representing the asynchronous unit test. [Test] public async Task HandleAdminGoalCreationAsync_WaitingForAmount_InvalidAmount_CancelsCreation() { @@ -197,59 +235,67 @@ public async Task HandleAdminGoalCreationAsync_WaitingForAmount_InvalidAmount_Ca { CurrentStep = AdminGoalStep.WaitingForAmount, Title = "Test Goal", - Description = "Test Description" + Description = "Test Description", }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _adminStateServiceMock + adminStateServiceMock .Setup(x => x.GetState(123)) .Returns(state); // Act - await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + await handler.HandleAdminGoalCreationAsync(botClientMock.Object, message, cancellationToken); // Assert - _adminStateServiceMock.Verify( + adminStateServiceMock.Verify( x => x.CancelGoalCreation(123), Times.Once); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("provided invalid amount")), + It.Is((v, t) => v.ToString() !.Contains("provided invalid amount")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that an amount exceeding the maximum limit triggers cancellation of goal creation. + /// + /// A representing the asynchronous unit test. [Test] public async Task HandleAdminGoalCreationAsync_WaitingForAmount_AmountTooLarge_CancelsCreation() { // Arrange var user = new User { Id = 123 }; - var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "100000000" }; // 100 миллионов + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "100000000" }; var state = new AdminGoalCreationState { CurrentStep = AdminGoalStep.WaitingForAmount, Title = "Test Goal", - Description = "Test Description" + Description = "Test Description", }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _adminStateServiceMock + adminStateServiceMock .Setup(x => x.GetState(123)) .Returns(state); // Act - await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + await handler.HandleAdminGoalCreationAsync(botClientMock.Object, message, cancellationToken); // Assert - _adminStateServiceMock.Verify( + adminStateServiceMock.Verify( x => x.CancelGoalCreation(123), Times.Once); } + /// + /// Tests that a valid amount triggers successful goal creation. + /// + /// A representing the asynchronous unit test. [Test] public async Task HandleAdminGoalCreationAsync_WaitingForAmount_ValidAmount_CreatesGoal() { @@ -260,41 +306,45 @@ public async Task HandleAdminGoalCreationAsync_WaitingForAmount_ValidAmount_Crea { CurrentStep = AdminGoalStep.WaitingForAmount, Title = "Test Goal", - Description = "Test Description" + Description = "Test Description", }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; var createdGoal = new Data.Models.DonationGoal { Id = 1, Title = "Test Goal", Description = "Test Description", TargetAmount = 5000 }; - _adminStateServiceMock + adminStateServiceMock .Setup(x => x.GetState(123)) .Returns(state); - _goalServiceMock + goalServiceMock .Setup(x => x.CreateGoalAsync("Test Goal", "Test Description", 5000)) .ReturnsAsync(createdGoal); // Act - await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + await handler.HandleAdminGoalCreationAsync(botClientMock.Object, message, cancellationToken); // Assert - _goalServiceMock.Verify( + goalServiceMock.Verify( x => x.CreateGoalAsync("Test Goal", "Test Description", 5000), Times.Once); - _adminStateServiceMock.Verify( + adminStateServiceMock.Verify( x => x.CancelGoalCreation(123), Times.Once); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Goal created successfully")), + It.Is((v, t) => v.ToString() !.Contains("Goal created successfully")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that an exception in goal service triggers error logging. + /// + /// A representing the asynchronous unit test. [Test] public async Task HandleAdminGoalCreationAsync_WaitingForAmount_GoalServiceThrows_LogsError() { @@ -305,129 +355,144 @@ public async Task HandleAdminGoalCreationAsync_WaitingForAmount_GoalServiceThrow { CurrentStep = AdminGoalStep.WaitingForAmount, Title = "Test Goal", - Description = "Test Description" + Description = "Test Description", }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _adminStateServiceMock + adminStateServiceMock .Setup(x => x.GetState(123)) .Returns(state); - _goalServiceMock + goalServiceMock .Setup(x => x.CreateGoalAsync("Test Goal", "Test Description", 5000)) .ThrowsAsync(new Exception("Database error")); // Act - await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + await handler.HandleAdminGoalCreationAsync(botClientMock.Object, message, cancellationToken); // Assert - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Failed to create goal in database")), + It.Is((v, t) => v.ToString() !.Contains("Failed to create goal in database")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that starting goal creation sets the admin state and logs appropriately. + /// + /// A representing the asynchronous unit test. [Test] public async Task StartGoalCreationAsync_SetsStateAndLogs() { // Arrange var chatId = 456L; var userId = 123L; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.StartGoalCreationAsync(_botClientMock.Object, chatId, userId, cancellationToken); + await handler.StartGoalCreationAsync(botClientMock.Object, chatId, userId, cancellationToken); // Assert - _adminStateServiceMock.Verify( + adminStateServiceMock.Verify( x => x.StartGoalCreation(123, 456), Times.Once); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Starting goal creation process")), + It.Is((v, t) => v.ToString() !.Contains("Starting goal creation process")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that an exception during goal creation start is properly logged. + /// [Test] - public async Task StartGoalCreationAsync_ThrowsException_LogsError() + public void StartGoalCreationAsyncThrowsExceptionLogsError() { // Arrange var chatId = 456L; var userId = 123L; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _adminStateServiceMock + adminStateServiceMock .Setup(x => x.StartGoalCreation(123, 456)) .Throws(new Exception("State service error")); // Act & Assert Assert.ThrowsAsync(() => - _handler.StartGoalCreationAsync(_botClientMock.Object, chatId, userId, cancellationToken)); + handler.StartGoalCreationAsync(botClientMock.Object, chatId, userId, cancellationToken)); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Failed to start goal creation")), + It.Is((v, t) => v.ToString() !.Contains("Failed to start goal creation")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that non-admin access attempts are properly logged. + /// + /// A representing the asynchronous unit test. [Test] public async Task HandleNotAdmin_LogsWarning() { // Arrange var chatId = 456L; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleNotAdmin(_botClientMock.Object, chatId, cancellationToken); + await handler.HandleNotAdmin(botClientMock.Object, chatId, cancellationToken); // Assert - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Non-admin access attempt detected")), + It.Is((v, t) => v.ToString() !.Contains("Non-admin access attempt detected")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that an unknown admin step triggers a warning log. + /// + /// A representing the asynchronous unit test. [Test] public async Task HandleAdminGoalCreationAsync_UnknownStep_LogsWarning() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "Test" }; - var state = new AdminGoalCreationState { CurrentStep = (AdminGoalStep)999 }; // Неизвестный шаг - var cancellationToken = new CancellationToken(); + var state = new AdminGoalCreationState { CurrentStep = (AdminGoalStep)999 }; + var cancellationToken = CancellationToken.None; - _adminStateServiceMock + adminStateServiceMock .Setup(x => x.GetState(123)) .Returns(state); // Act - await _handler.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken); + await handler.HandleAdminGoalCreationAsync(botClientMock.Object, message, cancellationToken); // Assert - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Unknown admin step")), + It.Is((v, t) => v.ToString() !.Contains("Unknown admin step")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } } \ No newline at end of file diff --git a/TestBot/Bot/Bot.Hadler/CallbackQueryHandlerTests.cs b/TestBot/Bot/Bot.Hadler/CallbackQueryHandlerTests.cs index a1c9478..84679fa 100755 --- a/TestBot/Bot/Bot.Hadler/CallbackQueryHandlerTests.cs +++ b/TestBot/Bot/Bot.Hadler/CallbackQueryHandlerTests.cs @@ -1,4 +1,8 @@ -using Bot.Handlers; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot.Handlers; using Bot.Services; using Configurations; using Microsoft.Extensions.Logging; @@ -6,29 +10,36 @@ using Moq; using NUnit.Framework; using Services; +using System.Reflection.Metadata; using Telegram.Bot; using Telegram.Bot.Types; namespace Bot.Tests.Handlers; +/// +/// Unit tests for the class. +/// [TestFixture] public class CallbackQueryHandlerTests { - private Mock> _loggerMock; - private Mock _paymentHandlerMock; - private Mock _commandHandlerMock; - private Mock _userStateServiceMock; - private Mock _botClientMock; - private CallbackQueryHandler _handler; - + private Mock> loggerMock; + private Mock paymentHandlerMock; + private Mock commandHandlerMock; + private Mock userStateServiceMock; + private Mock botClientMock; + private CallbackQueryHandler handler; + + /// + /// Sets up the test environment before each test execution. + /// [SetUp] public void Setup() { - _loggerMock = new Mock>(); + this.loggerMock = new Mock>(); // Явно создаем мок логгера для UserStateService var userStateServiceLoggerMock = new Mock>(); - _userStateServiceMock = new Mock(userStateServiceLoggerMock.Object); + this.userStateServiceMock = new Mock(userStateServiceLoggerMock.Object); // Создаем моки для зависимостей PaymentHandler var paymentLoggerMock = new Mock>(); @@ -41,7 +52,7 @@ public void Setup() PaymentProviderToken = "test-token", }); - _paymentHandlerMock = new Mock( + this.paymentHandlerMock = new Mock( paymentLoggerMock.Object, goalServiceMock.Object, donationServiceMock.Object, @@ -63,16 +74,16 @@ public void Setup() Mock.Of(), adminStateServiceMock.Object); - _commandHandlerMock = new Mock( + this.commandHandlerMock = new Mock( commandLoggerMock.Object, commandGoalServiceMock.Object, keyboardServiceMock.Object, adminHandlerMock.Object); - _botClientMock = new Mock(); + this.botClientMock = new Mock(); // Настраиваем бизнес-методы - _paymentHandlerMock + this.paymentHandlerMock .Setup(x => x.CreateDonationInvoice( It.IsAny(), It.IsAny(), @@ -81,54 +92,64 @@ public void Setup() It.IsAny())) .Returns(Task.CompletedTask); - _commandHandlerMock + this.commandHandlerMock .Setup(x => x.HandleStatsCommand( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); - _userStateServiceMock + this.userStateServiceMock .Setup(x => x.SetWaitingForAmount( It.IsAny(), It.IsAny())) .Verifiable(); - _handler = new CallbackQueryHandler( - _loggerMock.Object, - _paymentHandlerMock.Object, - _commandHandlerMock.Object, - _userStateServiceMock.Object); + this.handler = new CallbackQueryHandler( + this.loggerMock.Object, + this.paymentHandlerMock.Object, + this.commandHandlerMock.Object, + this.userStateServiceMock.Object); } + /// + /// Tests that the handler correctly identifies updates with callback queries. + /// [Test] - public void CanHandle_WithCallbackQuery_ReturnsTrue() + public void CanHandleWithCallbackQueryReturnsTrue() { // Arrange var update = new Update { CallbackQuery = new CallbackQuery() }; // Act - var result = _handler.CanHandle(update); + var result = this.handler.CanHandle(update); // Assert Assert.That(result, Is.True); } + /// + /// Tests that the handler correctly identifies updates without callback queries. + /// [Test] - public void CanHandle_WithoutCallbackQuery_ReturnsFalse() + public void CanHandleWithoutCallbackQueryReturnsFalse() { // Arrange var update = new Update { CallbackQuery = null }; // Act - var result = _handler.CanHandle(update); + var result = this.handler.CanHandle(update); // Assert Assert.That(result, Is.False); } + /// + /// Tests that a callback query without chat information does not trigger further processing. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_NoChatInformation_DoesNotProcessFurther() + public async Task HandleAsyncNoChatInformationDoesNotProcessFurther() { // Arrange var callbackQuery = new CallbackQuery @@ -136,22 +157,26 @@ public async Task HandleAsync_NoChatInformation_DoesNotProcessFurther() Id = "test_query_id", From = new User { Id = 123 }, Data = "donate_100", - Message = null // No message/chat information + Message = null, // No message/chat information }; var update = new Update { CallbackQuery = callbackQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - убеждаемся, что бизнес-логика не вызывалась - _paymentHandlerMock.Verify( + this.paymentHandlerMock.Verify( x => x.CreateDonationInvoice(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } + /// + /// Tests that a callback query with empty data does not trigger further processing. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_EmptyCallbackData_DoesNotProcessFurther() + public async Task HandleAsyncEmptyCallbackDataDoesNotProcessFurther() { // Arrange var callbackQuery = new CallbackQuery @@ -159,22 +184,26 @@ public async Task HandleAsync_EmptyCallbackData_DoesNotProcessFurther() Id = "test_query_id", From = new User { Id = 123 }, Data = null, // Empty callback data - Message = new Message { Chat = new Chat { Id = 456 } } + Message = new Message { Chat = new Chat { Id = 456 } }, }; var update = new Update { CallbackQuery = callbackQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - убеждаемся, что бизнес-логика не вызывалась - _paymentHandlerMock.Verify( + this.paymentHandlerMock.Verify( x => x.CreateDonationInvoice(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } + /// + /// Tests that the "enter custom amount" callback correctly sets the user state. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_EnterCustomAmount_SetsUserState() + public async Task HandleAsyncEnterCustomAmountSetsUserState() { // Arrange var callbackQuery = new CallbackQuery @@ -182,22 +211,26 @@ public async Task HandleAsync_EnterCustomAmount_SetsUserState() Id = "test_query_id", From = new User { Id = 123 }, Data = "enter_custom_amount", - Message = new Message { Chat = new Chat { Id = 456 } } + Message = new Message { Chat = new Chat { Id = 456 } }, }; var update = new Update { CallbackQuery = callbackQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _userStateServiceMock.Verify( + this.userStateServiceMock.Verify( x => x.SetWaitingForAmount(123, 456), Times.Once); } + /// + /// Tests that predefined donation callbacks correctly create invoices with appropriate amounts. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_PredefinedDonation_CreatesInvoice() + public async Task HandleAsyncPredefinedDonationCreatesInvoice() { // Arrange var testCases = new[] @@ -205,37 +238,41 @@ public async Task HandleAsync_PredefinedDonation_CreatesInvoice() new { CallbackData = "donate_100", ExpectedAmount = 100 }, new { CallbackData = "donate_500", ExpectedAmount = 500 }, new { CallbackData = "donate_1000", ExpectedAmount = 1000 }, - new { CallbackData = "donate_5000", ExpectedAmount = 5000 } + new { CallbackData = "donate_5000", ExpectedAmount = 5000 }, }; foreach (var testCase in testCases) { // Reset mocks for each test case - _paymentHandlerMock.Invocations.Clear(); + this.paymentHandlerMock.Invocations.Clear(); var callbackQuery = new CallbackQuery { Id = "test_query_id", From = new User { Id = 123 }, Data = testCase.CallbackData, - Message = new Message { Chat = new Chat { Id = 456 } } + Message = new Message { Chat = new Chat { Id = 456 } }, }; var update = new Update { CallbackQuery = callbackQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _paymentHandlerMock.Verify( - x => x.CreateDonationInvoice(_botClientMock.Object, 456, 123, testCase.ExpectedAmount, cancellationToken), + this.paymentHandlerMock.Verify( + x => x.CreateDonationInvoice(this.botClientMock.Object, 456, 123, testCase.ExpectedAmount, cancellationToken), Times.Once, $"Failed for callback data: {testCase.CallbackData}"); } } + /// + /// Tests that the "show stats" callback correctly triggers statistics display. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_ShowStats_CallsStatsCommand() + public async Task HandleAsyncShowStatsCallsStatsCommand() { // Arrange var callbackQuery = new CallbackQuery @@ -243,22 +280,26 @@ public async Task HandleAsync_ShowStats_CallsStatsCommand() Id = "test_query_id", From = new User { Id = 123 }, Data = "show_stats", - Message = new Message { Chat = new Chat { Id = 456 } } + Message = new Message { Chat = new Chat { Id = 456 } }, }; var update = new Update { CallbackQuery = callbackQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _commandHandlerMock.Verify( - x => x.HandleStatsCommand(_botClientMock.Object, 456, cancellationToken), + this.commandHandlerMock.Verify( + x => x.HandleStatsCommand(this.botClientMock.Object, 456, cancellationToken), Times.Once); } + /// + /// Tests that unknown callback data does not trigger any business logic. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_UnknownCallbackData_DoesNotCallBusinessLogic() + public async Task HandleAsyncUnknownCallbackDataDoesNotCallBusinessLogic() { // Arrange var callbackQuery = new CallbackQuery @@ -266,30 +307,34 @@ public async Task HandleAsync_UnknownCallbackData_DoesNotCallBusinessLogic() Id = "test_query_id", From = new User { Id = 123 }, Data = "unknown_command", - Message = new Message { Chat = new Chat { Id = 456 } } + Message = new Message { Chat = new Chat { Id = 456 } }, }; var update = new Update { CallbackQuery = callbackQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - убеждаемся, что бизнес-логика не вызывалась - _paymentHandlerMock.Verify( + this.paymentHandlerMock.Verify( x => x.CreateDonationInvoice(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - _commandHandlerMock.Verify( + this.commandHandlerMock.Verify( x => x.HandleStatsCommand(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - _userStateServiceMock.Verify( + this.userStateServiceMock.Verify( x => x.SetWaitingForAmount(It.IsAny(), It.IsAny()), Times.Never); } + /// + /// Tests that format exceptions during donation amount parsing are properly logged. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_PaymentHandlerThrowsFormatException_LogsError() + public async Task HandleAsyncPaymentHandlerThrowsFormatExceptionLogsError() { // Arrange var callbackQuery = new CallbackQuery @@ -297,27 +342,31 @@ public async Task HandleAsync_PaymentHandlerThrowsFormatException_LogsError() Id = "test_query_id", From = new User { Id = 123 }, Data = "donate_invalid", // This will cause FormatException in parsing - Message = new Message { Chat = new Chat { Id = 456 } } + Message = new Message { Chat = new Chat { Id = 456 } }, }; var update = new Update { CallbackQuery = callbackQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await handler.HandleAsync(botClientMock.Object, update, cancellationToken); // Assert - проверяем, что ошибка была залогирована - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Failed to parse donation amount")), + It.Is((v, t) => v.ToString() !.Contains("Failed to parse donation amount")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Never); } + /// + /// Tests that general exceptions during invoice creation are properly logged. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_GeneralExceptionInProcess_CallsSafeAnswer() + public async Task HandleAsyncGeneralExceptionInProcessCallsSafeAnswer() { // Arrange var callbackQuery = new CallbackQuery @@ -325,32 +374,36 @@ public async Task HandleAsync_GeneralExceptionInProcess_CallsSafeAnswer() Id = "test_query_id", From = new User { Id = 123 }, Data = "donate_100", - Message = new Message { Chat = new Chat { Id = 456 } } + Message = new Message { Chat = new Chat { Id = 456 } }, }; var update = new Update { CallbackQuery = callbackQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Симулируем исключение в PaymentHandler - _paymentHandlerMock + this.paymentHandlerMock .Setup(x => x.CreateDonationInvoice(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new Exception("Test exception")); // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - проверяем, что ошибка была залогирована - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Failed to create donation invoice")), + It.Is((v, t) => v.ToString() !.Contains("Failed to create donation invoice")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that exceptions during statistics display are properly logged. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_CommandHandlerThrowsException_LogsError() + public async Task HandleAsyncCommandHandlerThrowsExceptionLogsError() { // Arrange var callbackQuery = new CallbackQuery @@ -358,27 +411,27 @@ public async Task HandleAsync_CommandHandlerThrowsException_LogsError() Id = "test_query_id", From = new User { Id = 123 }, Data = "show_stats", - Message = new Message { Chat = new Chat { Id = 456 } } + Message = new Message { Chat = new Chat { Id = 456 } }, }; var update = new Update { CallbackQuery = callbackQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Симулируем исключение в CommandHandler - _commandHandlerMock + this.commandHandlerMock .Setup(x => x.HandleStatsCommand(It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new Exception("Test exception")); // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - проверяем, что ошибка была залогирована - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Failed to show statistics")), + It.Is((v, t) => v.ToString() !.Contains("Failed to show statistics")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } } \ No newline at end of file diff --git a/TestBot/Bot/Bot.Hadler/CommandHandlerTests.cs b/TestBot/Bot/Bot.Hadler/CommandHandlerTests.cs index 73786d6..34ec35d 100755 --- a/TestBot/Bot/Bot.Hadler/CommandHandlerTests.cs +++ b/TestBot/Bot/Bot.Hadler/CommandHandlerTests.cs @@ -1,5 +1,10 @@ -using Bot.Handlers; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot.Handlers; using Bot.Services; +using Data.Models; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -8,521 +13,593 @@ using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.ReplyMarkups; -using Data.Models; namespace Bot.Tests.Handlers; +/// +/// Unit tests for the class. +/// [TestFixture] public class CommandHandlerTests { - private Mock> _loggerMock; - private Mock _goalServiceMock; - private Mock _keyboardServiceMock; - private Mock _adminHandlerMock; - private Mock _botClientMock; - private CommandHandler _handler; - + private Mock> loggerMock; + private Mock goalServiceMock; + private Mock keyboardServiceMock; + private Mock adminHandlerMock; + private Mock botClientMock; + private CommandHandler handler; + + /// + /// Sets up the test environment before each test execution. + /// [SetUp] public void Setup() { - _loggerMock = new Mock>(); - _goalServiceMock = new Mock(); + this.loggerMock = new Mock>(); + this.goalServiceMock = new Mock(); - // Создаем мок для KeyboardService с правильным конструктором var keyboardServiceLoggerMock = new Mock>(); - _keyboardServiceMock = new Mock(keyboardServiceLoggerMock.Object); + this.keyboardServiceMock = new Mock(keyboardServiceLoggerMock.Object); - // Создаем мок для AdminHandler с правильным конструктором var adminHandlerLoggerMock = new Mock>(); var adminStateServiceMock = new Mock(Mock.Of>()); - _adminHandlerMock = new Mock( + this.adminHandlerMock = new Mock( adminHandlerLoggerMock.Object, Mock.Of(), adminStateServiceMock.Object); - _botClientMock = new Mock(); + this.botClientMock = new Mock(); - _handler = new CommandHandler( - _loggerMock.Object, - _goalServiceMock.Object, - _keyboardServiceMock.Object, - _adminHandlerMock.Object); + this.handler = new CommandHandler( + this.loggerMock.Object, + this.goalServiceMock.Object, + this.keyboardServiceMock.Object, + this.adminHandlerMock.Object); } + /// + /// Tests that a command from a null user triggers a warning log and returns. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCommandAsync_NullUser_LogsWarningAndReturns() + public async Task HandleCommandAsyncNullUserLogsWarningAndReturns() { // Arrange var message = new Message { From = null, Chat = new Chat { Id = 123 }, Text = "/start" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Received command from message with null user")), + It.Is((v, t) => v.ToString() !.Contains("Received command from message with null user")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that an empty message text triggers a warning log and returns. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCommandAsync_EmptyMessageText_LogsWarningAndReturns() + public async Task HandleCommandAsyncEmptyMessageTextLogsWarningAndReturns() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = null }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Received empty message text")), + It.Is((v, t) => v.ToString() !.Contains("Received empty message text")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that the start command for a regular user sends the main menu. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCommandAsync_StartCommand_RegularUser_SendsMainMenu() + public async Task HandleCommandAsyncStartCommandRegularUserSendsMainMenu() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; var startStats = "Статистика: 5000/10000"; var keyboard = new ReplyKeyboardMarkup(new[] { new KeyboardButton[] { "💳 Пожертвовать" } }); - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetStartStats()) .ReturnsAsync(startStats); - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(123)) .ReturnsAsync(false); - _keyboardServiceMock + this.keyboardServiceMock .Setup(x => x.GetMainMenuKeyboard()) .Returns(keyboard); // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _goalServiceMock.Verify(x => x.GetStartStats(), Times.Once); - _goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); - _keyboardServiceMock.Verify(x => x.GetMainMenuKeyboard(), Times.Once); - _keyboardServiceMock.Verify(x => x.GetMainMenuKeyboardForAdmin(), Times.Never); + this.goalServiceMock.Verify(x => x.GetStartStats(), Times.Once); + this.goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); + this.keyboardServiceMock.Verify(x => x.GetMainMenuKeyboard(), Times.Once); + this.keyboardServiceMock.Verify(x => x.GetMainMenuKeyboardForAdmin(), Times.Never); } + /// + /// Tests that the start command for an admin user sends the admin menu. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCommandAsync_StartCommand_AdminUser_SendsAdminMenu() + public async Task HandleCommandAsyncStartCommandAdminUserSendsAdminMenu() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; var startStats = "Статистика: 5000/10000"; var keyboard = new ReplyKeyboardMarkup(new[] { new KeyboardButton[] { "📝 Создать новую цель" } }); - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetStartStats()) .ReturnsAsync(startStats); - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(123)) .ReturnsAsync(true); - _keyboardServiceMock + this.keyboardServiceMock .Setup(x => x.GetMainMenuKeyboardForAdmin()) .Returns(keyboard); // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); - _keyboardServiceMock.Verify(x => x.GetMainMenuKeyboardForAdmin(), Times.Once); - _keyboardServiceMock.Verify(x => x.GetMainMenuKeyboard(), Times.Never); + this.goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); + this.keyboardServiceMock.Verify(x => x.GetMainMenuKeyboardForAdmin(), Times.Once); + this.keyboardServiceMock.Verify(x => x.GetMainMenuKeyboard(), Times.Never); } + /// + /// Tests that the donate command with an active goal sends the donation keyboard. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCommandAsync_DonateCommand_WithActiveGoal_SendsDonationKeyboard() + public async Task HandleCommandAsyncDonateCommandWithActiveGoalSendsDonationKeyboard() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/donate" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; var keyboard = new InlineKeyboardMarkup(new[] { new InlineKeyboardButton[] { InlineKeyboardButton.WithCallbackData("100") } }); - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(activeGoal); - _keyboardServiceMock + this.keyboardServiceMock .Setup(x => x.GetDonationAmountKeyboard()) .Returns(keyboard); // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _goalServiceMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); - _keyboardServiceMock.Verify(x => x.GetDonationAmountKeyboard(), Times.Once); + this.goalServiceMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + this.keyboardServiceMock.Verify(x => x.GetDonationAmountKeyboard(), Times.Once); } + /// + /// Tests that the donate command without an active goal sends an error message. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCommandAsync_DonateCommand_NoActiveGoal_SendsErrorMessage() + public async Task HandleCommandAsyncDonateCommandNoActiveGoalSendsErrorMessage() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/donate" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) - .ReturnsAsync((DonationGoal)null); + .ReturnsAsync(null as DonationGoal); // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _goalServiceMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); - _keyboardServiceMock.Verify(x => x.GetDonationAmountKeyboard(), Times.Never); + this.goalServiceMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + this.keyboardServiceMock.Verify(x => x.GetDonationAmountKeyboard(), Times.Never); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("No active goal found for donate command")), + It.Is((v, t) => v.ToString() !.Contains("No active goal found for donate command")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that the stats command calls the appropriate handler. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCommandAsync_StatsCommand_CallsHandleStatsCommand() + public async Task HandleCommandAsyncStatsCommandCallsHandleStatsCommand() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/stats" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); - - // Assert - проверяем, что вызывается метод HandleStatsCommand - // Поскольку HandleStatsCommand виртуальный, мы можем проверить его вызов + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); } + /// + /// Tests that the stats command handler successfully retrieves and sends statistics. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleStatsCommand_Successful_LogsAndSendsStats() + public async Task HandleStatsCommandSuccessfulLogsAndSendsStats() { // Arrange var chatId = 456L; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; var stats = "Статистика: 5000/10000"; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetGoalStatsAsync()) .ReturnsAsync(stats); // Act - await _handler.HandleStatsCommand(_botClientMock.Object, chatId, cancellationToken); + await this.handler.HandleStatsCommand(this.botClientMock.Object, chatId, cancellationToken); // Assert - _goalServiceMock.Verify(x => x.GetGoalStatsAsync(), Times.Once); + this.goalServiceMock.Verify(x => x.GetGoalStatsAsync(), Times.Once); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Processing stats command")), + It.Is((v, t) => v.ToString() !.Contains("Processing stats command")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Statistics sent successfully")), + It.Is((v, t) => v.ToString() !.Contains("Statistics sent successfully")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that exceptions during stats command handling are properly logged. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleStatsCommand_ThrowsException_LogsError() + public async Task HandleStatsCommandThrowsExceptionLogsError() { // Arrange var chatId = 456L; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetGoalStatsAsync()) .ThrowsAsync(new Exception("Database error")); // Act - await _handler.HandleStatsCommand(_botClientMock.Object, chatId, cancellationToken); + await this.handler.HandleStatsCommand(this.botClientMock.Object, chatId, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error getting stats")), + It.Is((v, t) => v.ToString() !.Contains("Error getting stats")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that the add goal command from an admin user starts goal creation. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCommandAsync_AddGoalCommand_AdminUser_StartsGoalCreation() + public async Task HandleCommandAsyncAddGoalCommandAdminUserStartsGoalCreation() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/addgoal" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(123)) .ReturnsAsync(true); // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _adminHandlerMock.Verify( - x => x.StartGoalCreationAsync(_botClientMock.Object, 456, 123, cancellationToken), + this.adminHandlerMock.Verify( + x => x.StartGoalCreationAsync(this.botClientMock.Object, 456, 123, cancellationToken), Times.Once); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Admin user") && v.ToString().Contains("starting goal creation")), + It.Is((v, t) => v.ToString() !.Contains("Admin user") && v.ToString() !.Contains("starting goal creation")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that the add goal command from a non-admin user triggers the not admin handler. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCommandAsync_AddGoalCommand_NonAdminUser_HandlesNotAdmin() + public async Task HandleCommandAsyncAddGoalCommandNonAdminUserHandlesNotAdmin() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/addgoal" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(123)) .ReturnsAsync(false); // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _adminHandlerMock.Verify( - x => x.HandleNotAdmin(_botClientMock.Object, 456, cancellationToken), + this.adminHandlerMock.Verify( + x => x.HandleNotAdmin(this.botClientMock.Object, 456, cancellationToken), Times.Once); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Non-admin user") && v.ToString().Contains("attempted to create goal")), + It.Is((v, t) => v.ToString() !.Contains("Non-admin user") && v.ToString() !.Contains("attempted to create goal")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that an unknown command from an admin user sends admin help. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCommandAsync_UnknownCommand_AdminUser_SendsAdminHelp() + public async Task HandleCommandAsyncUnknownCommandAdminUserSendsAdminHelp() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "unknown_command" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(123)) .ReturnsAsync(true); // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); + this.goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Unknown command from user")), + It.Is((v, t) => v.ToString() !.Contains("Unknown command from user")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that an unknown command from a regular user sends regular help. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCommandAsync_UnknownCommand_RegularUser_SendsRegularHelp() + public async Task HandleCommandAsyncUnknownCommandRegularUserSendsRegularHelp() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "unknown_command" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(123)) .ReturnsAsync(false); // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); + this.goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Unknown command from user")), + It.Is((v, t) => v.ToString() !.Contains("Unknown command from user")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that various start command variants all call the start command handler. + /// + /// The command variant to test. + /// A representing the asynchronous unit test. [TestCase("/start")] [TestCase("🔄 Обновить")] [TestCase("Обновить")] - public async Task HandleCommandAsync_StartCommandVariants_CallsHandleStartCommand(string command) + public async Task HandleCommandAsyncStartCommandVariantsCallsHandleStartCommand(string command) { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = command }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetStartStats()) .ReturnsAsync("Статистика"); - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(123)) .ReturnsAsync(false); - _keyboardServiceMock + this.keyboardServiceMock .Setup(x => x.GetMainMenuKeyboard()) .Returns(new ReplyKeyboardMarkup(new KeyboardButton[0][])); // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _goalServiceMock.Verify(x => x.GetStartStats(), Times.Once); + this.goalServiceMock.Verify(x => x.GetStartStats(), Times.Once); } + /// + /// Tests that various donate command variants all call the donate command handler. + /// + /// The command variant to test. + /// A representing the asynchronous unit test. [TestCase("/donate")] [TestCase("💳 Пожертвовать")] [TestCase("Пожертвовать")] - public async Task HandleCommandAsync_DonateCommandVariants_CallsHandleDonateCommand(string command) + public async Task HandleCommandAsyncDonateCommandVariantsCallsHandleDonateCommand(string command) { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = command }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(activeGoal); - _keyboardServiceMock + this.keyboardServiceMock .Setup(x => x.GetDonationAmountKeyboard()) .Returns(new InlineKeyboardMarkup(new InlineKeyboardButton[0][])); // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _goalServiceMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); - _keyboardServiceMock.Verify(x => x.GetDonationAmountKeyboard(), Times.Once); + this.goalServiceMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + this.keyboardServiceMock.Verify(x => x.GetDonationAmountKeyboard(), Times.Once); } + /// + /// Tests that various stats command aliases all call the stats command handler. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCommandAsync_StatsCommandAliases_AllCallHandleStatsCommand() + public async Task HandleCommandAsyncStatsCommandAliasesAllCallHandleStatsCommand() { // Arrange var user = new User { Id = 123 }; var aliases = new[] { "/stats", "📊 Статистика", "Статистика" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; foreach (var alias in aliases) { var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = alias }; // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); - - // Assert - проверяем, что команда обрабатывается без ошибок + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); } } + /// + /// Tests that various add goal command aliases all call the add goal command handler. + /// + /// The command variant to test. + /// A representing the asynchronous unit test. [TestCase("/addgoal")] [TestCase("📝 Создать новую цель")] [TestCase("Создать новую цель")] - public async Task HandleCommandAsync_AddGoalCommandAliases_AllCallHandleAddGoalCommand(string command) + public async Task HandleCommandAsyncAddGoalCommandAliasesAllCallHandleAddGoalCommand(string command) { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = command }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(123)) .ReturnsAsync(false); // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); - + this.goalServiceMock.Verify(x => x.IsUserAdminAsync(123), Times.Once); } + /// + /// Tests that exceptions during command handling are properly logged. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCommandAsync_ThrowsException_LogsErrorAndSendsErrorMessage() + public async Task HandleCommandAsyncThrowsExceptionLogsErrorAndSendsErrorMessage() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetStartStats()) .ThrowsAsync(new Exception("Test exception")); // Act - await _handler.HandleCommandAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error processing command")), + It.Is((v, t) => v.ToString() !.Contains("Error processing command")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Never); } } \ No newline at end of file diff --git a/TestBot/Bot/Bot.Hadler/MessageHandlerTests.cs b/TestBot/Bot/Bot.Hadler/MessageHandlerTests.cs index 20ec42c..3559ea4 100755 --- a/TestBot/Bot/Bot.Hadler/MessageHandlerTests.cs +++ b/TestBot/Bot/Bot.Hadler/MessageHandlerTests.cs @@ -1,4 +1,8 @@ -using Bot.Handlers; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot.Handlers; using Bot.Services; using Configurations; using Data.Models; @@ -13,34 +17,40 @@ namespace Bot.Tests.Handlers; +/// +/// Unit tests for the class. +/// [TestFixture] public class MessageHandlerTests { - private Mock> _loggerMock; - private Mock _donationServiceMock; - private Mock _goalServiceMock; - private Mock _commandHandlerMock; - private Mock _paymentHandlerMock; - private Mock _userStateServiceMock; - private Mock _adminHandlerMock; - private Mock _adminStateServiceMock; - private Mock _botClientMock; - private MessageHandler _handler; - + private Mock> loggerMock; + private Mock donationServiceMock; + private Mock goalServiceMock; + private Mock commandHandlerMock; + private Mock paymentHandlerMock; + private Mock userStateServiceMock; + private Mock adminHandlerMock; + private Mock adminStateServiceMock; + private Mock botClientMock; + private MessageHandler handler; + + /// + /// Sets up the test environment before each test execution. + /// [SetUp] public void Setup() { - _loggerMock = new Mock>(); - _donationServiceMock = new Mock(); - _goalServiceMock = new Mock(); + this.loggerMock = new Mock>(); + this.donationServiceMock = new Mock(); + this.goalServiceMock = new Mock(); - // Явно создаем моки для всех зависимостей, как в предыдущем SetUp + // Create mocks for all dependencies explicitly, as in the previous Setup - // Создаем мок логгера для UserStateService + // Create mock logger for UserStateService var userStateServiceLoggerMock = new Mock>(); - _userStateServiceMock = new Mock(userStateServiceLoggerMock.Object); + this.userStateServiceMock = new Mock(userStateServiceLoggerMock.Object); - // Создаем моки для зависимостей PaymentHandler (как в предыдущем SetUp) + // Create mocks for PaymentHandler dependencies (as in previous Setup) var paymentLoggerMock = new Mock>(); var paymentGoalServiceMock = new Mock(); var paymentDonationServiceMock = new Mock(); @@ -51,385 +61,431 @@ public void Setup() PaymentProviderToken = "test-token", }); - _paymentHandlerMock = new Mock( + this.paymentHandlerMock = new Mock( paymentLoggerMock.Object, paymentGoalServiceMock.Object, paymentDonationServiceMock.Object, userStateServiceForPaymentMock.Object, botConfigMock.Object); - // Создаем моки для зависимостей CommandHandler + // Create mocks for CommandHandler dependencies var commandLoggerMock = new Mock>(); var commandGoalServiceMock = new Mock(); var keyboardServiceMock = new Mock(Mock.Of>()); - // Создаем мок для AdminStateService + // Create mock for AdminStateService var adminStateServiceLoggerMock = new Mock>(); - _adminStateServiceMock = new Mock(adminStateServiceLoggerMock.Object); + this.adminStateServiceMock = new Mock(adminStateServiceLoggerMock.Object); - // Создаем мок для AdminHandler - _adminHandlerMock = new Mock( + // Create mock for AdminHandler + this.adminHandlerMock = new Mock( Mock.Of>(), Mock.Of(), - _adminStateServiceMock.Object); + this.adminStateServiceMock.Object); - _commandHandlerMock = new Mock( + this.commandHandlerMock = new Mock( commandLoggerMock.Object, commandGoalServiceMock.Object, keyboardServiceMock.Object, - _adminHandlerMock.Object); + this.adminHandlerMock.Object); - _botClientMock = new Mock(); + this.botClientMock = new Mock(); - // Настраиваем базовые методы - _donationServiceMock + // Configure base methods + this.donationServiceMock .Setup(x => x.GetOrCreateUserAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.FromResult(new Data.Models.Users { Id = 123, Username = "testuser", FirstName = "Test", - LastName = "User" - })); // Исправлено - возвращаем User + LastName = "User", + })); // Fixed - returning User - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(It.IsAny())) .ReturnsAsync(false); - _commandHandlerMock + this.commandHandlerMock .Setup(x => x.HandleCommandAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); - _paymentHandlerMock + this.paymentHandlerMock .Setup(x => x.HandleSuccessfulPaymentAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); - _paymentHandlerMock + this.paymentHandlerMock .Setup(x => x.HandleCustomAmountInputAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); - _adminHandlerMock + this.adminHandlerMock .Setup(x => x.HandleAdminGoalCreationAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); - _handler = new MessageHandler( - _loggerMock.Object, - _donationServiceMock.Object, - _goalServiceMock.Object, - _commandHandlerMock.Object, - _paymentHandlerMock.Object, - _userStateServiceMock.Object, - _adminHandlerMock.Object, - _adminStateServiceMock.Object); + this.handler = new MessageHandler( + this.loggerMock.Object, + this.donationServiceMock.Object, + this.goalServiceMock.Object, + this.commandHandlerMock.Object, + this.paymentHandlerMock.Object, + this.userStateServiceMock.Object, + this.adminHandlerMock.Object, + this.adminStateServiceMock.Object); } + /// + /// Tests that the handler correctly identifies updates with messages. + /// [Test] - public void CanHandle_WithMessage_ReturnsTrue() + public void CanHandleWithMessageReturnsTrue() { // Arrange var update = new Update { Message = new Message() }; // Act - var result = _handler.CanHandle(update); + var result = this.handler.CanHandle(update); // Assert Assert.That(result, Is.True); } + /// + /// Tests that the handler correctly identifies updates without messages. + /// [Test] - public void CanHandle_WithoutMessage_ReturnsFalse() + public void CanHandleWithoutMessageReturnsFalse() { // Arrange var update = new Update { Message = null }; // Act - var result = _handler.CanHandle(update); + var result = this.handler.CanHandle(update); // Assert Assert.That(result, Is.False); } + /// + /// Tests that a message with a null user triggers a warning log and returns. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_MessageWithNullUser_LogsWarningAndReturns() + public async Task HandleAsyncMessageWithNullUserLogsWarningAndReturns() { // Arrange var message = new Message { From = null, Chat = new Chat { Id = 123 } }; var update = new Update { Message = message }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Received message with null user information")), + It.Is((v, t) => v.ToString() !.Contains("Received message with null user information")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); - // Проверяем, что бизнес-логика не вызывалась - _donationServiceMock.Verify( + // Verify that business logic is not called + this.donationServiceMock.Verify( x => x.GetOrCreateUserAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } + /// + /// Tests that a valid message triggers user registration or update. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_ValidMessage_RegistersOrUpdatesUser() + public async Task HandleAsyncValidMessageRegistersOrUpdatesUser() { // Arrange var user = new User { Id = 123, Username = "testuser", FirstName = "Test", LastName = "User" }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "test" }; var update = new Update { Message = message }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _donationServiceMock.Verify( + this.donationServiceMock.Verify( x => x.GetOrCreateUserAsync(123, "testuser", "Test", "User"), Times.Once); } + /// + /// Tests that a message with successful payment triggers payment processing. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_SuccessfulPayment_ProcessesPayment() + public async Task HandleAsyncSuccessfulPaymentProcessesPayment() { // Arrange var user = new User { Id = 123 }; var successfulPayment = new SuccessfulPayment(); var message = new Message { From = user, Chat = new Chat { Id = 456 }, SuccessfulPayment = successfulPayment }; var update = new Update { Message = message }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _paymentHandlerMock.Verify( - x => x.HandleSuccessfulPaymentAsync(_botClientMock.Object, message, cancellationToken), + this.paymentHandlerMock.Verify( + x => x.HandleSuccessfulPaymentAsync(this.botClientMock.Object, message, cancellationToken), Times.Once); - // Проверяем, что дальнейшая обработка не происходит - _commandHandlerMock.Verify( + // Verify that further processing does not occur + this.commandHandlerMock.Verify( x => x.HandleCommandAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } + /// + /// Tests that a message from a user waiting for amount triggers custom amount processing. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_UserWaitingForAmount_ProcessesCustomAmount() + public async Task HandleAsyncUserWaitingForAmountProcessesCustomAmount() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "500" }; var update = new Update { Message = message }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _userStateServiceMock + this.userStateServiceMock .Setup(x => x.IsWaitingForAmount(123, 456)) .Returns(true); // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _paymentHandlerMock.Verify( - x => x.HandleCustomAmountInputAsync(_botClientMock.Object, message, cancellationToken), + this.paymentHandlerMock.Verify( + x => x.HandleCustomAmountInputAsync(this.botClientMock.Object, message, cancellationToken), Times.Once); - // Проверяем, что обычная обработка команд не происходит - _commandHandlerMock.Verify( + // Verify that regular command processing does not occur + this.commandHandlerMock.Verify( x => x.HandleCommandAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } + /// + /// Tests that a message from an admin creating a goal triggers goal creation processing. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_AdminCreatingGoal_ProcessesGoalCreation() + public async Task HandleAsyncAdminCreatingGoalProcessesGoalCreation() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "Новая цель" }; var update = new Update { Message = message }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _userStateServiceMock + this.userStateServiceMock .Setup(x => x.IsWaitingForAmount(123, 456)) .Returns(false); - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(123)) .ReturnsAsync(true); - _adminStateServiceMock + this.adminStateServiceMock .Setup(x => x.IsUserCreatingGoal(123)) .Returns(true); // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _adminHandlerMock.Verify( - x => x.HandleAdminGoalCreationAsync(_botClientMock.Object, message, cancellationToken), + this.adminHandlerMock.Verify( + x => x.HandleAdminGoalCreationAsync(this.botClientMock.Object, message, cancellationToken), Times.Once); - // Проверяем, что обычная обработка команд не происходит - _commandHandlerMock.Verify( + // Verify that regular command processing does not occur + this.commandHandlerMock.Verify( x => x.HandleCommandAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } + /// + /// Tests that a regular text message triggers command processing. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_RegularTextMessage_ProcessesCommand() + public async Task HandleAsyncRegularTextMessageProcessesCommand() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; var update = new Update { Message = message }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _userStateServiceMock + this.userStateServiceMock .Setup(x => x.IsWaitingForAmount(123, 456)) .Returns(false); - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(123)) .ReturnsAsync(false); // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _commandHandlerMock.Verify( - x => x.HandleCommandAsync(_botClientMock.Object, message, cancellationToken), + this.commandHandlerMock.Verify( + x => x.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken), Times.Once); } + /// + /// Tests that a non-text message does not trigger command processing. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_NonTextMessage_DoesNotProcessCommand() + public async Task HandleAsyncNonTextMessageDoesNotProcessCommand() { // Arrange var user = new User { Id = 123 }; - var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = null }; // Не текстовое сообщение + var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = null }; // Non-text message var update = new Update { Message = message }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _userStateServiceMock + this.userStateServiceMock .Setup(x => x.IsWaitingForAmount(123, 456)) .Returns(false); - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(123)) .ReturnsAsync(false); // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _commandHandlerMock.Verify( + this.commandHandlerMock.Verify( x => x.HandleCommandAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } + /// + /// Tests that when user registration fails, error is logged but processing continues. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_UserRegistrationFails_LogsErrorButContinues() + public async Task HandleAsyncUserRegistrationFailsLogsErrorButContinues() { // Arrange var user = new User { Id = 123, Username = "testuser", FirstName = "Test", LastName = "User" }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; var update = new Update { Message = message }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _donationServiceMock + this.donationServiceMock .Setup(x => x.GetOrCreateUserAsync(123, "testuser", "Test", "User")) .ThrowsAsync(new Exception("Database error")); // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Failed to register/update user")), + It.Is((v, t) => v.ToString() !.Contains("Failed to register/update user")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); - // Проверяем, что обработка команды все равно происходит - _commandHandlerMock.Verify( - x => x.HandleCommandAsync(_botClientMock.Object, message, cancellationToken), + // Verify that command processing still occurs + this.commandHandlerMock.Verify( + x => x.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken), Times.Once); } + /// + /// Tests that when command handler throws an exception, error is logged. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_CommandHandlerThrowsException_LogsError() + public async Task HandleAsyncCommandHandlerThrowsExceptionLogsError() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; var update = new Update { Message = message }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _userStateServiceMock + this.userStateServiceMock .Setup(x => x.IsWaitingForAmount(123, 456)) .Returns(false); - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(123)) .ReturnsAsync(false); - _commandHandlerMock + this.commandHandlerMock .Setup(x => x.HandleCommandAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ThrowsAsync(new Exception("Command processing failed")); // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error processing text message")), + It.Is((v, t) => v.ToString() !.Contains("Error processing text message")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that when an admin is not creating a goal, message is processed as regular command. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_AdminNotCreatingGoal_ProcessesAsRegularCommand() + public async Task HandleAsyncAdminNotCreatingGoalProcessesAsRegularCommand() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "/start" }; var update = new Update { Message = message }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _userStateServiceMock + this.userStateServiceMock .Setup(x => x.IsWaitingForAmount(123, 456)) .Returns(false); - _goalServiceMock + this.goalServiceMock .Setup(x => x.IsUserAdminAsync(123)) - .ReturnsAsync(true); // Пользователь - админ + .ReturnsAsync(true); // User is admin - _adminStateServiceMock + this.adminStateServiceMock .Setup(x => x.IsUserCreatingGoal(123)) - .Returns(false); // Но не создает цель + .Returns(false); // But not creating a goal // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _adminHandlerMock.Verify( + this.adminHandlerMock.Verify( x => x.HandleAdminGoalCreationAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - _commandHandlerMock.Verify( - x => x.HandleCommandAsync(_botClientMock.Object, message, cancellationToken), + this.commandHandlerMock.Verify( + x => x.HandleCommandAsync(this.botClientMock.Object, message, cancellationToken), Times.Once); } } \ No newline at end of file diff --git a/TestBot/Bot/Bot.Hadler/PaymentHandlerTests.cs b/TestBot/Bot/Bot.Hadler/PaymentHandlerTests.cs index 34359da..1fcb64e 100755 --- a/TestBot/Bot/Bot.Hadler/PaymentHandlerTests.cs +++ b/TestBot/Bot/Bot.Hadler/PaymentHandlerTests.cs @@ -13,292 +13,342 @@ namespace Bot.Tests.Handlers; +/// +/// Unit tests for the class. +/// [TestFixture] public class PaymentHandlerTests { - private Mock> _loggerMock; - private Mock _goalServiceMock; - private Mock _donationServiceMock; - private Mock _userStateServiceMock; - private Mock _botClientMock; - private PaymentHandler _handler; - private BotConfig _botConfig; - + private Mock> loggerMock; + private Mock goalServiceMock; + private Mock donationServiceMock; + private Mock userStateServiceMock; + private Mock botClientMock; + private PaymentHandler handler; + private BotConfig botConfig; + + /// + /// Sets up the test environment before each test execution. + /// [SetUp] public void Setup() { - _loggerMock = new Mock>(); - _goalServiceMock = new Mock(); - _donationServiceMock = new Mock(); + this.loggerMock = new Mock>(); + this.goalServiceMock = new Mock(); + this.donationServiceMock = new Mock(); - // Создаем мок для UserStateService с правильным конструктором + // Create mock for UserStateService with correct constructor var userStateServiceLoggerMock = new Mock>(); - _userStateServiceMock = new Mock(userStateServiceLoggerMock.Object); + this.userStateServiceMock = new Mock(userStateServiceLoggerMock.Object); - _botClientMock = new Mock(); + this.botClientMock = new Mock(); - // Настраиваем конфигурацию бота - _botConfig = new BotConfig { PaymentProviderToken = "test-payment-token" }; - var botConfigOptions = Options.Create(_botConfig); + // Configure bot configuration + this.botConfig = new BotConfig { PaymentProviderToken = "test-payment-token" }; + var botConfigOptions = Options.Create(this.botConfig); - _handler = new PaymentHandler( - _loggerMock.Object, - _goalServiceMock.Object, - _donationServiceMock.Object, - _userStateServiceMock.Object, + this.handler = new PaymentHandler( + this.loggerMock.Object, + this.goalServiceMock.Object, + this.donationServiceMock.Object, + this.userStateServiceMock.Object, botConfigOptions); } + /// + /// Tests that custom amount input with null user triggers a warning log. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCustomAmountInputAsync_NullUser_LogsWarningAndReturns() + public async Task HandleCustomAmountInputAsyncNullUserLogsWarningAndReturns() { // Arrange var message = new Message { From = null, Chat = new Chat { Id = 123 }, Text = "500" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleCustomAmountInputAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCustomAmountInputAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Received custom amount input from null user")), + It.Is((v, t) => v.ToString() !.Contains("Received custom amount input from null user")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that empty text input removes user state and sends error message. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCustomAmountInputAsync_EmptyText_RemovesStateAndSendsErrorMessage() + public async Task HandleCustomAmountInputAsyncEmptyTextRemovesStateAndSendsErrorMessage() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = null }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleCustomAmountInputAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCustomAmountInputAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _userStateServiceMock.Verify(x => x.RemoveWaitingForAmount(123), Times.Once); + this.userStateServiceMock.Verify(x => x.RemoveWaitingForAmount(123), Times.Once); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("sent empty custom amount")), + It.Is((v, t) => v.ToString() !.Contains("sent empty custom amount")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that invalid number input removes user state and sends error message. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCustomAmountInputAsync_InvalidNumber_RemovesStateAndSendsErrorMessage() + public async Task HandleCustomAmountInputAsyncInvalidNumberRemovesStateAndSendsErrorMessage() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "invalid" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleCustomAmountInputAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCustomAmountInputAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _userStateServiceMock.Verify(x => x.RemoveWaitingForAmount(123), Times.Once); + this.userStateServiceMock.Verify(x => x.RemoveWaitingForAmount(123), Times.Once); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("sent invalid custom amount")), + It.Is((v, t) => v.ToString() !.Contains("sent invalid custom amount")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that valid amount input calls CreateDonationInvoice method. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleCustomAmountInputAsync_ValidAmount_CallsCreateDonationInvoice() + public async Task HandleCustomAmountInputAsyncValidAmountCallsCreateDonationInvoice() { // Arrange var user = new User { Id = 123 }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, Text = "500" }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(activeGoal); // Act - await _handler.HandleCustomAmountInputAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleCustomAmountInputAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _userStateServiceMock.Verify(x => x.RemoveWaitingForAmount(123), Times.Once); + this.userStateServiceMock.Verify(x => x.RemoveWaitingForAmount(123), Times.Once); } + /// + /// Tests that donation creation without active goal sends error message. + /// + /// A representing the asynchronous unit test. [Test] - public async Task CreateDonationInvoice_NoActiveGoal_SendsErrorMessage() + public async Task CreateDonationInvoiceNoActiveGoalSendsErrorMessage() { // Arrange var chatId = 456L; var userId = 123L; var amount = 500; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) - .ReturnsAsync((DonationGoal)null); + .ReturnsAsync(null as DonationGoal); // Act - await _handler.CreateDonationInvoice(_botClientMock.Object, chatId, userId, amount, cancellationToken); + await this.handler.CreateDonationInvoice(this.botClientMock.Object, chatId, userId, amount, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("No active goal found for donation")), + It.Is((v, t) => v.ToString() !.Contains("No active goal found for donation")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that donation amount below minimum sends validation message. + /// + /// A representing the asynchronous unit test. [Test] - public async Task CreateDonationInvoice_AmountBelowMinimum_SendsValidationMessage() + public async Task CreateDonationInvoiceAmountBelowMinimumSendsValidationMessage() { // Arrange var chatId = 456L; var userId = 123L; - var amount = 50; // Ниже минимума 60 - var cancellationToken = new CancellationToken(); + var amount = 50; // Below minimum 60 + var cancellationToken = CancellationToken.None; var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(activeGoal); // Act - await _handler.CreateDonationInvoice(_botClientMock.Object, chatId, userId, amount, cancellationToken); + await this.handler.CreateDonationInvoice(this.botClientMock.Object, chatId, userId, amount, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("below minimum limit")), + It.Is((v, t) => v.ToString() !.Contains("below minimum limit")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that donation amount above maximum sends validation message. + /// + /// A representing the asynchronous unit test. [Test] - public async Task CreateDonationInvoice_AmountAboveMaximum_SendsValidationMessage() + public async Task CreateDonationInvoiceAmountAboveMaximumSendsValidationMessage() { // Arrange var chatId = 456L; var userId = 123L; - var amount = 100001; // Выше максимума 100000 - var cancellationToken = new CancellationToken(); + var amount = 100001; // Above maximum 100000 + var cancellationToken = CancellationToken.None; var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(activeGoal); // Act - await _handler.CreateDonationInvoice(_botClientMock.Object, chatId, userId, amount, cancellationToken); + await this.handler.CreateDonationInvoice(this.botClientMock.Object, chatId, userId, amount, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("exceeds maximum limit")), + It.Is((v, t) => v.ToString() !.Contains("exceeds maximum limit")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that valid donation amount logs success. + /// + /// A representing the asynchronous unit test. [Test] - public async Task CreateDonationInvoice_ValidAmount_LogsSuccess() + public async Task CreateDonationInvoiceValidAmountLogsSuccess() { // Arrange var chatId = 456L; var userId = 123L; var amount = 500; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(activeGoal); // Act - await _handler.CreateDonationInvoice(_botClientMock.Object, chatId, userId, amount, cancellationToken); + await this.handler.CreateDonationInvoice(this.botClientMock.Object, chatId, userId, amount, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Creating donation invoice")), + It.Is((v, t) => v.ToString() !.Contains("Creating donation invoice")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that goal service exception logs error and sends error message. + /// + /// A representing the asynchronous unit test. [Test] - public async Task CreateDonationInvoice_GoalServiceThrows_LogsErrorAndSendsErrorMessage() + public async Task CreateDonationInvoiceGoalServiceThrowsLogsErrorAndSendsErrorMessage() { // Arrange var chatId = 456L; var userId = 123L; var amount = 500; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) .ThrowsAsync(new Exception("Database error")); // Act - await _handler.CreateDonationInvoice(_botClientMock.Object, chatId, userId, amount, cancellationToken); + await this.handler.CreateDonationInvoice(this.botClientMock.Object, chatId, userId, amount, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error creating donation invoice")), + It.Is((v, t) => v.ToString() !.Contains("Error creating donation invoice")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that successful payment with null payment or user logs warning. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleSuccessfulPaymentAsync_NullPaymentOrUser_LogsWarningAndReturns() + public async Task HandleSuccessfulPaymentAsyncNullPaymentOrUserLogsWarningAndReturns() { // Arrange var message = new Message { From = null, Chat = new Chat { Id = 123 }, SuccessfulPayment = null }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleSuccessfulPaymentAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleSuccessfulPaymentAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Received successful payment with null payment or user information")), + It.Is((v, t) => v.ToString() !.Contains("Received successful payment with null payment or user information")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that valid successful payment processes donation. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleSuccessfulPaymentAsync_ValidPayment_ProcessesDonation() + public async Task HandleSuccessfulPaymentAsyncValidPaymentProcessesDonation() { // Arrange var user = new User { Id = 123 }; @@ -306,7 +356,7 @@ public async Task HandleSuccessfulPaymentAsync_ValidPayment_ProcessesDonation() { TelegramPaymentChargeId = "charge_123", TotalAmount = 50000, // 500.00 RUB - Currency = "RUB" + Currency = "RUB", }; var message = new Message { @@ -314,32 +364,36 @@ public async Task HandleSuccessfulPaymentAsync_ValidPayment_ProcessesDonation() Chat = new Chat { Id = 456 }, SuccessfulPayment = successfulPayment, }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _donationServiceMock + this.donationServiceMock .Setup(x => x.ProcessDonationAsync(123, 500, "RUB", "charge_123")) .ReturnsAsync(true); // Act - await _handler.HandleSuccessfulPaymentAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleSuccessfulPaymentAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _donationServiceMock.Verify( + this.donationServiceMock.Verify( x => x.ProcessDonationAsync(123, 500, "RUB", "charge_123"), Times.Once); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Successfully processed donation")), + It.Is((v, t) => v.ToString() !.Contains("Successfully processed donation")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that donation service returning false logs error and sends error message. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleSuccessfulPaymentAsync_DonationServiceReturnsFalse_LogsErrorAndSendsErrorMessage() + public async Task HandleSuccessfulPaymentAsyncDonationServiceReturnsFalseLogsErrorAndSendsErrorMessage() { // Arrange var user = new User { Id = 123 }; @@ -347,7 +401,7 @@ public async Task HandleSuccessfulPaymentAsync_DonationServiceReturnsFalse_LogsE { TelegramPaymentChargeId = "charge_123", TotalAmount = 50000, - Currency = "RUB" + Currency = "RUB", }; var message = new Message { @@ -355,28 +409,32 @@ public async Task HandleSuccessfulPaymentAsync_DonationServiceReturnsFalse_LogsE Chat = new Chat { Id = 456 }, SuccessfulPayment = successfulPayment, }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _donationServiceMock + this.donationServiceMock .Setup(x => x.ProcessDonationAsync(123, 500, "RUB", "charge_123")) .ReturnsAsync(false); // Act - await _handler.HandleSuccessfulPaymentAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleSuccessfulPaymentAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Failed to process donation")), + It.Is((v, t) => v.ToString() !.Contains("Failed to process donation")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that donation service exception logs error and sends error message. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleSuccessfulPaymentAsync_DonationServiceThrows_LogsErrorAndSendsErrorMessage() + public async Task HandleSuccessfulPaymentAsyncDonationServiceThrowsLogsErrorAndSendsErrorMessage() { // Arrange var user = new User { Id = 123 }; @@ -384,7 +442,7 @@ public async Task HandleSuccessfulPaymentAsync_DonationServiceThrows_LogsErrorAn { TelegramPaymentChargeId = "charge_123", TotalAmount = 50000, - Currency = "RUB" + Currency = "RUB", }; var message = new Message { @@ -392,28 +450,32 @@ public async Task HandleSuccessfulPaymentAsync_DonationServiceThrows_LogsErrorAn Chat = new Chat { Id = 456 }, SuccessfulPayment = successfulPayment, }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _donationServiceMock + this.donationServiceMock .Setup(x => x.ProcessDonationAsync(123, 500, "RUB", "charge_123")) .ThrowsAsync(new Exception("Database error")); // Act - await _handler.HandleSuccessfulPaymentAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleSuccessfulPaymentAsync(this.botClientMock.Object, message, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error handling successful payment")), + It.Is((v, t) => v.ToString() !.Contains("Error handling successful payment")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that valid successful payment deletes invoice message. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleSuccessfulPaymentAsync_ValidPayment_DeletesInvoiceMessage() + public async Task HandleSuccessfulPaymentAsyncValidPaymentDeletesInvoiceMessage() { // Arrange var user = new User { Id = 123 }; @@ -421,35 +483,38 @@ public async Task HandleSuccessfulPaymentAsync_ValidPayment_DeletesInvoiceMessag { TelegramPaymentChargeId = "charge_123", TotalAmount = 50000, - Currency = "RUB" + Currency = "RUB", }; var message = new Message { From = user, Chat = new Chat { Id = 456 }, - //MessageId = 789, - SuccessfulPayment = successfulPayment + SuccessfulPayment = successfulPayment, }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _donationServiceMock + this.donationServiceMock .Setup(x => x.ProcessDonationAsync(123, 500, "RUB", "charge_123")) .ReturnsAsync(true); // Act - await _handler.HandleSuccessfulPaymentAsync(_botClientMock.Object, message, cancellationToken); + await this.handler.HandleSuccessfulPaymentAsync(this.botClientMock.Object, message, cancellationToken); } + /// + /// Tests that donation amount boundary values are validated correctly. + /// + /// A representing the asynchronous unit test. [Test] - public async Task CreateDonationInvoice_AmountBoundaryValues_ValidatesCorrectly() + public async Task CreateDonationInvoiceAmountBoundaryValuesValidatesCorrectly() { // Arrange var chatId = 456L; var userId = 123L; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal" }; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(activeGoal); @@ -462,24 +527,24 @@ public async Task CreateDonationInvoice_AmountBoundaryValues_ValidatesCorrectly( new { Amount = 100001, ShouldBeValid = false }, new { Amount = 500, ShouldBeValid = true }, new { Amount = 0, ShouldBeValid = false }, - new { Amount = -100, ShouldBeValid = false } + new { Amount = -100, ShouldBeValid = false }, }; foreach (var testCase in testCases) { // Act - await _handler.CreateDonationInvoice(_botClientMock.Object, chatId, userId, testCase.Amount, cancellationToken); + await this.handler.CreateDonationInvoice(this.botClientMock.Object, chatId, userId, testCase.Amount, cancellationToken); // Assert if (!testCase.ShouldBeValid) { - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("provided invalid donation amount")), + It.Is((v, t) => v.ToString() !.Contains("provided invalid donation amount")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.AtLeastOnce); } } diff --git a/TestBot/Bot/Bot.Hadler/PreCheckoutQueryHandlerTests.cs b/TestBot/Bot/Bot.Hadler/PreCheckoutQueryHandlerTests.cs index 18091b8..42d5e4f 100755 --- a/TestBot/Bot/Bot.Hadler/PreCheckoutQueryHandlerTests.cs +++ b/TestBot/Bot/Bot.Hadler/PreCheckoutQueryHandlerTests.cs @@ -10,54 +10,70 @@ namespace Bot.Tests.Handlers; +/// +/// Unit tests for the class. +/// [TestFixture] public class PreCheckoutQueryHandlerTests { - private Mock> _loggerMock; - private Mock _goalServiceMock; - private Mock _botClientMock; - private PreCheckoutQueryHandler _handler; - + private Mock> loggerMock; + private Mock goalServiceMock; + private Mock botClientMock; + private PreCheckoutQueryHandler handler; + + /// + /// Sets up the test environment before each test execution. + /// [SetUp] public void Setup() { - _loggerMock = new Mock>(); - _goalServiceMock = new Mock(); - _botClientMock = new Mock(); + this.loggerMock = new Mock>(); + this.goalServiceMock = new Mock(); + this.botClientMock = new Mock(); - _handler = new PreCheckoutQueryHandler( - _loggerMock.Object, - _goalServiceMock.Object); + this.handler = new PreCheckoutQueryHandler( + this.loggerMock.Object, + this.goalServiceMock.Object); } + /// + /// Tests that the handler correctly identifies updates with pre-checkout queries. + /// [Test] - public void CanHandle_WithPreCheckoutQuery_ReturnsTrue() + public void CanHandleWithPreCheckoutQueryReturnsTrue() { // Arrange var update = new Update { PreCheckoutQuery = new PreCheckoutQuery() }; // Act - var result = _handler.CanHandle(update); + var result = this.handler.CanHandle(update); // Assert Assert.That(result, Is.True); } + /// + /// Tests that the handler correctly identifies updates without pre-checkout queries. + /// [Test] - public void CanHandle_WithoutPreCheckoutQuery_ReturnsFalse() + public void CanHandleWithoutPreCheckoutQueryReturnsFalse() { // Arrange var update = new Update { PreCheckoutQuery = null }; // Act - var result = _handler.CanHandle(update); + var result = this.handler.CanHandle(update); // Assert Assert.That(result, Is.False); } + /// + /// Tests that a pre-checkout query with null user triggers a warning log and returns. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_PreCheckoutQueryWithNullUser_LogsWarningAndReturns() + public async Task HandleAsyncPreCheckoutQueryWithNullUserLogsWarningAndReturns() { // Arrange var preCheckoutQuery = new PreCheckoutQuery @@ -66,32 +82,36 @@ public async Task HandleAsync_PreCheckoutQueryWithNullUser_LogsWarningAndReturns From = null, // Null user InvoicePayload = "test_payload", TotalAmount = 10000, // 100.00 RUB - Currency = "RUB" + Currency = "RUB", }; var update = new Update { PreCheckoutQuery = preCheckoutQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Received pre-checkout query with null user information")), + It.Is((v, t) => v.ToString() !.Contains("Received pre-checkout query with null user information")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); - // Проверяем, что бизнес-логика не вызывалась - _goalServiceMock.Verify( + // Verify that business logic is not called + this.goalServiceMock.Verify( x => x.GetActiveGoalAsync(), Times.Never); } + /// + /// Tests that a pre-checkout query with an active goal is approved. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_WithActiveGoal_ApprovesPreCheckoutQuery() + public async Task HandleAsyncWithActiveGoalApprovesPreCheckoutQuery() { // Arrange var user = new User { Id = 123 }; @@ -101,32 +121,36 @@ public async Task HandleAsync_WithActiveGoal_ApprovesPreCheckoutQuery() From = user, InvoicePayload = "test_payload", TotalAmount = 10000, // 100.00 RUB - Currency = "RUB" + Currency = "RUB", }; var update = new Update { PreCheckoutQuery = preCheckoutQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal", TargetAmount = 100000 }; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(activeGoal); // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Approved pre-checkout query")), + It.Is((v, t) => v.ToString() !.Contains("Approved pre-checkout query")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that a pre-checkout query without an active goal is rejected. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_WithoutActiveGoal_RejectsPreCheckoutQuery() + public async Task HandleAsyncWithoutActiveGoalRejectsPreCheckoutQuery() { // Arrange var user = new User { Id = 123 }; @@ -136,31 +160,35 @@ public async Task HandleAsync_WithoutActiveGoal_RejectsPreCheckoutQuery() From = user, InvoicePayload = "test_payload", TotalAmount = 10000, - Currency = "RUB" + Currency = "RUB", }; var update = new Update { PreCheckoutQuery = preCheckoutQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) - .ReturnsAsync((DonationGoal)null); // Нет активной цели + .ReturnsAsync(null as DonationGoal); // No active goal // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Rejected pre-checkout query") && v.ToString().Contains("no active goal found")), + It.Is((v, t) => v.ToString() !.Contains("Rejected pre-checkout query") && v.ToString() !.Contains("no active goal found")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that exceptions in goal service are logged and handled safely. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_GoalServiceThrowsException_LogsErrorAndCallsSafeAnswer() + public async Task HandleAsyncGoalServiceThrowsExceptionLogsErrorAndCallsSafeAnswer() { // Arrange var user = new User { Id = 123 }; @@ -170,31 +198,35 @@ public async Task HandleAsync_GoalServiceThrowsException_LogsErrorAndCallsSafeAn From = user, InvoicePayload = "test_payload", TotalAmount = 10000, - Currency = "RUB" + Currency = "RUB", }; var update = new Update { PreCheckoutQuery = preCheckoutQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) .ThrowsAsync(new Exception("Database connection failed")); // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error handling pre-checkout query")), + It.Is((v, t) => v.ToString() !.Contains("Error handling pre-checkout query")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that approval logs contain correct details. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_WithActiveGoal_LogsCorrectDetails() + public async Task HandleAsyncWithActiveGoalLogsCorrectDetails() { // Arrange var user = new User { Id = 123 }; @@ -204,37 +236,41 @@ public async Task HandleAsync_WithActiveGoal_LogsCorrectDetails() From = user, InvoicePayload = "donation_500", TotalAmount = 50000, // 500.00 RUB - Currency = "RUB" + Currency = "RUB", }; var update = new Update { PreCheckoutQuery = preCheckoutQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; var activeGoal = new DonationGoal { Id = 1, Title = "Test Goal", TargetAmount = 100000 }; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(activeGoal); // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); - // Assert - проверяем, что логи содержат правильные детали - _loggerMock.Verify( + // Assert - verify that logs contain correct details + this.loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), It.Is((v, t) => - v.ToString().Contains("Approved pre-checkout query") && - v.ToString().Contains("123") && // user ID - v.ToString().Contains("donation_500") && // payload - v.ToString().Contains("500") && // amount - v.ToString().Contains("RUB")), // currency + v.ToString() !.Contains("Approved pre-checkout query") && + v.ToString() !.Contains("123") && // user ID + v.ToString() !.Contains("donation_500") && // payload + v.ToString() !.Contains("500") && // amount + v.ToString() !.Contains("RUB")), // currency It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that rejection logs contain correct details. + /// + /// A representing the asynchronous unit test. [Test] - public async Task HandleAsync_WithoutActiveGoal_LogsRejectionDetails() + public async Task HandleAsyncWithoutActiveGoalLogsRejectionDetails() { // Arrange var user = new User { Id = 123 }; @@ -244,29 +280,29 @@ public async Task HandleAsync_WithoutActiveGoal_LogsRejectionDetails() From = user, InvoicePayload = "donation_500", TotalAmount = 50000, - Currency = "RUB" + Currency = "RUB", }; var update = new Update { PreCheckoutQuery = preCheckoutQuery }; - var cancellationToken = new CancellationToken(); + var cancellationToken = CancellationToken.None; - _goalServiceMock + this.goalServiceMock .Setup(x => x.GetActiveGoalAsync()) - .ReturnsAsync((DonationGoal)null); + .ReturnsAsync(null as DonationGoal); // Act - await _handler.HandleAsync(_botClientMock.Object, update, cancellationToken); + await this.handler.HandleAsync(this.botClientMock.Object, update, cancellationToken); - // Assert - проверяем, что логи содержат правильные детали отклонения - _loggerMock.Verify( + // Assert - verify that logs contain correct rejection details + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), It.Is((v, t) => - v.ToString().Contains("Rejected pre-checkout query") && - v.ToString().Contains("123") && // user ID - v.ToString().Contains("no active goal found")), + v.ToString() !.Contains("Rejected pre-checkout query") && + v.ToString() !.Contains("123") && // user ID + v.ToString() !.Contains("no active goal found")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } } \ No newline at end of file diff --git a/TestBot/Bot/Bot.Service/AdminStateServiceTests.cs b/TestBot/Bot/Bot.Service/AdminStateServiceTests.cs index cbbc54b..032b9f1 100755 --- a/TestBot/Bot/Bot.Service/AdminStateServiceTests.cs +++ b/TestBot/Bot/Bot.Service/AdminStateServiceTests.cs @@ -1,4 +1,8 @@ -using Bot.Services; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot.Services; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -6,31 +10,40 @@ namespace Bot.Tests.Services; +/// +/// Unit tests for the class. +/// [TestFixture] public class AdminStateServiceTests { - private Mock> _loggerMock; - private AdminStateService _adminStateService; + private Mock> loggerMock; + private AdminStateService adminStateService; + /// + /// Sets up the test environment before each test execution. + /// [SetUp] public void Setup() { - _loggerMock = new Mock>(); - _adminStateService = new AdminStateService(_loggerMock.Object); + this.loggerMock = new Mock>(); + this.adminStateService = new AdminStateService(this.loggerMock.Object); } + /// + /// Tests that starting goal creation for a new user sets the initial state correctly. + /// [Test] - public void StartGoalCreation_NewUser_SetsInitialState() + public void StartGoalCreationNewUserSetsInitialState() { // Arrange var userId = 123L; var chatId = 456L; // Act - _adminStateService.StartGoalCreation(userId, chatId); + this.adminStateService.StartGoalCreation(userId, chatId); // Assert - var state = _adminStateService.GetState(userId); + var state = this.adminStateService.GetState(userId); Assert.That(state, Is.Not.Null); Assert.That(state.ChatId, Is.EqualTo(chatId)); Assert.That(state.CurrentStep, Is.EqualTo(AdminGoalStep.WaitingForTitle)); @@ -38,397 +51,454 @@ public void StartGoalCreation_NewUser_SetsInitialState() Assert.That(state.Description, Is.Null); Assert.That(state.TargetAmount, Is.Null); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Started goal creation for admin user")), + It.Is((v, t) => v.ToString() !.Contains("Started goal creation for admin user")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that starting goal creation for an existing user overwrites the previous state. + /// [Test] - public void StartGoalCreation_ExistingUser_OverwritesState() + public void StartGoalCreationExistingUserOverwritesState() { // Arrange var userId = 123L; var chatId = 456L; - _adminStateService.StartGoalCreation(userId, chatId); + this.adminStateService.StartGoalCreation(userId, chatId); - // Act - запускаем создание цели заново - _adminStateService.StartGoalCreation(userId, 789L); // Новый chatId + // Act - start goal creation again + this.adminStateService.StartGoalCreation(userId, 789L); // New chatId // Assert - var state = _adminStateService.GetState(userId); + var state = this.adminStateService.GetState(userId); Assert.That(state.ChatId, Is.EqualTo(789L)); Assert.That(state.CurrentStep, Is.EqualTo(AdminGoalStep.WaitingForTitle)); } + /// + /// Tests that getting state for a non-existent user returns null. + /// [Test] - public void GetState_NonExistentUser_ReturnsNull() + public void GetStateNonExistentUserReturnsNull() { // Arrange - var userId = 999L; // Несуществующий пользователь + var userId = 999L; // Non-existent user // Act - var state = _adminStateService.GetState(userId); + var state = this.adminStateService.GetState(userId); // Assert Assert.That(state, Is.Null); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("No state found for user")), + It.Is((v, t) => v.ToString() !.Contains("No state found for user")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that getting state for an existing user returns the correct state. + /// [Test] - public void GetState_ExistingUser_ReturnsState() + public void GetStateExistingUserReturnsState() { // Arrange var userId = 123L; var chatId = 456L; - _adminStateService.StartGoalCreation(userId, chatId); + this.adminStateService.StartGoalCreation(userId, chatId); // Act - var state = _adminStateService.GetState(userId); + var state = this.adminStateService.GetState(userId); // Assert Assert.That(state, Is.Not.Null); Assert.That(state.ChatId, Is.EqualTo(chatId)); } + /// + /// Tests that setting title for an existing user updates the title and advances the step. + /// [Test] - public void SetTitle_ExistingUser_SetsTitleAndAdvancesStep() + public void SetTitleExistingUserSetsTitleAndAdvancesStep() { // Arrange var userId = 123L; var chatId = 456L; var title = "Новая цель"; - _adminStateService.StartGoalCreation(userId, chatId); + this.adminStateService.StartGoalCreation(userId, chatId); // Act - _adminStateService.SetTitle(userId, title); + this.adminStateService.SetTitle(userId, title); // Assert - var state = _adminStateService.GetState(userId); + var state = this.adminStateService.GetState(userId); Assert.That(state.Title, Is.EqualTo(title)); Assert.That(state.CurrentStep, Is.EqualTo(AdminGoalStep.WaitingForDescription)); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Set title for user") && v.ToString().Contains(title)), + It.Is((v, t) => v.ToString() !.Contains("Set title for user") && v.ToString() !.Contains(title)), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that setting title for a non-existent user logs a warning. + /// [Test] - public void SetTitle_NonExistentUser_LogsWarning() + public void SetTitleNonExistentUserLogsWarning() { // Arrange - var userId = 999L; // Несуществующий пользователь + var userId = 999L; // Non-existent user var title = "Новая цель"; // Act - _adminStateService.SetTitle(userId, title); + this.adminStateService.SetTitle(userId, title); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Attempted to set title for non-existent user state")), + It.Is((v, t) => v.ToString() !.Contains("Attempted to set title for non-existent user state")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that setting description for an existing user updates the description and advances the step. + /// [Test] - public void SetDescription_ExistingUser_SetsDescriptionAndAdvancesStep() + public void SetDescriptionExistingUserSetsDescriptionAndAdvancesStep() { // Arrange var userId = 123L; var chatId = 456L; var description = "Описание цели"; - _adminStateService.StartGoalCreation(userId, chatId); - _adminStateService.SetTitle(userId, "Название"); + this.adminStateService.StartGoalCreation(userId, chatId); + this.adminStateService.SetTitle(userId, "Название"); // Act - _adminStateService.SetDescription(userId, description); + this.adminStateService.SetDescription(userId, description); // Assert - var state = _adminStateService.GetState(userId); + var state = this.adminStateService.GetState(userId); Assert.That(state.Description, Is.EqualTo(description)); Assert.That(state.CurrentStep, Is.EqualTo(AdminGoalStep.WaitingForAmount)); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Set description for user")), + It.Is((v, t) => v.ToString() !.Contains("Set description for user")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that setting description for a non-existent user logs a warning. + /// [Test] - public void SetDescription_NonExistentUser_LogsWarning() + public void SetDescriptionNonExistentUserLogsWarning() { // Arrange - var userId = 999L; // Несуществующий пользователь + var userId = 999L; // Non-existent user var description = "Описание цели"; // Act - _adminStateService.SetDescription(userId, description); + this.adminStateService.SetDescription(userId, description); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Attempted to set description for non-existent user state")), + It.Is((v, t) => v.ToString() !.Contains("Attempted to set description for non-existent user state")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that setting amount for an existing user updates the amount and completes the process. + /// [Test] - public void SetAmount_ExistingUser_SetsAmountAndCompletesProcess() + public void SetAmountExistingUserSetsAmountAndCompletesProcess() { // Arrange var userId = 123L; var chatId = 456L; var amount = 5000m; - _adminStateService.StartGoalCreation(userId, chatId); - _adminStateService.SetTitle(userId, "Название"); - _adminStateService.SetDescription(userId, "Описание"); + this.adminStateService.StartGoalCreation(userId, chatId); + this.adminStateService.SetTitle(userId, "Название"); + this.adminStateService.SetDescription(userId, "Описание"); // Act - _adminStateService.SetAmount(userId, amount); + this.adminStateService.SetAmount(userId, amount); // Assert - var state = _adminStateService.GetState(userId); + var state = this.adminStateService.GetState(userId); Assert.That(state.TargetAmount, Is.EqualTo(amount)); Assert.That(state.CurrentStep, Is.EqualTo(AdminGoalStep.None)); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Set amount for user") && v.ToString().Contains(amount.ToString())), + It.Is((v, t) => v.ToString() !.Contains("Set amount for user") && v.ToString() !.Contains(amount.ToString())), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that setting amount for a non-existent user logs a warning. + /// [Test] - public void SetAmount_NonExistentUser_LogsWarning() + public void SetAmountNonExistentUserLogsWarning() { // Arrange - var userId = 999L; // Несуществующий пользователь + var userId = 999L; // Non-existent user var amount = 5000m; // Act - _adminStateService.SetAmount(userId, amount); + this.adminStateService.SetAmount(userId, amount); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Attempted to set amount for non-existent user state")), + It.Is((v, t) => v.ToString() !.Contains("Attempted to set amount for non-existent user state")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that canceling goal creation for an existing user removes the state. + /// [Test] - public void CancelGoalCreation_ExistingUser_RemovesState() + public void CancelGoalCreationExistingUserRemovesState() { // Arrange var userId = 123L; var chatId = 456L; - _adminStateService.StartGoalCreation(userId, chatId); + this.adminStateService.StartGoalCreation(userId, chatId); // Act - _adminStateService.CancelGoalCreation(userId); + this.adminStateService.CancelGoalCreation(userId); // Assert - var state = _adminStateService.GetState(userId); + var state = this.adminStateService.GetState(userId); Assert.That(state, Is.Null); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Canceled goal creation for user")), + It.Is((v, t) => v.ToString() !.Contains("Canceled goal creation for user")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that canceling goal creation for a non-existent user logs a debug message. + /// [Test] - public void CancelGoalCreation_NonExistentUser_LogsDebug() + public void CancelGoalCreationNonExistentUserLogsDebug() { // Arrange - var userId = 999L; // Несуществующий пользователь + var userId = 999L; // Non-existent user // Act - _adminStateService.CancelGoalCreation(userId); + this.adminStateService.CancelGoalCreation(userId); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Attempted to cancel non-existent goal creation for user")), + It.Is((v, t) => v.ToString() !.Contains("Attempted to cancel non-existent goal creation for user")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that a user in the goal creation process returns true. + /// [Test] - public void IsUserCreatingGoal_UserInProcess_ReturnsTrue() + public void IsUserCreatingGoalUserInProcessReturnsTrue() { // Arrange var userId = 123L; var chatId = 456L; - _adminStateService.StartGoalCreation(userId, chatId); + this.adminStateService.StartGoalCreation(userId, chatId); // Act - var isCreating = _adminStateService.IsUserCreatingGoal(userId); + var isCreating = this.adminStateService.IsUserCreatingGoal(userId); // Assert Assert.That(isCreating, Is.True); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("User") && v.ToString().Contains("goal creation status: True")), + It.Is((v, t) => v.ToString() !.Contains("User") && v.ToString() !.Contains("goal creation status: True")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that a user not in the goal creation process returns false. + /// [Test] - public void IsUserCreatingGoal_UserNotInProcess_ReturnsFalse() + public void IsUserCreatingGoalUserNotInProcessReturnsFalse() { // Arrange - var userId = 123L; // Пользователь не начинал создание цели + var userId = 123L; // User has not started goal creation // Act - var isCreating = _adminStateService.IsUserCreatingGoal(userId); + var isCreating = this.adminStateService.IsUserCreatingGoal(userId); // Assert Assert.That(isCreating, Is.False); } + /// + /// Tests that a user who completed the goal creation process returns false. + /// [Test] - public void IsUserCreatingGoal_UserCompletedProcess_ReturnsFalse() + public void IsUserCreatingGoalUserCompletedProcessReturnsFalse() { // Arrange var userId = 123L; var chatId = 456L; - _adminStateService.StartGoalCreation(userId, chatId); - _adminStateService.SetTitle(userId, "Название"); - _adminStateService.SetDescription(userId, "Описание"); - _adminStateService.SetAmount(userId, 5000m); // Завершаем процесс + this.adminStateService.StartGoalCreation(userId, chatId); + this.adminStateService.SetTitle(userId, "Название"); + this.adminStateService.SetDescription(userId, "Описание"); + this.adminStateService.SetAmount(userId, 5000m); // Complete the process // Act - var isCreating = _adminStateService.IsUserCreatingGoal(userId); + var isCreating = this.adminStateService.IsUserCreatingGoal(userId); // Assert Assert.That(isCreating, Is.False); } + /// + /// Tests that a user who cancelled the goal creation process returns false. + /// [Test] - public void IsUserCreatingGoal_UserCancelledProcess_ReturnsFalse() + public void IsUserCreatingGoalUserCancelledProcessReturnsFalse() { // Arrange var userId = 123L; var chatId = 456L; - _adminStateService.StartGoalCreation(userId, chatId); - _adminStateService.CancelGoalCreation(userId); // Отменяем процесс + this.adminStateService.StartGoalCreation(userId, chatId); + this.adminStateService.CancelGoalCreation(userId); // Cancel the process // Act - var isCreating = _adminStateService.IsUserCreatingGoal(userId); + var isCreating = this.adminStateService.IsUserCreatingGoal(userId); // Assert Assert.That(isCreating, Is.False); } + /// + /// Tests that the active state count returns zero when there are no users. + /// [Test] - public void GetActiveStateCount_NoUsers_ReturnsZero() + public void GetActiveStateCountNoUsersReturnsZero() { - // Arrange - нет активных пользователей + // Arrange - no active users // Act - var count = _adminStateService.GetActiveStateCount(); + var count = this.adminStateService.GetActiveStateCount(); // Assert Assert.That(count, Is.EqualTo(0)); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Trace, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Current active admin states: 0")), + It.Is((v, t) => v.ToString() !.Contains("Current active admin states: 0")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that the active state count returns the correct count for multiple users. + /// [Test] - public void GetActiveStateCount_MultipleUsers_ReturnsCorrectCount() + public void GetActiveStateCountMultipleUsersReturnsCorrectCount() { // Arrange - _adminStateService.StartGoalCreation(123L, 456L); - _adminStateService.StartGoalCreation(124L, 457L); - _adminStateService.StartGoalCreation(125L, 458L); + this.adminStateService.StartGoalCreation(123L, 456L); + this.adminStateService.StartGoalCreation(124L, 457L); + this.adminStateService.StartGoalCreation(125L, 458L); // Act - var count = _adminStateService.GetActiveStateCount(); + var count = this.adminStateService.GetActiveStateCount(); // Assert Assert.That(count, Is.EqualTo(3)); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Trace, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Current active admin states: 3")), + It.Is((v, t) => v.ToString() !.Contains("Current active admin states: 3")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that the active state count is updated after cancellation. + /// [Test] - public void GetActiveStateCount_AfterCancellation_ReturnsUpdatedCount() + public void GetActiveStateCountAfterCancellationReturnsUpdatedCount() { // Arrange - _adminStateService.StartGoalCreation(123L, 456L); - _adminStateService.StartGoalCreation(124L, 457L); - _adminStateService.StartGoalCreation(125L, 458L); + this.adminStateService.StartGoalCreation(123L, 456L); + this.adminStateService.StartGoalCreation(124L, 457L); + this.adminStateService.StartGoalCreation(125L, 458L); - // Act - отменяем одного пользователя - _adminStateService.CancelGoalCreation(124L); - var count = _adminStateService.GetActiveStateCount(); + // Act - cancel one user + this.adminStateService.CancelGoalCreation(124L); + var count = this.adminStateService.GetActiveStateCount(); // Assert Assert.That(count, Is.EqualTo(2)); } + /// + /// Tests that multiple users have independent states. + /// [Test] - public void MultipleUsers_IndependentStates() + public void MultipleUsersIndependentStates() { // Arrange var user1 = 123L; @@ -437,15 +507,15 @@ public void MultipleUsers_IndependentStates() var chat2 = 457L; // Act - _adminStateService.StartGoalCreation(user1, chat1); - _adminStateService.StartGoalCreation(user2, chat2); + this.adminStateService.StartGoalCreation(user1, chat1); + this.adminStateService.StartGoalCreation(user2, chat2); - _adminStateService.SetTitle(user1, "Цель пользователя 1"); - _adminStateService.SetTitle(user2, "Цель пользователя 2"); + this.adminStateService.SetTitle(user1, "Цель пользователя 1"); + this.adminStateService.SetTitle(user2, "Цель пользователя 2"); // Assert - var state1 = _adminStateService.GetState(user1); - var state2 = _adminStateService.GetState(user2); + var state1 = this.adminStateService.GetState(user1); + var state2 = this.adminStateService.GetState(user2); Assert.That(state1.Title, Is.EqualTo("Цель пользователя 1")); Assert.That(state2.Title, Is.EqualTo("Цель пользователя 2")); diff --git a/TestBot/Bot/Bot.Service/UserStateServiceTests.cs b/TestBot/Bot/Bot.Service/UserStateServiceTests.cs index ffefdae..bbefb18 100755 --- a/TestBot/Bot/Bot.Service/UserStateServiceTests.cs +++ b/TestBot/Bot/Bot.Service/UserStateServiceTests.cs @@ -1,417 +1,480 @@ -using Bot.Services; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using Bot.Services; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; namespace Bot.Tests.Services; +/// +/// Unit tests for the class. +/// [TestFixture] public class UserStateServiceTests { - private Mock> _loggerMock; - private UserStateService _userStateService; + private Mock> loggerMock; + private UserStateService userStateService; + /// + /// Sets up the test environment before each test execution. + /// [SetUp] public void Setup() { - _loggerMock = new Mock>(); - _userStateService = new UserStateService(_loggerMock.Object); + this.loggerMock = new Mock>(); + this.userStateService = new UserStateService(this.loggerMock.Object); } + /// + /// Tests that setting waiting for amount for a new user sets the state correctly. + /// [Test] - public void SetWaitingForAmount_NewUser_SetsState() + public void SetWaitingForAmountNewUserSetsState() { // Arrange var userId = 123L; var chatId = 456L; // Act - _userStateService.SetWaitingForAmount(userId, chatId); + this.userStateService.SetWaitingForAmount(userId, chatId); // Assert - var isWaiting = _userStateService.IsWaitingForAmount(userId, chatId); + var isWaiting = this.userStateService.IsWaitingForAmount(userId, chatId); Assert.That(isWaiting, Is.True); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("User") && v.ToString().Contains("set to waiting for amount input")), + It.Is((v, t) => v.ToString() !.Contains("User") && v.ToString() !.Contains("set to waiting for amount input")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that setting waiting for amount for an existing user updates the state. + /// [Test] - public void SetWaitingForAmount_ExistingUser_UpdatesState() + public void SetWaitingForAmountExistingUserUpdatesState() { // Arrange var userId = 123L; - _userStateService.SetWaitingForAmount(userId, 456L); + this.userStateService.SetWaitingForAmount(userId, 456L); - // Act - обновляем chatId для того же пользователя - _userStateService.SetWaitingForAmount(userId, 789L); + // Act - update chatId for the same user + this.userStateService.SetWaitingForAmount(userId, 789L); // Assert - var isWaitingOldChat = _userStateService.IsWaitingForAmount(userId, 456L); - var isWaitingNewChat = _userStateService.IsWaitingForAmount(userId, 789L); + var isWaitingOldChat = this.userStateService.IsWaitingForAmount(userId, 456L); + var isWaitingNewChat = this.userStateService.IsWaitingForAmount(userId, 789L); Assert.That(isWaitingOldChat, Is.False); Assert.That(isWaitingNewChat, Is.True); } + /// + /// Tests that checking waiting for amount for a user not waiting returns false. + /// [Test] - public void IsWaitingForAmount_UserNotWaiting_ReturnsFalse() + public void IsWaitingForAmountUserNotWaitingReturnsFalse() { // Arrange var userId = 123L; var chatId = 456L; // Act - var isWaiting = _userStateService.IsWaitingForAmount(userId, chatId); + var isWaiting = this.userStateService.IsWaitingForAmount(userId, chatId); // Assert Assert.That(isWaiting, Is.False); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Checked waiting for amount status") && v.ToString().Contains("False")), + It.Is((v, t) => v.ToString() !.Contains("Checked waiting for amount status") && v.ToString() !.Contains("False")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that checking waiting for amount for a user waiting in a different chat returns false. + /// [Test] - public void IsWaitingForAmount_UserWaitingInDifferentChat_ReturnsFalse() + public void IsWaitingForAmountUserWaitingInDifferentChatReturnsFalse() { // Arrange var userId = 123L; - _userStateService.SetWaitingForAmount(userId, 456L); + this.userStateService.SetWaitingForAmount(userId, 456L); // Act - var isWaiting = _userStateService.IsWaitingForAmount(userId, 789L); // Другой chatId + var isWaiting = this.userStateService.IsWaitingForAmount(userId, 789L); // Different chatId // Assert Assert.That(isWaiting, Is.False); } + /// + /// Tests that checking waiting for amount for a user waiting in the same chat returns true. + /// [Test] - public void IsWaitingForAmount_UserWaitingInSameChat_ReturnsTrue() + public void IsWaitingForAmountUserWaitingInSameChatReturnsTrue() { // Arrange var userId = 123L; var chatId = 456L; - _userStateService.SetWaitingForAmount(userId, chatId); + this.userStateService.SetWaitingForAmount(userId, chatId); // Act - var isWaiting = _userStateService.IsWaitingForAmount(userId, chatId); + var isWaiting = this.userStateService.IsWaitingForAmount(userId, chatId); // Assert Assert.That(isWaiting, Is.True); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Checked waiting for amount status") && v.ToString().Contains("True")), + It.Is((v, t) => v.ToString() !.Contains("Checked waiting for amount status") && v.ToString() !.Contains("True")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that removing waiting for amount for an existing user removes the state. + /// [Test] - public void RemoveWaitingForAmount_ExistingUser_RemovesState() + public void RemoveWaitingForAmountExistingUserRemovesState() { // Arrange var userId = 123L; var chatId = 456L; - _userStateService.SetWaitingForAmount(userId, chatId); + this.userStateService.SetWaitingForAmount(userId, chatId); // Act - _userStateService.RemoveWaitingForAmount(userId); + this.userStateService.RemoveWaitingForAmount(userId); // Assert - var isWaiting = _userStateService.IsWaitingForAmount(userId, chatId); + var isWaiting = this.userStateService.IsWaitingForAmount(userId, chatId); Assert.That(isWaiting, Is.False); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Removed user") && v.ToString().Contains("from waiting for amount state")), + It.Is((v, t) => v.ToString() !.Contains("Removed user") && v.ToString() !.Contains("from waiting for amount state")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that removing waiting for amount for a non-existent user logs a debug message. + /// [Test] - public void RemoveWaitingForAmount_NonExistentUser_LogsDebug() + public void RemoveWaitingForAmountNonExistentUserLogsDebug() { // Arrange - var userId = 999L; // Несуществующий пользователь + var userId = 999L; // Non-existent user // Act - _userStateService.RemoveWaitingForAmount(userId); + this.userStateService.RemoveWaitingForAmount(userId); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Attempted to remove non-existent waiting state for user")), + It.Is((v, t) => v.ToString() !.Contains("Attempted to remove non-existent waiting state for user")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that getting waiting users count returns zero when there are no users. + /// [Test] - public void GetWaitingUsersCount_NoUsers_ReturnsZero() + public void GetWaitingUsersCountNoUsersReturnsZero() { - // Arrange - нет пользователей в состоянии ожидания + // Arrange - no users in waiting state // Act - var count = _userStateService.GetWaitingUsersCount(); + var count = this.userStateService.GetWaitingUsersCount(); // Assert Assert.That(count, Is.EqualTo(0)); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Trace, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Current users waiting for amount input: 0")), + It.Is((v, t) => v.ToString() !.Contains("Current users waiting for amount input: 0")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that getting waiting users count returns the correct count for multiple users. + /// [Test] - public void GetWaitingUsersCount_MultipleUsers_ReturnsCorrectCount() + public void GetWaitingUsersCountMultipleUsersReturnsCorrectCount() { // Arrange - _userStateService.SetWaitingForAmount(123L, 456L); - _userStateService.SetWaitingForAmount(124L, 457L); - _userStateService.SetWaitingForAmount(125L, 458L); + this.userStateService.SetWaitingForAmount(123L, 456L); + this.userStateService.SetWaitingForAmount(124L, 457L); + this.userStateService.SetWaitingForAmount(125L, 458L); // Act - var count = _userStateService.GetWaitingUsersCount(); + var count = this.userStateService.GetWaitingUsersCount(); // Assert Assert.That(count, Is.EqualTo(3)); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Trace, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Current users waiting for amount input: 3")), + It.Is((v, t) => v.ToString() !.Contains("Current users waiting for amount input: 3")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that getting waiting users count is updated after removal. + /// [Test] - public void GetWaitingUsersCount_AfterRemoval_ReturnsUpdatedCount() + public void GetWaitingUsersCountAfterRemovalReturnsUpdatedCount() { // Arrange - _userStateService.SetWaitingForAmount(123L, 456L); - _userStateService.SetWaitingForAmount(124L, 457L); - _userStateService.SetWaitingForAmount(125L, 458L); + this.userStateService.SetWaitingForAmount(123L, 456L); + this.userStateService.SetWaitingForAmount(124L, 457L); + this.userStateService.SetWaitingForAmount(125L, 458L); - // Act - удаляем одного пользователя - _userStateService.RemoveWaitingForAmount(124L); - var count = _userStateService.GetWaitingUsersCount(); + // Act - remove one user + this.userStateService.RemoveWaitingForAmount(124L); + var count = this.userStateService.GetWaitingUsersCount(); // Assert Assert.That(count, Is.EqualTo(2)); } + /// + /// Tests that clearing all waiting states with users clears all states. + /// [Test] - public void ClearAllWaitingStates_WithUsers_ClearsAllStates() + public void ClearAllWaitingStatesWithUsersClearsAllStates() { // Arrange - _userStateService.SetWaitingForAmount(123L, 456L); - _userStateService.SetWaitingForAmount(124L, 457L); - _userStateService.SetWaitingForAmount(125L, 458L); + this.userStateService.SetWaitingForAmount(123L, 456L); + this.userStateService.SetWaitingForAmount(124L, 457L); + this.userStateService.SetWaitingForAmount(125L, 458L); // Act - _userStateService.ClearAllWaitingStates(); + this.userStateService.ClearAllWaitingStates(); // Assert - var count = _userStateService.GetWaitingUsersCount(); + var count = this.userStateService.GetWaitingUsersCount(); Assert.That(count, Is.EqualTo(0)); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Cleared all waiting states, affected 3 users")), + It.Is((v, t) => v.ToString() !.Contains("Cleared all waiting states, affected 3 users")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that clearing all waiting states with no users logs zero. + /// [Test] - public void ClearAllWaitingStates_NoUsers_LogsZero() + public void ClearAllWaitingStatesNoUsersLogsZero() { - // Arrange - нет пользователей + // Arrange - no users // Act - _userStateService.ClearAllWaitingStates(); + this.userStateService.ClearAllWaitingStates(); // Assert - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Cleared all waiting states, affected 0 users")), + It.Is((v, t) => v.ToString() !.Contains("Cleared all waiting states, affected 0 users")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that removing multiple waiting states for all existing users removes all. + /// [Test] - public void RemoveMultipleWaitingStates_AllUsersExist_RemovesAll() + public void RemoveMultipleWaitingStatesAllUsersExistRemovesAll() { // Arrange - _userStateService.SetWaitingForAmount(123L, 456L); - _userStateService.SetWaitingForAmount(124L, 457L); - _userStateService.SetWaitingForAmount(125L, 458L); + this.userStateService.SetWaitingForAmount(123L, 456L); + this.userStateService.SetWaitingForAmount(124L, 457L); + this.userStateService.SetWaitingForAmount(125L, 458L); var userIdsToRemove = new long[] { 123L, 124L, 125L }; // Act - var removedCount = _userStateService.RemoveMultipleWaitingStates(userIdsToRemove); + var removedCount = this.userStateService.RemoveMultipleWaitingStates(userIdsToRemove); // Assert Assert.That(removedCount, Is.EqualTo(3)); - var remainingCount = _userStateService.GetWaitingUsersCount(); + var remainingCount = this.userStateService.GetWaitingUsersCount(); Assert.That(remainingCount, Is.EqualTo(0)); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Removed waiting states for 3 out of 3 users")), + It.Is((v, t) => v.ToString() !.Contains("Removed waiting states for 3 out of 3 users")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that removing multiple waiting states for some existing users removes only existing ones. + /// [Test] - public void RemoveMultipleWaitingStates_SomeUsersExist_RemovesOnlyExisting() + public void RemoveMultipleWaitingStatesSomeUsersExistRemovesOnlyExisting() { // Arrange - _userStateService.SetWaitingForAmount(123L, 456L); - _userStateService.SetWaitingForAmount(124L, 457L); - // 125L не добавляли + this.userStateService.SetWaitingForAmount(123L, 456L); + this.userStateService.SetWaitingForAmount(124L, 457L); var userIdsToRemove = new long[] { 123L, 124L, 125L }; // Act - var removedCount = _userStateService.RemoveMultipleWaitingStates(userIdsToRemove); + var removedCount = this.userStateService.RemoveMultipleWaitingStates(userIdsToRemove); // Assert Assert.That(removedCount, Is.EqualTo(2)); - var remainingCount = _userStateService.GetWaitingUsersCount(); + var remainingCount = this.userStateService.GetWaitingUsersCount(); Assert.That(remainingCount, Is.EqualTo(0)); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Removed waiting states for 2 out of 3 users")), + It.Is((v, t) => v.ToString() !.Contains("Removed waiting states for 2 out of 3 users")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that removing multiple waiting states for non-existent users returns zero. + /// [Test] - public void RemoveMultipleWaitingStates_NoUsersExist_ReturnsZero() + public void RemoveMultipleWaitingStatesNoUsersExistReturnsZero() { // Arrange var userIdsToRemove = new long[] { 123L, 124L, 125L }; // Act - var removedCount = _userStateService.RemoveMultipleWaitingStates(userIdsToRemove); + var removedCount = this.userStateService.RemoveMultipleWaitingStates(userIdsToRemove); // Assert Assert.That(removedCount, Is.EqualTo(0)); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Removed waiting states for 0 out of 3 users")), + It.Is((v, t) => v.ToString() !.Contains("Removed waiting states for 0 out of 3 users")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that removing multiple waiting states with an empty list returns zero. + /// [Test] - public void RemoveMultipleWaitingStates_EmptyList_ReturnsZero() + public void RemoveMultipleWaitingStatesEmptyListReturnsZero() { // Arrange var userIdsToRemove = Array.Empty(); // Act - var removedCount = _userStateService.RemoveMultipleWaitingStates(userIdsToRemove); + var removedCount = this.userStateService.RemoveMultipleWaitingStates(userIdsToRemove); // Assert Assert.That(removedCount, Is.EqualTo(0)); - _loggerMock.Verify( + this.loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Removed waiting states for 0 out of 0 users")), + It.Is((v, t) => v.ToString() !.Contains("Removed waiting states for 0 out of 0 users")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests a complex scenario with multiple operations works correctly. + /// [Test] - public void MultipleOperations_ComplexScenario_WorksCorrectly() + public void MultipleOperationsComplexScenarioWorksCorrectly() { - // Arrange & Act - комплексный сценарий - _userStateService.SetWaitingForAmount(123L, 456L); - _userStateService.SetWaitingForAmount(124L, 457L); + // Arrange & Act - complex scenario + this.userStateService.SetWaitingForAmount(123L, 456L); + this.userStateService.SetWaitingForAmount(124L, 457L); - var countAfterAdd = _userStateService.GetWaitingUsersCount(); + var countAfterAdd = this.userStateService.GetWaitingUsersCount(); Assert.That(countAfterAdd, Is.EqualTo(2)); - // Проверяем состояния - var user123Waiting = _userStateService.IsWaitingForAmount(123L, 456L); - var user124Waiting = _userStateService.IsWaitingForAmount(124L, 457L); + // Check states + var user123Waiting = this.userStateService.IsWaitingForAmount(123L, 456L); + var user124Waiting = this.userStateService.IsWaitingForAmount(124L, 457L); Assert.That(user123Waiting, Is.True); Assert.That(user124Waiting, Is.True); - // Обновляем состояние одного пользователя - _userStateService.SetWaitingForAmount(123L, 789L); + // Update state for one user + this.userStateService.SetWaitingForAmount(123L, 789L); - var user123OldChat = _userStateService.IsWaitingForAmount(123L, 456L); - var user123NewChat = _userStateService.IsWaitingForAmount(123L, 789L); + var user123OldChat = this.userStateService.IsWaitingForAmount(123L, 456L); + var user123NewChat = this.userStateService.IsWaitingForAmount(123L, 789L); Assert.That(user123OldChat, Is.False); Assert.That(user123NewChat, Is.True); - // Удаляем одного пользователя - _userStateService.RemoveWaitingForAmount(124L); + // Remove one user + this.userStateService.RemoveWaitingForAmount(124L); - var countAfterRemove = _userStateService.GetWaitingUsersCount(); + var countAfterRemove = this.userStateService.GetWaitingUsersCount(); Assert.That(countAfterRemove, Is.EqualTo(1)); - // Очищаем все - _userStateService.ClearAllWaitingStates(); + // Clear all + this.userStateService.ClearAllWaitingStates(); - var finalCount = _userStateService.GetWaitingUsersCount(); + var finalCount = this.userStateService.GetWaitingUsersCount(); Assert.That(finalCount, Is.EqualTo(0)); } + /// + /// Tests that independent users do not interfere with each other's states. + /// [Test] - public void IndependentUsers_DoNotInterfere() + public void IndependentUsersDoNotInterfere() { // Arrange var user1 = 123L; @@ -420,25 +483,25 @@ public void IndependentUsers_DoNotInterfere() var chat2 = 457L; // Act - _userStateService.SetWaitingForAmount(user1, chat1); - _userStateService.SetWaitingForAmount(user2, chat2); + this.userStateService.SetWaitingForAmount(user1, chat1); + this.userStateService.SetWaitingForAmount(user2, chat2); - // Assert - проверяем, что состояния независимы - var user1InChat1 = _userStateService.IsWaitingForAmount(user1, chat1); - var user1InChat2 = _userStateService.IsWaitingForAmount(user1, chat2); - var user2InChat1 = _userStateService.IsWaitingForAmount(user2, chat1); - var user2InChat2 = _userStateService.IsWaitingForAmount(user2, chat2); + // Assert - check that states are independent + var user1InChat1 = this.userStateService.IsWaitingForAmount(user1, chat1); + var user1InChat2 = this.userStateService.IsWaitingForAmount(user1, chat2); + var user2InChat1 = this.userStateService.IsWaitingForAmount(user2, chat1); + var user2InChat2 = this.userStateService.IsWaitingForAmount(user2, chat2); Assert.That(user1InChat1, Is.True); Assert.That(user1InChat2, Is.False); Assert.That(user2InChat1, Is.False); Assert.That(user2InChat2, Is.True); - // Удаляем только user1 - _userStateService.RemoveWaitingForAmount(user1); + // Remove only user1 + this.userStateService.RemoveWaitingForAmount(user1); - var user1AfterRemove = _userStateService.IsWaitingForAmount(user1, chat1); - var user2AfterRemove = _userStateService.IsWaitingForAmount(user2, chat2); + var user1AfterRemove = this.userStateService.IsWaitingForAmount(user1, chat1); + var user2AfterRemove = this.userStateService.IsWaitingForAmount(user2, chat2); Assert.That(user1AfterRemove, Is.False); Assert.That(user2AfterRemove, Is.True); diff --git a/TestBot/Bot/UpdateHandlerTests.cs b/TestBot/Bot/UpdateHandlerTests.cs index afda669..32c4055 100755 --- a/TestBot/Bot/UpdateHandlerTests.cs +++ b/TestBot/Bot/UpdateHandlerTests.cs @@ -1,202 +1,255 @@ -using Bot; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + using Bot.Handlers; using Microsoft.Extensions.Logging; using Moq; -using NUnit.Framework; using Telegram.Bot; -using Telegram.Bot.Exceptions; -using Telegram.Bot.Polling; using Telegram.Bot.Types; -using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Payments; -namespace Bot.Tests; - -[TestFixture] -public class UpdateHandlerTests +namespace Bot.Tests { - private Mock> _loggerMock; - private Mock _botClientMock; - private List> _handlerMocks; - private UpdateHandler _updateHandler; - - [SetUp] - public void Setup() - { - _loggerMock = new Mock>(); - _botClientMock = new Mock(); - - // Создаем моки обработчиков - _handlerMocks = new List>(); - - var handlers = new List(); - _updateHandler = new UpdateHandler(_loggerMock.Object, handlers); - } - - private void AddHandler(Mock handlerMock) + /// + /// Contains unit tests for the class. + /// Tests various scenarios including update routing, exception handling, + /// logging behavior, and handler selection logic. + /// + [TestFixture] + public class UpdateHandlerTests { - _handlerMocks.Add(handlerMock); - var handlers = _handlerMocks.Select(m => m.Object).ToList(); - _updateHandler = new UpdateHandler(_loggerMock.Object, handlers); - } - - [Test] - public async Task HandleUpdateAsync_NoHandlers_LogsWarning() - { - // Arrange - var update = new Update { Id = 123 }; - var cancellationToken = new CancellationToken(); - - // Act - await _updateHandler.HandleUpdateAsync(_botClientMock.Object, update, cancellationToken); - - // Assert - _loggerMock.Verify( - x => x.Log( - LogLevel.Warning, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("No handler found for update")), - It.IsAny(), - It.Is>((v, t) => true)), - Times.Once); - } - - [Test] - public async Task HandleUpdateAsync_FirstMatchingHandlerUsed_DoesNotCheckOtherHandlers() - { - // Arrange - var update = new Update { Id = 123, CallbackQuery = new CallbackQuery() }; - var cancellationToken = new CancellationToken(); + private Mock> loggerMock; + private Mock botClientMock; + private List> handlerMocks; + private UpdateHandler updateHandler; + + /// + /// Initializes test setup before each test execution. + /// Creates mock instances for logger, bot client, and prepares empty handler list. + /// + [SetUp] + public void Setup() + { + loggerMock = new Mock>(); + botClientMock = new Mock(); + + // Create mock handlers + handlerMocks = new List>(); + + var handlers = new List(); + updateHandler = new UpdateHandler(loggerMock.Object, handlers); + } + + /// + /// Tests that a warning log is recorded when no handlers are available to process an update. + /// + /// A task representing the asynchronous operation. + [Test] + public async Task HandleUpdateAsyncNoHandlersLogsWarning() + { + // Arrange + var update = new Update { Id = 123 }; + var cancellationToken = CancellationToken.None; + + // Act + await updateHandler.HandleUpdateAsync(botClientMock.Object, update, cancellationToken); + + // Assert + loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString() !.Contains("No handler found for update")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + /// + /// Tests that only the first matching handler is executed when multiple handlers could process an update. + /// Verifies that subsequent matching handlers are not checked or executed. + /// + /// A task representing the asynchronous operation. + [Test] + public async Task HandleUpdateAsyncFirstMatchingHandlerUsedDoesNotCheckOtherHandlers() + { + // Arrange + var update = new Update { Id = 123, CallbackQuery = new CallbackQuery() }; + var cancellationToken = CancellationToken.None; - var firstHandlerMock = new Mock(); - firstHandlerMock.Setup(h => h.CanHandle(update)).Returns(true); - firstHandlerMock.Setup(h => h.HandleAsync(_botClientMock.Object, update, cancellationToken)) - .Returns(Task.CompletedTask); + var firstHandlerMock = new Mock(); + firstHandlerMock.Setup(h => h.CanHandle(update)).Returns(true); + firstHandlerMock.Setup(h => h.HandleAsync(botClientMock.Object, update, cancellationToken)) + .Returns(Task.CompletedTask); - var secondHandlerMock = new Mock(); - secondHandlerMock.Setup(h => h.CanHandle(update)).Returns(true); // Этот тоже подходит, но не должен быть вызван + var secondHandlerMock = new Mock(); - AddHandler(firstHandlerMock); - AddHandler(secondHandlerMock); + // This one also matches but should not be called + secondHandlerMock.Setup(h => h.CanHandle(update)).Returns(true); - // Act - await _updateHandler.HandleUpdateAsync(_botClientMock.Object, update, cancellationToken); + AddHandler(firstHandlerMock); + AddHandler(secondHandlerMock); - // Assert - firstHandlerMock.Verify(h => h.CanHandle(update), Times.Once); - firstHandlerMock.Verify(h => h.HandleAsync(_botClientMock.Object, update, cancellationToken), Times.Once); - secondHandlerMock.Verify(h => h.CanHandle(update), Times.Never); // Не должен проверяться - } + // Act + await updateHandler.HandleUpdateAsync(botClientMock.Object, update, cancellationToken); - [Test] - public async Task HandleUpdateAsync_HandlerThrowsException_LogsError() - { - // Arrange - var update = new Update { Id = 123, Message = new Message() }; - var cancellationToken = new CancellationToken(); - var exception = new Exception("Handler failed"); - - var handlerMock = new Mock(); - handlerMock.Setup(h => h.CanHandle(update)).Returns(true); - handlerMock.Setup(h => h.HandleAsync(_botClientMock.Object, update, cancellationToken)) - .ThrowsAsync(exception); - - AddHandler(handlerMock); - - // Act - await _updateHandler.HandleUpdateAsync(_botClientMock.Object, update, cancellationToken); - - // Assert - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error processing update")), - exception, - It.Is>((v, t) => true)), - Times.Once); - } + // Assert + firstHandlerMock.Verify(h => h.CanHandle(update), Times.Once); + firstHandlerMock.Verify(h => h.HandleAsync(botClientMock.Object, update, cancellationToken), Times.Once); - [Test] - public async Task HandleUpdateAsync_LogsDebugOnReceipt() - { - // Arrange - var update = new Update { Id = 123 }; - var cancellationToken = new CancellationToken(); - - // Act - await _updateHandler.HandleUpdateAsync(_botClientMock.Object, update, cancellationToken); - - // Assert - _loggerMock.Verify( - x => x.Log( - LogLevel.Debug, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Received update") && v.ToString().Contains("123")), - It.IsAny(), - It.Is>((v, t) => true)), - Times.Once); - } + // Should not be checked + secondHandlerMock.Verify(h => h.CanHandle(update), Times.Never); + } - [Test] - public void Constructor_WithHandlers_LogsHandlerCount() - { - // Arrange - var handlers = new List + /// + /// Tests that an error log is recorded when a handler throws an exception during update processing. + /// + /// A task representing the asynchronous operation. + [Test] + public async Task HandleUpdateAsyncHandlerThrowsExceptionLogsError() { - new Mock().Object, - new Mock().Object, - new Mock().Object - }; - - // Act - var handler = new UpdateHandler(_loggerMock.Object, handlers); - - // Assert - _loggerMock.Verify( - x => x.Log( - LogLevel.Debug, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("UpdateHandler initialized with 3 handlers")), - It.IsAny(), - It.Is>((v, t) => true)), - Times.Once); - } - - [Test] - public async Task HandleUpdateAsync_MultipleHandlerTypes_RoutesCorrectly() - { - // Arrange - var messageUpdate = new Update { Id = 1, Message = new Message() }; - var callbackUpdate = new Update { Id = 2, CallbackQuery = new CallbackQuery() }; - var preCheckoutUpdate = new Update { Id = 3, PreCheckoutQuery = new PreCheckoutQuery() }; - var cancellationToken = new CancellationToken(); - - var messageHandlerMock = new Mock(); - messageHandlerMock.Setup(h => h.CanHandle(It.Is(u => u.Message != null))).Returns(true); - - var callbackHandlerMock = new Mock(); - callbackHandlerMock.Setup(h => h.CanHandle(It.Is(u => u.CallbackQuery != null))).Returns(true); - - var preCheckoutHandlerMock = new Mock(); - preCheckoutHandlerMock.Setup(h => h.CanHandle(It.Is(u => u.PreCheckoutQuery != null))).Returns(true); - - AddHandler(messageHandlerMock); - AddHandler(callbackHandlerMock); - AddHandler(preCheckoutHandlerMock); - - // Act & Assert - Message update - await _updateHandler.HandleUpdateAsync(_botClientMock.Object, messageUpdate, cancellationToken); - messageHandlerMock.Verify(h => h.HandleAsync(_botClientMock.Object, messageUpdate, cancellationToken), Times.Once); - callbackHandlerMock.Verify(h => h.HandleAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - preCheckoutHandlerMock.Verify(h => h.HandleAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - - // Act & Assert - Callback update - await _updateHandler.HandleUpdateAsync(_botClientMock.Object, callbackUpdate, cancellationToken); - callbackHandlerMock.Verify(h => h.HandleAsync(_botClientMock.Object, callbackUpdate, cancellationToken), Times.Once); - - // Act & Assert - PreCheckout update - await _updateHandler.HandleUpdateAsync(_botClientMock.Object, preCheckoutUpdate, cancellationToken); - preCheckoutHandlerMock.Verify(h => h.HandleAsync(_botClientMock.Object, preCheckoutUpdate, cancellationToken), Times.Once); + // Arrange + var update = new Update { Id = 123, Message = new Message() }; + var cancellationToken = CancellationToken.None; + var exception = new Exception("Handler failed"); + + var handlerMock = new Mock(); + handlerMock.Setup(h => h.CanHandle(update)).Returns(true); + handlerMock.Setup(h => h.HandleAsync(botClientMock.Object, update, cancellationToken)) + .ThrowsAsync(exception); + + AddHandler(handlerMock); + + // Act + await updateHandler.HandleUpdateAsync(botClientMock.Object, update, cancellationToken); + + // Assert + loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString() !.Contains("Error processing update")), + exception, + It.IsAny>()), + Times.Once); + } + + /// + /// Tests that a debug log is recorded when an update is received. + /// + /// A task representing the asynchronous operation. + [Test] + public async Task HandleUpdateAsyncLogsDebugOnReceipt() + { + // Arrange + var update = new Update { Id = 123 }; + var cancellationToken = CancellationToken.None; + + // Act + await updateHandler.HandleUpdateAsync(botClientMock.Object, update, cancellationToken); + + // Assert + loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString() !.Contains("Received update") && v.ToString() !.Contains("123")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + /// + /// Tests that the constructor logs the count of registered handlers during initialization. + /// + [Test] + public void ConstructorWithHandlersLogsHandlerCount() + { + // Arrange + var handlers = new List + { + new Mock().Object, + new Mock().Object, + new Mock().Object, + }; + + // Act + var handler = new UpdateHandler(loggerMock.Object, handlers); + + // Assert + loggerMock.Verify( + x => x.Log( + LogLevel.Debug, + It.IsAny(), + It.Is((v, t) => v.ToString() !.Contains("UpdateHandler initialized with 3 handlers")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + /// + /// Tests that different types of updates are correctly routed to their corresponding handlers. + /// Verifies that message updates go to message handlers, callback updates to callback handlers, etc. + /// + /// A task representing the asynchronous operation. + [Test] + public async Task HandleUpdateAsyncMultipleHandlerTypesRoutesCorrectly() + { + // Arrange + var messageUpdate = new Update { Id = 1, Message = new Message() }; + var callbackUpdate = new Update { Id = 2, CallbackQuery = new CallbackQuery() }; + var preCheckoutUpdate = new Update { Id = 3, PreCheckoutQuery = new PreCheckoutQuery() }; + var cancellationToken = CancellationToken.None; + + var messageHandlerMock = new Mock(); + messageHandlerMock.Setup(h => h.CanHandle(It.Is(u => u.Message != null))).Returns(true); + + var callbackHandlerMock = new Mock(); + callbackHandlerMock.Setup(h => h.CanHandle(It.Is(u => u.CallbackQuery != null))).Returns(true); + + var preCheckoutHandlerMock = new Mock(); + preCheckoutHandlerMock.Setup(h => h.CanHandle(It.Is(u => u.PreCheckoutQuery != null))).Returns(true); + + AddHandler(messageHandlerMock); + AddHandler(callbackHandlerMock); + AddHandler(preCheckoutHandlerMock); + + // Act & Assert - Message update + await updateHandler.HandleUpdateAsync(botClientMock.Object, messageUpdate, cancellationToken); + messageHandlerMock.Verify( + h => h.HandleAsync(botClientMock.Object, messageUpdate, cancellationToken), + Times.Once); + callbackHandlerMock.Verify( + h => h.HandleAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + preCheckoutHandlerMock.Verify( + h => h.HandleAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + + // Act & Assert - Callback update + await updateHandler.HandleUpdateAsync(botClientMock.Object, callbackUpdate, cancellationToken); + callbackHandlerMock.Verify( + h => h.HandleAsync(botClientMock.Object, callbackUpdate, cancellationToken), + Times.Once); + + // Act & Assert - PreCheckout update + await updateHandler.HandleUpdateAsync(botClientMock.Object, preCheckoutUpdate, cancellationToken); + preCheckoutHandlerMock.Verify( + h => h.HandleAsync(botClientMock.Object, preCheckoutUpdate, cancellationToken), + Times.Once); + } + + /// + /// Helper method to add a mock handler to the update handler instance. + /// Recreates the update handler with the updated handler collection. + /// + /// The mock handler to add. + private void AddHandler(Mock handlerMock) + { + handlerMocks.Add(handlerMock); + var handlers = handlerMocks.Select(m => m.Object).ToList(); + updateHandler = new UpdateHandler(loggerMock.Object, handlers); + } } } \ No newline at end of file diff --git a/TestBot/Data/DonationServiceTests.cs b/TestBot/Data/DonationServiceTests.cs index 1c69294..57102c2 100755 --- a/TestBot/Data/DonationServiceTests.cs +++ b/TestBot/Data/DonationServiceTests.cs @@ -1,4 +1,10 @@ -using Data; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using System; +using System.Threading.Tasks; +using Data; using Data.Models; using Microsoft.Extensions.Logging; using Moq; @@ -7,52 +13,71 @@ namespace Services.Tests; +/// +/// Contains unit tests for the class. +/// Tests various scenarios including user management, donation processing, +/// exception handling, and logging behavior. +/// [TestFixture] public class DonationServiceTests { - private Mock _repositoryMock; - private Mock> _loggerMock; - private DonationService _donationService; - + private Mock repositoryMock; + private Mock> loggerMock; + private DonationService donationService; + + /// + /// Initializes test environment before each test execution. + /// Creates mock instances for repository and logger, and instantiates the donation service. + /// [SetUp] public void Setup() { - _repositoryMock = new Mock(); - _loggerMock = new Mock>(); - _donationService = new DonationService(_repositoryMock.Object, _loggerMock.Object); + repositoryMock = new Mock(); + loggerMock = new Mock>(); + donationService = new DonationService(repositoryMock.Object, loggerMock.Object); } + /// + /// Tests that when a user already exists in the database, + /// the method returns the existing user without creating a new one. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetOrCreateUserAsync_ExistingUser_ReturnsUser() + public async Task GetOrCreateUserAsyncExistingUserReturnsUser() { // Arrange var telegramId = 123L; var existingUser = new Users { Id = 1, TelegramId = telegramId, Username = "testuser", FirstName = "Test", LastName = "User" }; - _repositoryMock + repositoryMock .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) .ReturnsAsync(existingUser); // Act - var result = await _donationService.GetOrCreateUserAsync(telegramId, "testuser", "Test", "User"); + var result = await donationService.GetOrCreateUserAsync(telegramId, "testuser", "Test", "User"); // Assert Assert.That(result, Is.EqualTo(existingUser)); - _repositoryMock.Verify(x => x.GetUserByTelegramIdAsync(telegramId), Times.Once); - _repositoryMock.Verify(x => x.CreateUserAsync(It.IsAny()), Times.Never); + repositoryMock.Verify(x => x.GetUserByTelegramIdAsync(telegramId), Times.Once); + repositoryMock.Verify(x => x.CreateUserAsync(It.IsAny()), Times.Never); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Found existing user")), + It.Is((v, t) => v.ToString() !.Contains("Found existing user")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that when a user does not exist in the database, + /// the method creates a new user with the provided information. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetOrCreateUserAsync_NewUser_CreatesUser() + public async Task GetOrCreateUserAsyncNewUserCreatesUser() { // Arrange var telegramId = 123L; @@ -63,14 +88,14 @@ public async Task GetOrCreateUserAsync_NewUser_CreatesUser() Username = "newuser", FirstName = "New", LastName = "User", - Admin = false + Admin = false, }; - _repositoryMock + repositoryMock .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) - .ReturnsAsync((Users)null); + .ReturnsAsync((Users)null!); - _repositoryMock + repositoryMock .Setup(x => x.CreateUserAsync(It.Is(u => u.TelegramId == telegramId && u.Username == "newuser" && @@ -80,35 +105,40 @@ public async Task GetOrCreateUserAsync_NewUser_CreatesUser() .ReturnsAsync(newUser); // Act - var result = await _donationService.GetOrCreateUserAsync(telegramId, "newuser", "New", "User"); + var result = await donationService.GetOrCreateUserAsync(telegramId, "newuser", "New", "User"); // Assert Assert.That(result, Is.EqualTo(newUser)); - _repositoryMock.Verify(x => x.GetUserByTelegramIdAsync(telegramId), Times.Once); - _repositoryMock.Verify(x => x.CreateUserAsync(It.IsAny()), Times.Once); + repositoryMock.Verify(x => x.GetUserByTelegramIdAsync(telegramId), Times.Once); + repositoryMock.Verify(x => x.CreateUserAsync(It.IsAny()), Times.Once); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Creating new user") && v.ToString().Contains("newuser")), + It.Is((v, t) => v.ToString() !.Contains("Creating new user") && v.ToString() !.Contains("newuser")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that the method correctly handles null values for user properties + /// when creating a new user. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetOrCreateUserAsync_NullNames_HandlesCorrectly() + public async Task GetOrCreateUserAsyncNullNamesHandlesCorrectly() { // Arrange var telegramId = 123L; var newUser = new Users { Id = 1, TelegramId = telegramId, Username = null, FirstName = null, LastName = null }; - _repositoryMock + repositoryMock .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) - .ReturnsAsync((Users)null); + .ReturnsAsync((Users)null!); - _repositoryMock + repositoryMock .Setup(x => x.CreateUserAsync(It.Is(u => u.TelegramId == telegramId && u.Username == null && @@ -117,39 +147,48 @@ public async Task GetOrCreateUserAsync_NullNames_HandlesCorrectly() .ReturnsAsync(newUser); // Act - var result = await _donationService.GetOrCreateUserAsync(telegramId, null, null, null); + var result = await donationService.GetOrCreateUserAsync(telegramId, null, null, null); // Assert Assert.That(result, Is.EqualTo(newUser)); } + /// + /// Tests that when the repository throws an exception, + /// the method logs the error and rethrows the exception. + /// [Test] - public async Task GetOrCreateUserAsync_RepositoryThrows_LogsErrorAndThrows() + public void GetOrCreateUserAsyncRepositoryThrowsLogsErrorAndThrows() { // Arrange var telegramId = 123L; var exception = new Exception("Database error"); - _repositoryMock + repositoryMock .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) .ThrowsAsync(exception); // Act & Assert Assert.ThrowsAsync(() => - _donationService.GetOrCreateUserAsync(telegramId, "testuser", "Test", "User")); + donationService.GetOrCreateUserAsync(telegramId, "testuser", "Test", "User")); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error getting or creating user")), + It.Is((v, t) => v.ToString() !.Contains("Error getting or creating user")), exception, - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that a successful donation is processed correctly, + /// creating a donation record and updating the goal amount. + /// + /// A task representing the asynchronous operation. [Test] - public async Task ProcessDonationAsync_SuccessfulDonation_ReturnsTrue() + public async Task ProcessDonationAsyncSuccessfulDonationReturnsTrue() { // Arrange var userId = 123L; @@ -159,15 +198,15 @@ public async Task ProcessDonationAsync_SuccessfulDonation_ReturnsTrue() var goal = new DonationGoal { Id = 1, Title = "Test Goal", TargetAmount = 10000, CurrentAmount = 0 }; var donation = new Donation { Id = 1, UserTelegramId = userId, GoalId = goal.Id, Amount = amount, Currency = currency, ProviderPaymentId = donationId }; - _repositoryMock + repositoryMock .Setup(x => x.GetDonationAsync(donationId)) - .ReturnsAsync((Donation)null); + .ReturnsAsync((Donation)null!); - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(goal); - _repositoryMock + repositoryMock .Setup(x => x.CreateDonationAsync(It.Is(d => d.UserTelegramId == userId && d.GoalId == goal.Id && @@ -177,33 +216,38 @@ public async Task ProcessDonationAsync_SuccessfulDonation_ReturnsTrue() d.Status == "completed"))) .ReturnsAsync(donation); - _repositoryMock + repositoryMock .Setup(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount)) .Returns(Task.CompletedTask); // Act - var result = await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + var result = await donationService.ProcessDonationAsync(userId, amount, currency, donationId); // Assert Assert.That(result, Is.True); - _repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); - _repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); - _repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Once); - _repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount), Times.Once); + repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); + repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Once); + repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount), Times.Once); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Successfully processed donation")), + It.Is((v, t) => v.ToString() !.Contains("Successfully processed donation")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that when a duplicate donation is detected (same donation ID), + /// the method returns true without creating a new donation record. + /// + /// A task representing the asynchronous operation. [Test] - public async Task ProcessDonationAsync_DuplicateDonation_ReturnsTrue() + public async Task ProcessDonationAsyncDuplicateDonationReturnsTrue() { // Arrange var userId = 123L; @@ -213,33 +257,38 @@ public async Task ProcessDonationAsync_DuplicateDonation_ReturnsTrue() var donationId = "donation_123"; var existingDonation = new Donation { Id = 1, UserTelegramId = userId, GoalId = goalId, Amount = amount, CreatedAt = DateTime.UtcNow }; - _repositoryMock + repositoryMock .Setup(x => x.GetDonationAsync(donationId)) .ReturnsAsync(existingDonation); // Act - var result = await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + var result = await donationService.ProcessDonationAsync(userId, amount, currency, donationId); // Assert Assert.That(result, Is.True); - _repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); - _repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Never); - _repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Never); - _repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(It.IsAny(), It.IsAny()), Times.Never); + repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); + repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Never); + repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Never); + repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(It.IsAny(), It.IsAny()), Times.Never); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Warning, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("has already been processed")), + It.Is((v, t) => v.ToString() !.Contains("has already been processed")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that when there is no active donation goal, + /// the method returns false and logs an error. + /// + /// A task representing the asynchronous operation. [Test] - public async Task ProcessDonationAsync_NoActiveGoal_ReturnsFalse() + public async Task ProcessDonationAsyncNoActiveGoalReturnsFalse() { // Arrange var userId = 123L; @@ -247,37 +296,42 @@ public async Task ProcessDonationAsync_NoActiveGoal_ReturnsFalse() var currency = "RUB"; var donationId = "donation_123"; - _repositoryMock + repositoryMock .Setup(x => x.GetDonationAsync(donationId)) - .ReturnsAsync((Donation)null); + .ReturnsAsync((Donation)null!); - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) - .ReturnsAsync((DonationGoal)null); + .ReturnsAsync((DonationGoal)null!); // Act - var result = await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + var result = await donationService.ProcessDonationAsync(userId, amount, currency, donationId); // Assert Assert.That(result, Is.False); - _repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); - _repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); - _repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Never); - _repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(It.IsAny(), It.IsAny()), Times.Never); + repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); + repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Never); + repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(It.IsAny(), It.IsAny()), Times.Never); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("No active goal found for donation")), + It.Is((v, t) => v.ToString() !.Contains("No active goal found for donation")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that when creating a donation record throws an exception, + /// the method returns false and logs the error. + /// + /// A task representing the asynchronous operation. [Test] - public async Task ProcessDonationAsync_CreateDonationThrows_ReturnsFalse() + public async Task ProcessDonationAsyncCreateDonationThrowsReturnsFalse() { // Arrange var userId = 123L; @@ -287,41 +341,46 @@ public async Task ProcessDonationAsync_CreateDonationThrows_ReturnsFalse() var goal = new DonationGoal { Id = 1, Title = "Test Goal" }; var exception = new Exception("Database error"); - _repositoryMock + repositoryMock .Setup(x => x.GetDonationAsync(donationId)) - .ReturnsAsync((Donation)null); + .ReturnsAsync((Donation)null!); - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(goal); - _repositoryMock + repositoryMock .Setup(x => x.CreateDonationAsync(It.IsAny())) .ThrowsAsync(exception); // Act - var result = await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + var result = await donationService.ProcessDonationAsync(userId, amount, currency, donationId); // Assert Assert.That(result, Is.False); - _repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); - _repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); - _repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Once); - _repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(It.IsAny(), It.IsAny()), Times.Never); + repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); + repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Once); + repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(It.IsAny(), It.IsAny()), Times.Never); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error processing donation")), + It.Is((v, t) => v.ToString() !.Contains("Error processing donation")), exception, - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that when updating the goal amount throws an exception, + /// the method returns false and logs the error. + /// + /// A task representing the asynchronous operation. [Test] - public async Task ProcessDonationAsync_UpdateGoalThrows_ReturnsFalse() + public async Task ProcessDonationAsyncUpdateGoalThrowsReturnsFalse() { // Arrange var userId = 123L; @@ -332,45 +391,49 @@ public async Task ProcessDonationAsync_UpdateGoalThrows_ReturnsFalse() var donation = new Donation { Id = 1, UserTelegramId = userId, GoalId = goal.Id, Amount = amount }; var exception = new Exception("Database error"); - _repositoryMock + repositoryMock .Setup(x => x.GetDonationAsync(donationId)) - .ReturnsAsync((Donation)null); + .ReturnsAsync((Donation)null!); - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(goal); - _repositoryMock + repositoryMock .Setup(x => x.CreateDonationAsync(It.IsAny())) .ReturnsAsync(donation); - _repositoryMock + repositoryMock .Setup(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount)) .ThrowsAsync(exception); // Act - var result = await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + var result = await donationService.ProcessDonationAsync(userId, amount, currency, donationId); // Assert Assert.That(result, Is.False); - _repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); - _repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); - _repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Once); - _repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount), Times.Once); + repositoryMock.Verify(x => x.GetDonationAsync(donationId), Times.Once); + repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + repositoryMock.Verify(x => x.CreateDonationAsync(It.IsAny()), Times.Once); + repositoryMock.Verify(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount), Times.Once); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error processing donation")), + It.Is((v, t) => v.ToString() !.Contains("Error processing donation")), exception, - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that the donation record is created with the correct status ("completed"). + /// + /// A task representing the asynchronous operation. [Test] - public async Task ProcessDonationAsync_ValidatesDonationStatus() + public async Task ProcessDonationAsyncValidatesDonationStatus() { // Arrange var userId = 123L; @@ -380,34 +443,38 @@ public async Task ProcessDonationAsync_ValidatesDonationStatus() var goal = new DonationGoal { Id = 1, Title = "Test Goal" }; var donation = new Donation { Id = 1, UserTelegramId = userId, GoalId = goal.Id, Amount = amount }; - _repositoryMock + repositoryMock .Setup(x => x.GetDonationAsync(donationId)) - .ReturnsAsync((Donation)null); + .ReturnsAsync((Donation)null!); - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(goal); - _repositoryMock + repositoryMock .Setup(x => x.CreateDonationAsync(It.Is(d => d.Status == "completed"))) .ReturnsAsync(donation); - _repositoryMock + repositoryMock .Setup(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount)) .Returns(Task.CompletedTask); // Act - var result = await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + var result = await donationService.ProcessDonationAsync(userId, amount, currency, donationId); // Assert Assert.That(result, Is.True); // Проверяем, что статус доната установлен в "completed" - _repositoryMock.Verify(x => x.CreateDonationAsync(It.Is(d => d.Status == "completed")), Times.Once); + repositoryMock.Verify(x => x.CreateDonationAsync(It.Is(d => d.Status == "completed")), Times.Once); } + /// + /// Tests that appropriate log messages are recorded during donation processing. + /// + /// A task representing the asynchronous operation. [Test] - public async Task ProcessDonationAsync_LogsAppropriateMessages() + public async Task ProcessDonationAsyncLogsAppropriateMessages() { // Arrange var userId = 123L; @@ -417,51 +484,51 @@ public async Task ProcessDonationAsync_LogsAppropriateMessages() var goal = new DonationGoal { Id = 1, Title = "Test Goal" }; var donation = new Donation { Id = 1, UserTelegramId = userId, GoalId = goal.Id, Amount = amount }; - _repositoryMock + repositoryMock .Setup(x => x.GetDonationAsync(donationId)) - .ReturnsAsync((Donation)null); + .ReturnsAsync((Donation)null!); - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(goal); - _repositoryMock + repositoryMock .Setup(x => x.CreateDonationAsync(It.IsAny())) .ReturnsAsync(donation); - _repositoryMock + repositoryMock .Setup(x => x.UpdateGoalCurrentAmountAsync(goal.Id, amount)) .Returns(Task.CompletedTask); // Act - await _donationService.ProcessDonationAsync(userId, amount, currency, donationId); + await donationService.ProcessDonationAsync(userId, amount, currency, donationId); // Assert - проверяем последовательность логирования - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Processing donation")), + It.Is((v, t) => v.ToString() !.Contains("Processing donation")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Found active goal")), + It.Is((v, t) => v.ToString() !.Contains("Found active goal")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Created donation record")), + It.Is((v, t) => v.ToString() !.Contains("Created donation record")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } } \ No newline at end of file diff --git a/TestBot/Data/GoalServiceTests.cs b/TestBot/Data/GoalServiceTests.cs index 11a6aad..16b1a58 100755 --- a/TestBot/Data/GoalServiceTests.cs +++ b/TestBot/Data/GoalServiceTests.cs @@ -1,4 +1,12 @@ -using Data; +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Data; using Data.Models; using Microsoft.Extensions.Logging; using Moq; @@ -7,188 +15,232 @@ namespace Services.Tests; +/// +/// Contains unit tests for the class. +/// Tests user authorization, goal management, statistics generation, and progress tracking functionality. +/// [TestFixture] public class GoalServiceTests { - private Mock _repositoryMock; - private Mock> _loggerMock; - private GoalService _goalService; - + private Mock repositoryMock; + private Mock> loggerMock; + private GoalService goalService; + + /// + /// Initializes test environment before each test execution. + /// Sets up mock dependencies, creates service instance, and configures Russian culture for formatting tests. + /// [SetUp] public void Setup() { - _repositoryMock = new Mock(); - _loggerMock = new Mock>(); - _goalService = new GoalService(_repositoryMock.Object, _loggerMock.Object); + repositoryMock = new Mock(); + loggerMock = new Mock>(); + goalService = new GoalService(repositoryMock.Object, loggerMock.Object); + + Thread.CurrentThread.CurrentCulture = new CultureInfo("ru-RU"); + Thread.CurrentThread.CurrentUICulture = new CultureInfo("ru-RU"); } + /// + /// Tests that a user with admin privileges is correctly identified as an administrator. + /// + /// A task representing the asynchronous operation. [Test] - public async Task IsUserAdminAsync_AdminUser_ReturnsTrue() + public async Task IsUserAdminAsyncAdminUserReturnsTrue() { // Arrange var telegramId = 123L; var adminUser = new Users { Id = 1, TelegramId = telegramId, Admin = true }; - _repositoryMock + repositoryMock .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) .ReturnsAsync(adminUser); // Act - var result = await _goalService.IsUserAdminAsync(telegramId); + var result = await goalService.IsUserAdminAsync(telegramId); // Assert Assert.That(result, Is.True); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("User") && v.ToString().Contains("admin status: True")), + It.Is((v, t) => v.ToString() !.Contains("User") && v.ToString() !.Contains("admin status: True")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that a non-admin user is correctly identified as not having administrator privileges. + /// + /// A task representing the asynchronous operation. [Test] - public async Task IsUserAdminAsync_NonAdminUser_ReturnsFalse() + public async Task IsUserAdminAsyncNonAdminUserReturnsFalse() { // Arrange var telegramId = 123L; var regularUser = new Users { Id = 1, TelegramId = telegramId, Admin = false }; - _repositoryMock + repositoryMock .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) .ReturnsAsync(regularUser); // Act - var result = await _goalService.IsUserAdminAsync(telegramId); + var result = await goalService.IsUserAdminAsync(telegramId); // Assert Assert.That(result, Is.False); } + /// + /// Tests that when a user is not found in the database, the method returns false. + /// + /// A task representing the asynchronous operation. [Test] - public async Task IsUserAdminAsync_UserNotFound_ReturnsFalse() + public async Task IsUserAdminAsyncUserNotFoundReturnsFalse() { // Arrange var telegramId = 999L; - _repositoryMock + repositoryMock .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) - .ReturnsAsync((Users)null); + .ReturnsAsync((Users)null!); // Act - var result = await _goalService.IsUserAdminAsync(telegramId); + var result = await goalService.IsUserAdminAsync(telegramId); // Assert Assert.That(result, Is.False); } + /// + /// Tests that when the repository throws an exception, the method returns false and logs an error. + /// + /// A task representing the asynchronous operation. [Test] - public async Task IsUserAdminAsync_RepositoryThrows_ReturnsFalseAndLogsError() + public async Task IsUserAdminAsyncRepositoryThrowsReturnsFalseAndLogsError() { // Arrange var telegramId = 123L; var exception = new Exception("Database error"); - _repositoryMock + repositoryMock .Setup(x => x.GetUserByTelegramIdAsync(telegramId)) .ThrowsAsync(exception); // Act - var result = await _goalService.IsUserAdminAsync(telegramId); + var result = await goalService.IsUserAdminAsync(telegramId); // Assert Assert.That(result, Is.False); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error checking admin status")), + It.Is((v, t) => v.ToString() !.Contains("Error checking admin status")), exception, - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that when an active goal exists, it is successfully retrieved from the repository. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetActiveGoalAsync_GoalExists_ReturnsGoal() + public async Task GetActiveGoalAsyncGoalExistsReturnsGoal() { // Arrange var goal = new DonationGoal { Id = 1, Title = "Test Goal", TargetAmount = 10000, CurrentAmount = 5000 }; - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(goal); // Act - var result = await _goalService.GetActiveGoalAsync(); + var result = await goalService.GetActiveGoalAsync(); // Assert Assert.That(result, Is.EqualTo(goal)); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Active goal retrieval succeeded")), + It.Is((v, t) => v.ToString() !.Contains("Active goal retrieval succeeded")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that when no active goal exists, the method returns null. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetActiveGoalAsync_NoGoal_ReturnsNull() + public async Task GetActiveGoalAsyncNoGoalReturnsNull() { // Arrange - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) - .ReturnsAsync((DonationGoal)null); + .ReturnsAsync((DonationGoal)null!); // Act - var result = await _goalService.GetActiveGoalAsync(); + var result = await goalService.GetActiveGoalAsync(); // Assert Assert.That(result, Is.Null); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Active goal retrieval failed - no active goal")), + It.Is((v, t) => v.ToString() !.Contains("Active goal retrieval failed - no active goal")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that when the repository throws an exception, the method returns null and logs an error. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetActiveGoalAsync_RepositoryThrows_ReturnsNullAndLogsError() + public async Task GetActiveGoalAsyncRepositoryThrowsReturnsNullAndLogsError() { // Arrange var exception = new Exception("Database error"); - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ThrowsAsync(exception); // Act - var result = await _goalService.GetActiveGoalAsync(); + var result = await goalService.GetActiveGoalAsync(); // Assert Assert.That(result, Is.Null); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error retrieving active goal")), + It.Is((v, t) => v.ToString() !.Contains("Error retrieving active goal")), exception, - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that when an active goal exists, detailed statistics are correctly formatted and returned. + /// Includes verification of progress bars, percentage calculations, and formatted currency values. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetGoalStatsAsync_WithActiveGoal_ReturnsFormattedStats() + public async Task GetGoalStatsAsyncWithActiveGoalReturnsFormattedStats() { // Arrange var goal = new DonationGoal @@ -198,23 +250,23 @@ public async Task GetGoalStatsAsync_WithActiveGoal_ReturnsFormattedStats() Description = "Test Description", TargetAmount = 10000, CurrentAmount = 5000, - CreatedAt = new DateTime(2024, 1, 1) + CreatedAt = new DateTime(2024, 1, 1), }; - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(goal); - _repositoryMock + repositoryMock .Setup(x => x.GetCountUsersForActiveGoals()) .ReturnsAsync(25); - _repositoryMock + repositoryMock .Setup(x => x.GetCountDonationsForActiveGoals()) .ReturnsAsync(50); // Act - var result = await _goalService.GetGoalStatsAsync(); + var result = await goalService.GetGoalStatsAsync(); // Assert Assert.That(result, Is.Not.Null); @@ -228,37 +280,45 @@ public async Task GetGoalStatsAsync_WithActiveGoal_ReturnsFormattedStats() Assert.That(result, Contains.Substring("01.01.2024")); Assert.That(result, Contains.Substring("[■■■■■□□□□□]")); // 50% progress bar - _repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); - _repositoryMock.Verify(x => x.GetCountUsersForActiveGoals(), Times.Once); - _repositoryMock.Verify(x => x.GetCountDonationsForActiveGoals(), Times.Once); + repositoryMock.Verify(x => x.GetActiveGoalAsync(), Times.Once); + repositoryMock.Verify(x => x.GetCountUsersForActiveGoals(), Times.Once); + repositoryMock.Verify(x => x.GetCountDonationsForActiveGoals(), Times.Once); } + /// + /// Tests that when no active goal exists, an appropriate message is returned to the user. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetGoalStatsAsync_NoActiveGoal_ReturnsNoActiveGoalMessage() + public async Task GetGoalStatsAsyncNoActiveGoalReturnsNoActiveGoalMessage() { // Arrange - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) - .ReturnsAsync((DonationGoal)null); + .ReturnsAsync((DonationGoal)null!); // Act - var result = await _goalService.GetGoalStatsAsync(); + var result = await goalService.GetGoalStatsAsync(); // Assert Assert.That(result, Is.EqualTo("🎯 На данный момент нет активных целей для сбора.")); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("No active goal found for statistics")), + It.Is((v, t) => v.ToString() !.Contains("No active goal found for statistics")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that goal statistics handle zero target amounts correctly to prevent division by zero. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetGoalStatsAsync_ZeroTargetAmount_HandlesCorrectly() + public async Task GetGoalStatsAsyncZeroTargetAmountHandlesCorrectly() { // Arrange var goal = new DonationGoal @@ -266,27 +326,27 @@ public async Task GetGoalStatsAsync_ZeroTargetAmount_HandlesCorrectly() Id = 1, Title = "Test Goal", TargetAmount = 0, - CurrentAmount = 5000 + CurrentAmount = 5000, }; - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(goal); - _repositoryMock + repositoryMock .Setup(x => x.GetCountUsersForActiveGoals()) .ReturnsAsync(10); - _repositoryMock + repositoryMock .Setup(x => x.GetCountDonationsForActiveGoals()) .ReturnsAsync(20); // Act - var result = await _goalService.GetGoalStatsAsync(); + var result = await goalService.GetGoalStatsAsync(); // Assert Assert.That(result, Is.Not.Null); - var today = DateTime.Now.ToString("dd.MM.yyyy"); + var today = DateTime.UtcNow.ToString("dd.MM.yyyy"); Assert.That(result, Contains.Substring( $"🎯 **Test Goal** — 0₽ \n" + $"📝 Описание: \n\n" + @@ -297,8 +357,12 @@ public async Task GetGoalStatsAsync_ZeroTargetAmount_HandlesCorrectly() $"[□□□□□□□□□□]")); } + /// + /// Tests that when goal progress reaches 100%, a full progress bar is displayed. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetGoalStatsAsync_FullProgress_ShowsFullProgressBar() + public async Task GetGoalStatsAsyncFullProgressShowsFullProgressBar() { // Arrange var goal = new DonationGoal @@ -306,57 +370,65 @@ public async Task GetGoalStatsAsync_FullProgress_ShowsFullProgressBar() Id = 1, Title = "Test Goal", TargetAmount = 1000, - CurrentAmount = 1000 + CurrentAmount = 1000, }; - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(goal); - _repositoryMock + repositoryMock .Setup(x => x.GetCountUsersForActiveGoals()) .ReturnsAsync(10); - _repositoryMock + repositoryMock .Setup(x => x.GetCountDonationsForActiveGoals()) .ReturnsAsync(20); // Act - var result = await _goalService.GetGoalStatsAsync(); + var result = await goalService.GetGoalStatsAsync(); // Assert Assert.That(result, Contains.Substring("[■■■■■■■■■■]")); // 100% progress bar Assert.That(result, Contains.Substring("100,0%")); } + /// + /// Tests that when repository operations fail, an error message is returned and the exception is logged. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetGoalStatsAsync_RepositoryThrows_ReturnsErrorMessage() + public async Task GetGoalStatsAsyncRepositoryThrowsReturnsErrorMessage() { // Arrange var exception = new Exception("Database error"); - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ThrowsAsync(exception); // Act - var result = await _goalService.GetGoalStatsAsync(); + var result = await goalService.GetGoalStatsAsync(); // Assert Assert.That(result, Is.EqualTo("🎯 На данный момент нет активных целей для сбора.")); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error retrieving active goal")), + It.Is((v, t) => v.ToString() !.Contains("Error retrieving active goal")), exception, - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that start command statistics are correctly formatted when an active goal exists. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetStartStats_WithActiveGoal_ReturnsFormattedStats() + public async Task GetStartStatsWithActiveGoalReturnsFormattedStats() { // Arrange var goal = new DonationGoal @@ -365,15 +437,15 @@ public async Task GetStartStats_WithActiveGoal_ReturnsFormattedStats() Title = "Test Goal", Description = "Test Description", TargetAmount = 10000, - CurrentAmount = 7500 + CurrentAmount = 7500, }; - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(goal); // Act - var result = await _goalService.GetStartStats(); + var result = await goalService.GetStartStats(); // Assert Assert.That(result, Is.Not.Null); @@ -385,58 +457,70 @@ public async Task GetStartStats_WithActiveGoal_ReturnsFormattedStats() Assert.That(result, Contains.Substring("[■■■■■■■■□□]")); // 75% progress bar (rounded to 8 blocks out of 10) } + /// + /// Tests that when no active goal exists for start command, an appropriate message is returned. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetStartStats_NoActiveGoal_ReturnsNoActiveGoalMessage() + public async Task GetStartStatsNoActiveGoalReturnsNoActiveGoalMessage() { // Arrange - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) - .ReturnsAsync((DonationGoal)null); + .ReturnsAsync((DonationGoal)null!); // Act - var result = await _goalService.GetStartStats(); + var result = await goalService.GetStartStats(); // Assert Assert.That(result, Is.EqualTo("🎯 На данный момент нет активных целей для сбора.")); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("No active goal found for start statistics")), + It.Is((v, t) => v.ToString() !.Contains("No active goal found for start statistics")), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that repository exceptions during start statistics retrieval are handled gracefully. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetStartStats_RepositoryThrows_ReturnsErrorMessage() + public async Task GetStartStatsRepositoryThrowsReturnsErrorMessage() { // Arrange var exception = new Exception("Database error"); - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ThrowsAsync(exception); // Act - var result = await _goalService.GetStartStats(); + var result = await goalService.GetStartStats(); // Assert Assert.That(result, Is.EqualTo("🎯 На данный момент нет активных целей для сбора.")); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error retrieving active goal")), + It.Is((v, t) => v.ToString() !.Contains("Error retrieving active goal")), exception, - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that a valid goal is successfully created and persisted to the repository. + /// + /// A task representing the asynchronous operation. [Test] - public async Task CreateGoalAsync_ValidGoal_CreatesAndReturnsGoal() + public async Task CreateGoalAsyncValidGoalCreatesAndReturnsGoal() { // Arrange var title = "New Goal"; @@ -444,7 +528,7 @@ public async Task CreateGoalAsync_ValidGoal_CreatesAndReturnsGoal() var targetAmount = 5000m; var createdGoal = new DonationGoal { Id = 1, Title = title, Description = description, TargetAmount = targetAmount }; - _repositoryMock + repositoryMock .Setup(x => x.CreateGoalAsync(It.Is(g => g.Title == title && g.Description == description && @@ -453,23 +537,26 @@ public async Task CreateGoalAsync_ValidGoal_CreatesAndReturnsGoal() .ReturnsAsync(createdGoal); // Act - var result = await _goalService.CreateGoalAsync(title, description, targetAmount); + var result = await goalService.CreateGoalAsync(title, description, targetAmount); // Assert Assert.That(result, Is.EqualTo(createdGoal)); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Goal created successfully") && v.ToString().Contains(title)), + It.Is((v, t) => v.ToString() !.Contains("Goal created successfully") && v.ToString() !.Contains(title)), It.IsAny(), - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that repository exceptions during goal creation are properly logged and re-thrown. + /// [Test] - public void CreateGoalAsync_RepositoryThrows_LogsErrorAndThrows() + public void CreateGoalAsyncRepositoryThrowsLogsErrorAndThrows() { // Arrange var title = "New Goal"; @@ -477,32 +564,37 @@ public void CreateGoalAsync_RepositoryThrows_LogsErrorAndThrows() var targetAmount = 5000m; var exception = new Exception("Database error"); - _repositoryMock + repositoryMock .Setup(x => x.CreateGoalAsync(It.IsAny())) .ThrowsAsync(exception); // Act & Assert Assert.ThrowsAsync(() => - _goalService.CreateGoalAsync(title, description, targetAmount)); + goalService.CreateGoalAsync(title, description, targetAmount)); - _loggerMock.Verify( + loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), - It.Is((v, t) => v.ToString().Contains("Error creating goal") && v.ToString().Contains(title)), + It.Is((v, t) => v.ToString() !.Contains("Error creating goal") && v.ToString() !.Contains(title)), exception, - It.Is>((v, t) => true)), + It.IsAny>()), Times.Once); } + /// + /// Tests that the progress bar generation correctly converts percentage values to visual representations. + /// Includes edge cases like 0%, 100%, and values exceeding 100%. + /// [Test] - public void CreateProgressBar_VariousPercentages_ReturnsCorrectBars() + public void CreateProgressBarVariousPercentagesReturnsCorrectBars() { // Arrange - var service = new GoalService(_repositoryMock.Object, _loggerMock.Object); + var service = new GoalService(repositoryMock.Object, loggerMock.Object); // Use reflection to test private method - var method = typeof(GoalService).GetMethod("CreateProgressBar", + var method = typeof(GoalService).GetMethod( + "CreateProgressBar", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); // Act & Assert @@ -523,8 +615,12 @@ public void CreateProgressBar_VariousPercentages_ReturnsCorrectBars() Assert.That(result110, Is.EqualTo("[■■■■■■■■■■]")); // Over 100% still shows full bar } + /// + /// Tests that partial progress percentages are correctly rounded for visual progress bar display. + /// + /// A task representing the asynchronous operation. [Test] - public async Task GetGoalStatsAsync_PartialProgressBar_RoundsCorrectly() + public async Task GetGoalStatsAsyncPartialProgressBarRoundsCorrectly() { // Arrange var goal = new DonationGoal @@ -532,27 +628,27 @@ public async Task GetGoalStatsAsync_PartialProgressBar_RoundsCorrectly() Id = 1, Title = "Test Goal", TargetAmount = 1000, - CurrentAmount = 123 // 12.3% + CurrentAmount = 123, // 12.3% }; - _repositoryMock + repositoryMock .Setup(x => x.GetActiveGoalAsync()) .ReturnsAsync(goal); - _repositoryMock + repositoryMock .Setup(x => x.GetCountUsersForActiveGoals()) .ReturnsAsync(5); - _repositoryMock + repositoryMock .Setup(x => x.GetCountDonationsForActiveGoals()) .ReturnsAsync(10); // Act - var result = await _goalService.GetGoalStatsAsync(); + var result = await goalService.GetGoalStatsAsync(); - var today = DateTime.Now.ToString("dd.MM.yyyy"); + var today = DateTime.UtcNow.ToString("dd.MM.yyyy"); Assert.That(result, Contains.Substring( - $"🎯 **Test Goal** — 1 000₽ \n" + + $"🎯 **Test Goal** — 1\u00A0000₽ \n" + $"📝 Описание: \n\n" + $"📈 Количество пожертвований на текущую цель: 10\n" + $"🧮 Количество пожертвовавших: 5 \n" +