diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..8e4c1ab --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,25 @@ +name: .NET Tests + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ "*" ] +env: + "telegram_test_token": ${{secrets.TELEGRAM_TEST_TOKEN}} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 7.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore /p:TreatWarningsAsErrors=false + - name: Test + run: dotnet test --no-build --verbosity normal -e telegram_test_token=${{secrets.TELEGRAM_TEST_TOKEN}} \ No newline at end of file diff --git a/Examples/Telegram.Examples/Program.cs b/Examples/Telegram.Examples/Program.cs index ae1d2ca..1f2436a 100644 --- a/Examples/Telegram.Examples/Program.cs +++ b/Examples/Telegram.Examples/Program.cs @@ -1,14 +1,18 @@ -using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Telegram.Net; var webHost = Host.CreateDefaultBuilder() + .ConfigureLogging(l => l.ClearProviders().AddConsole()) .ConfigureServices(k => { - k.ConnectTelegram(new("YOUR-TOKEN") + k.ConnectTelegram(new("TOKEN") { errorHandler = async (client, exception, ctx) => { - await Console.Out.WriteLineAsync(exception.Message); + Console.WriteLine(exception); } }); - }); \ No newline at end of file + }); +webHost.Build().Run(); \ No newline at end of file diff --git a/Examples/Telegram.Examples/Telegram.Examples.csproj b/Examples/Telegram.Examples/Telegram.Examples.csproj index 1622ed8..2891280 100644 --- a/Examples/Telegram.Examples/Telegram.Examples.csproj +++ b/Examples/Telegram.Examples/Telegram.Examples.csproj @@ -5,6 +5,7 @@ net7.0 enable enable + true diff --git a/Examples/Telegram.Examples/UpdatePolling/Update.cs b/Examples/Telegram.Examples/UpdatePolling/Update.cs index 622baf1..f6c703c 100644 --- a/Examples/Telegram.Examples/UpdatePolling/Update.cs +++ b/Examples/Telegram.Examples/UpdatePolling/Update.cs @@ -5,7 +5,7 @@ using Telegram.Net.Interfaces; namespace Telegram.Examples.UpdatePolling; -public class Update : IUpdatePollingSerivce +public class Update : IUpdatePollingService { [Update] public async Task UpdateExample(ITelegramBotClient client, Bot.Types.Update update, CancellationToken ctx) diff --git a/Telegram.Net.sln b/Telegram.Net.sln index 362a68f..9e3dd27 100644 --- a/Telegram.Net.sln +++ b/Telegram.Net.sln @@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Telegram.Net", "Telegram.Ne EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Telegram.Examples", "Examples\Telegram.Examples\Telegram.Examples.csproj", "{073E66F2-F82E-4378-B390-C2364DFDC491}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Telegram.Tests", "tests\Telegram.Tests\Telegram.Tests.csproj", "{1FF230D6-7DDC-48B6-8D56-2E14726FDD9E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +20,9 @@ Global {073E66F2-F82E-4378-B390-C2364DFDC491}.Debug|Any CPU.Build.0 = Debug|Any CPU {073E66F2-F82E-4378-B390-C2364DFDC491}.Release|Any CPU.ActiveCfg = Release|Any CPU {073E66F2-F82E-4378-B390-C2364DFDC491}.Release|Any CPU.Build.0 = Release|Any CPU + {1FF230D6-7DDC-48B6-8D56-2E14726FDD9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FF230D6-7DDC-48B6-8D56-2E14726FDD9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FF230D6-7DDC-48B6-8D56-2E14726FDD9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FF230D6-7DDC-48B6-8D56-2E14726FDD9E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Telegram.Net/AssemblyInfo.cs b/Telegram.Net/AssemblyInfo.cs new file mode 100644 index 0000000..bc47f6e --- /dev/null +++ b/Telegram.Net/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Telegram.Tests")] \ No newline at end of file diff --git a/Telegram.Net/Attributes/CallbackAttribute.cs b/Telegram.Net/Attributes/CallbackAttribute.cs index bc4f28e..278e61b 100644 --- a/Telegram.Net/Attributes/CallbackAttribute.cs +++ b/Telegram.Net/Attributes/CallbackAttribute.cs @@ -29,31 +29,5 @@ public class CallbackAttribute : Attribute public CallbackAttribute(string queryId) { this.QueryId = queryId; - - var methods = typeof(IUpdatePollingSerivce).GetMethods() - .Where(m => m.GetCustomAttribute(this.GetType()) != null); - - foreach (var method in methods) - { - if (IsValidHandlerMethod(method)) - { - var attr = method.GetCustomAttribute(); - var handler = (Func) - Delegate.CreateDelegate(typeof(Func), null, - method); - - TelegramHostedService.CallbackQueryHandler.Add(attr!.QueryId, handler); - } - } - } - - static bool IsValidHandlerMethod(MethodInfo method) - { - var parameters = method.GetParameters(); - return method.ReturnType == typeof(Task) && - parameters.Length == 3 && - parameters[0].ParameterType == typeof(ITelegramBotClient) && - parameters[1].ParameterType == typeof(CallbackQuery) && - parameters[2].ParameterType == typeof(CancellationToken); } } \ No newline at end of file diff --git a/Telegram.Net/Attributes/CommandAttribute.cs b/Telegram.Net/Attributes/CommandAttribute.cs index 89c7072..5b70dab 100644 --- a/Telegram.Net/Attributes/CommandAttribute.cs +++ b/Telegram.Net/Attributes/CommandAttribute.cs @@ -29,31 +29,5 @@ public class CommandAttribute : Attribute public CommandAttribute(string command) { Command = command; - - var methods = typeof(IUpdatePollingSerivce).GetMethods() - .Where(m => m.GetCustomAttribute(this.GetType()) != null); - - foreach (var method in methods) - { - if (IsValidHandlerMethod(method)) - { - var attr = method.GetCustomAttribute(); - var handler = (Func) - Delegate.CreateDelegate(typeof(Func), null, - method); - - TelegramHostedService.CommandHandler.Add(attr!.Command, handler); - } - } - } - - static bool IsValidHandlerMethod(MethodInfo method) - { - var parameters = method.GetParameters(); - return method.ReturnType == typeof(Task) && - parameters.Length == 3 && - parameters[0].ParameterType == typeof(ITelegramBotClient) && - parameters[1].ParameterType == typeof(Message) && - parameters[2].ParameterType == typeof(CancellationToken); } } \ No newline at end of file diff --git a/Telegram.Net/Attributes/EditMessageAttribute.cs b/Telegram.Net/Attributes/EditMessageAttribute.cs index 0190c3c..f29724e 100644 --- a/Telegram.Net/Attributes/EditMessageAttribute.cs +++ b/Telegram.Net/Attributes/EditMessageAttribute.cs @@ -26,29 +26,5 @@ public class EditMessageAttribute : Attribute /// public EditMessageAttribute() { - var methods = typeof(IUpdatePollingSerivce).GetMethods() - .Where(m => m.GetCustomAttribute(this.GetType()) != null); - - foreach (var method in methods) - { - if (IsValidHandlerMethod(method)) - { - var handler = (Func) - Delegate.CreateDelegate(typeof(Func), null, - method); - - TelegramHostedService.EditedMessageHandler.Add(handler); - } - } - } - - static bool IsValidHandlerMethod(MethodInfo method) - { - var parameters = method.GetParameters(); - return method.ReturnType == typeof(Task) && - parameters.Length == 3 && - parameters[0].ParameterType == typeof(ITelegramBotClient) && - parameters[1].ParameterType == typeof(Message) && - parameters[2].ParameterType == typeof(CancellationToken); } } \ No newline at end of file diff --git a/Telegram.Net/Attributes/InlineAttribute.cs b/Telegram.Net/Attributes/InlineAttribute.cs index bfaa31d..218e7d0 100644 --- a/Telegram.Net/Attributes/InlineAttribute.cs +++ b/Telegram.Net/Attributes/InlineAttribute.cs @@ -31,30 +31,5 @@ public class InlineAttribute : Attribute public InlineAttribute(string inlineId) { this.InlineId = inlineId; - var methods = typeof(IUpdatePollingSerivce).GetMethods() - .Where(m => m.GetCustomAttribute(this.GetType()) != null); - - foreach (var method in methods) - { - if (IsValidHandlerMethod(method)) - { - var attr = method.GetCustomAttribute(); - var handler = (Func) - Delegate.CreateDelegate(typeof(Func), null, - method); - - TelegramHostedService.InlineHandler.Add(attr!.InlineId, handler); - } - } - } - - static bool IsValidHandlerMethod(MethodInfo method) - { - var parameters = method.GetParameters(); - return method.ReturnType == typeof(Task) && - parameters.Length == 3 && - parameters[0].ParameterType == typeof(ITelegramBotClient) && - parameters[1].ParameterType == typeof(InlineQuery) && - parameters[2].ParameterType == typeof(CancellationToken); } } \ No newline at end of file diff --git a/Telegram.Net/Attributes/PreCheckoutAttribute.cs b/Telegram.Net/Attributes/PreCheckoutAttribute.cs index 924609c..e90ada1 100644 --- a/Telegram.Net/Attributes/PreCheckoutAttribute.cs +++ b/Telegram.Net/Attributes/PreCheckoutAttribute.cs @@ -22,29 +22,5 @@ public class PreCheckoutAttribute : Attribute public bool IsReusable => true; public PreCheckoutAttribute() { - var methods = typeof(IUpdatePollingSerivce).GetMethods() - .Where(m => m.GetCustomAttribute(this.GetType()) != null); - - foreach (var method in methods) - { - if (IsValidHandlerMethod(method)) - { - var handler = (Func) - Delegate.CreateDelegate(typeof(Func), null, - method); - - TelegramHostedService.PreCheckoutHandler = (handler); - } - } - } - - static bool IsValidHandlerMethod(MethodInfo method) - { - var parameters = method.GetParameters(); - return method.ReturnType == typeof(Task) && - parameters.Length == 3 && - parameters[0].ParameterType == typeof(ITelegramBotClient) && - parameters[1].ParameterType == typeof(PreCheckoutQuery) && - parameters[2].ParameterType == typeof(CancellationToken); } } \ No newline at end of file diff --git a/Telegram.Net/Attributes/UpdateAttribute.cs b/Telegram.Net/Attributes/UpdateAttribute.cs index 597d53d..f287a51 100644 --- a/Telegram.Net/Attributes/UpdateAttribute.cs +++ b/Telegram.Net/Attributes/UpdateAttribute.cs @@ -21,29 +21,5 @@ public class UpdateAttribute : Attribute public bool IsReusable => true; public UpdateAttribute() { - var methods = typeof(IUpdatePollingSerivce).GetMethods() - .Where(m => m.GetCustomAttribute(this.GetType()) != null); - - foreach (var method in methods) - { - if (IsValidHandlerMethod(method)) - { - var handler = (Func) - Delegate.CreateDelegate(typeof(Func), null, - method); - - TelegramHostedService.DefaultUpdateHandler.Add(handler); - } - } - } - - static bool IsValidHandlerMethod(MethodInfo method) - { - var parameters = method.GetParameters(); - return method.ReturnType == typeof(Task) && - parameters.Length == 3 && - parameters[0].ParameterType == typeof(ITelegramBotClient) && - parameters[1].ParameterType == typeof(Update) && - parameters[2].ParameterType == typeof(CancellationToken); } } \ No newline at end of file diff --git a/Telegram.Net/Interfaces/ITelegramBotConfig.cs b/Telegram.Net/Interfaces/ITelegramBotConfig.cs index d928e70..cdb7865 100644 --- a/Telegram.Net/Interfaces/ITelegramBotConfig.cs +++ b/Telegram.Net/Interfaces/ITelegramBotConfig.cs @@ -8,7 +8,7 @@ public interface ITelegramBotConfig /// /// Token of telegram bot. You can take it from @BotFather /// - public string Token { internal get; init; } + public string Token { get; set; } /// /// Custom error handler for bot. You can add custom logger or anything. /// diff --git a/Telegram.Net/Interfaces/IUpdatePollingSerivce.cs b/Telegram.Net/Interfaces/IUpdatePollingService.cs similarity index 80% rename from Telegram.Net/Interfaces/IUpdatePollingSerivce.cs rename to Telegram.Net/Interfaces/IUpdatePollingService.cs index 929a654..92f9b21 100644 --- a/Telegram.Net/Interfaces/IUpdatePollingSerivce.cs +++ b/Telegram.Net/Interfaces/IUpdatePollingService.cs @@ -3,7 +3,7 @@ /// /// You should implement this interface in all your classes with update polling logic /// -public interface IUpdatePollingSerivce +public interface IUpdatePollingService { } \ No newline at end of file diff --git a/Telegram.Net/Models/TelegramBotConfig.cs b/Telegram.Net/Models/TelegramBotConfig.cs index 84fd5b7..0fbb672 100644 --- a/Telegram.Net/Models/TelegramBotConfig.cs +++ b/Telegram.Net/Models/TelegramBotConfig.cs @@ -11,7 +11,7 @@ public class TelegramBotConfig : ITelegramBotConfig Token = token; } - public string Token { get; init; } + public string Token { get; set; } public Func? errorHandler { get; init; } public ReceiverOptions? ReceiverOptions { get; init; } } \ No newline at end of file diff --git a/Telegram.Net/ServiceBindings.cs b/Telegram.Net/ServiceBindings.cs index 684733b..9a1d0a1 100644 --- a/Telegram.Net/ServiceBindings.cs +++ b/Telegram.Net/ServiceBindings.cs @@ -8,7 +8,7 @@ public static class ServiceBindings { public static IServiceCollection ConnectTelegram(this IServiceCollection isc, TelegramBotConfig config) { - isc.AddHostedService(k => new(config)); + isc.AddHostedService(k => new(config, isc)); return isc; } } \ No newline at end of file diff --git a/Telegram.Net/Services/TelegramHostedService.cs b/Telegram.Net/Services/TelegramHostedService.cs index d1afc91..3f05c4f 100644 --- a/Telegram.Net/Services/TelegramHostedService.cs +++ b/Telegram.Net/Services/TelegramHostedService.cs @@ -1,8 +1,11 @@ using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Payments; +using Telegram.Net.Attributes; using Telegram.Net.Interfaces; #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously @@ -10,52 +13,171 @@ namespace Telegram.Net.Services; public class TelegramHostedService : IHostedService { - private TelegramBotClient Client { get; } + private IServiceCollection isc { get; } + internal TelegramBotClient Client { get; set; } private ITelegramBotConfig Config { get; } - internal static Dictionary> CommandHandler { get; } = new(); - internal static List> EditedMessageHandler { get; } = new(); - internal static Dictionary> CallbackQueryHandler { get; } = new(); - internal static Dictionary> InlineHandler { get; } = new(); - internal static Func? PreCheckoutHandler { get; set; } - internal static List> DefaultUpdateHandler { get; } = new(); + internal Dictionary> CommandHandler { get; } = new(); + internal List> EditedMessageHandler { get; } = new(); + internal Dictionary> CallbackQueryHandler { get; } = new(); + internal Dictionary> InlineHandler { get; } = new(); + internal Func? PreCheckoutHandler { get; set; } + internal List> DefaultUpdateHandler { get; } = new(); - public TelegramHostedService(ITelegramBotConfig config) + public TelegramHostedService(ITelegramBotConfig config, IServiceCollection isc) { Client = new TelegramBotClient(config.Token); Config = config; + this.isc = isc; + } + internal static bool IsValidHandlerMethod(MethodInfo method, Type parameterType) + { + var parameters = method.GetParameters(); + return method.ReturnType == typeof(Task) && + parameters.Length == 3 && + parameters[0].ParameterType == typeof(ITelegramBotClient) && + parameters[1].ParameterType == parameterType && + parameters[2].ParameterType == typeof(CancellationToken); + } + internal static Func CreateDelegate(MethodInfo method) + { + var delegateType = typeof(Func); + return (Delegate.CreateDelegate(delegateType, null, method) as Func)!; + } + + internal async Task AddAttributes(CancellationToken cancellationToken) + { + await Task.Run(async () => + { + var implementations = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => a.GetTypes()) + .Where(t => typeof(IUpdatePollingService).IsAssignableFrom(t) && !t.IsInterface); + + foreach (var implementation in implementations) + { + isc.AddSingleton(implementation); + } + + var methods = implementations + .SelectMany(t => t.GetMethods( + BindingFlags.Instance | + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.DeclaredOnly)) + .Where(m => + m.GetCustomAttribute() != null || + m.GetCustomAttribute() != null || + m.GetCustomAttribute() != null || + m.GetCustomAttribute() != null || + m.GetCustomAttribute() != null || + m.GetCustomAttribute() != null); + + foreach (var method in methods) + { + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + var commandAttr = method.GetCustomAttribute(); + if (commandAttr != null) + { + if (IsValidHandlerMethod(method, typeof(Message))) + { + var handler = CreateDelegate(method); + CommandHandler.TryAdd(commandAttr.Command, handler); + } + continue; + } + + var callbackAttr = method.GetCustomAttribute(); + if (callbackAttr != null) + { + if (IsValidHandlerMethod(method, typeof(CallbackQuery))) + { + var handler = CreateDelegate(method); + CallbackQueryHandler.TryAdd(callbackAttr.QueryId, handler); + } + continue; + } + + var editMessageAttr = method.GetCustomAttribute(); + if (editMessageAttr != null) + { + if (IsValidHandlerMethod(method, typeof(Message))) + { + var handler = CreateDelegate(method); + EditedMessageHandler.Add(handler); + } + continue; + } + + var inlineAttr = method.GetCustomAttribute(); + if (inlineAttr != null) + { + if (IsValidHandlerMethod(method, typeof(InlineQuery))) + { + var handler = CreateDelegate(method); + InlineHandler.TryAdd(inlineAttr.InlineId, handler); + } + continue; + } + + var preCheckoutAttr = method.GetCustomAttribute(); + if (preCheckoutAttr != null) + { + if (IsValidHandlerMethod(method, typeof(PreCheckoutQuery))) + { + var handler = CreateDelegate(method); + PreCheckoutHandler = handler; + } + continue; + } + + var updateAttr = method.GetCustomAttribute(); + if (updateAttr != null) + { + if (IsValidHandlerMethod(method, typeof(Update))) + { + var handler = CreateDelegate(method); + DefaultUpdateHandler.Add(handler); + } + continue; + } + } + }, cancellationToken); + } + + + internal async Task UpdateHandler(ITelegramBotClient client, Update update, CancellationToken ctx) + { + switch (update) + { + case { Message: { } message }: + await CommandHandler.FirstOrDefault(k => message.Text!.StartsWith(k.Key)).Value(client, message, ctx); + break; + case { EditedMessage: { } message }: + EditedMessageHandler.ForEach(async k => await k(client, message, ctx)); + break; + case { CallbackQuery: { } callbackQuery }: + await CallbackQueryHandler.FirstOrDefault(k => callbackQuery.Data!.StartsWith(k.Key)).Value(client, callbackQuery, ctx); + break; + case { InlineQuery: { } inlineQuery }: + await InlineHandler.FirstOrDefault(k => inlineQuery.Id.StartsWith(k.Key)).Value(client, inlineQuery, ctx); + break; + case { PreCheckoutQuery: { } preCheckoutQuery }: + if (PreCheckoutHandler != null) await PreCheckoutHandler(client, preCheckoutQuery, ctx); + break; + default: + DefaultUpdateHandler.ForEach(async k => await k(client, update, ctx)); + break; + } } [SuppressMessage("ReSharper", "AsyncVoidLambda")] public async Task StartAsync(CancellationToken cancellationToken) { + await AddAttributes(cancellationToken); + + + Client.StartReceiving( - async (client, update, ctx) => - { - switch (update) - { - case { Message: { } message }: - await CommandHandler.FirstOrDefault(k => message.Text!.StartsWith(k.Key)).Value(client, message, cancellationToken); - break; - case { EditedMessage: { } message }: - EditedMessageHandler.ForEach(async k => await k(client, message, cancellationToken)); - break; - case { CallbackQuery: { } callbackQuery }: - await CallbackQueryHandler.FirstOrDefault(k => callbackQuery.Data!.StartsWith(k.Key)).Value(client, callbackQuery, cancellationToken); - break; - case { InlineQuery: { } inlineQuery }: - await InlineHandler.FirstOrDefault(k => inlineQuery.Id.StartsWith(k.Key)).Value(client, inlineQuery, cancellationToken); - break; - case {PreCheckoutQuery: { } preCheckoutQuery}: - if (PreCheckoutHandler != null) - await PreCheckoutHandler(client, preCheckoutQuery, cancellationToken); - break; - default: - DefaultUpdateHandler.ForEach(async k => await k(client, update, ctx)); - break; - } - - }, + UpdateHandler, Config.errorHandler ?? ((_, _, _) => Task.CompletedTask), Config.ReceiverOptions, cancellationToken); diff --git a/Telegram.Net/Telegram.Net.csproj b/Telegram.Net/Telegram.Net.csproj index dd6a97e..f612b35 100644 --- a/Telegram.Net/Telegram.Net.csproj +++ b/Telegram.Net/Telegram.Net.csproj @@ -1,9 +1,18 @@ + true net7.0 enable enable + 1.0.0 + yawaflua + Telegram.Net + Telegram.Bots extender pack, what provides to user attributes, which can used with services + Dmitrii Shimanskii + https://github.com/yawaflua/telegram.Net + https://github.com/yawaflua/telegram.Net + telegram diff --git a/global.json b/global.json new file mode 100644 index 0000000..aaac9e0 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/tests/Telegram.Tests/GlobalUsings.cs b/tests/Telegram.Tests/GlobalUsings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/tests/Telegram.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/tests/Telegram.Tests/Telegram.Tests.csproj b/tests/Telegram.Tests/Telegram.Tests.csproj new file mode 100644 index 0000000..0b9b510 --- /dev/null +++ b/tests/Telegram.Tests/Telegram.Tests.csproj @@ -0,0 +1,33 @@ + + + + net7.0 + enable + enable + true + false + true + + + + + + + + + + + + + + + + + + + Always + + + + + diff --git a/tests/Telegram.Tests/TestTelegramHostedService.cs b/tests/Telegram.Tests/TestTelegramHostedService.cs new file mode 100644 index 0000000..656485f --- /dev/null +++ b/tests/Telegram.Tests/TestTelegramHostedService.cs @@ -0,0 +1,175 @@ +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; +using Telegram.Bot.Types.InlineQueryResults; +using Telegram.Bot.Types.Payments; +using Telegram.Net.Attributes; +using Telegram.Net.Interfaces; +using Telegram.Net.Services; + +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Telegram.Bot.Types.Enums; +using Telegram.Bot.Types.ReplyMarkups; +using Telegram.Net.Models; + +namespace Telegram.Tests +{ + [TestFixture] + public class TelegramHostedServiceTests + { + private TelegramBotConfig _configMock; + private ServiceCollection _services; + + [SetUp] + public void Setup() + { + var conf = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", false) + .AddEnvironmentVariables() + .Build(); + + _configMock = new TelegramBotConfig(conf.GetValue("telegram_test_token") ?? throw new Exception("Provide telegram token first")); + _services = new ServiceCollection(); + } + + public class TestHandler : IUpdatePollingService + { + [Command("/start")] + public async Task HandleStart(ITelegramBotClient client, Message message, CancellationToken ct) + => await client.SendTextMessageAsync(message.Chat.Id, "Started", cancellationToken: ct); + + [Callback("test_callback")] + public async Task HandleCallback(ITelegramBotClient client, CallbackQuery query, CancellationToken ct) + => await client.AnswerCallbackQueryAsync(query.Id, "Callback handled", cancellationToken: ct); + + [EditMessage] + public async Task HandleEdit(ITelegramBotClient client, Message message, CancellationToken ct) + => await client.EditMessageTextAsync(message.Chat.Id, message.MessageId, "Edited", cancellationToken: ct); + + [Inline("search")] + public async Task HandleInline(ITelegramBotClient client, InlineQuery query, CancellationToken ct) + => await client.AnswerInlineQueryAsync(query.Id, new[] { new InlineQueryResultArticle("1", "Test", new InputTextMessageContent("Test")) }, cancellationToken: ct); + + [PreCheckout] + public async Task HandlePayment(ITelegramBotClient client, PreCheckoutQuery query, CancellationToken ct) + => await client.AnswerPreCheckoutQueryAsync(query.Id, cancellationToken: ct); + + [Update] + public async Task HandleUpdate(ITelegramBotClient client, Update update, CancellationToken ct) + => await client.SendTextMessageAsync(123, "Update handled", cancellationToken: ct); + } + + [Test] + public void AddAttributes_RegistersCommandHandlersCorrectly() + { + _services.AddSingleton(); + var service = new TelegramHostedService(_configMock, _services); + + service.AddAttributes(CancellationToken.None).Wait(); + + Assert.Multiple(() => + { + Assert.That(service.CommandHandler, Contains.Key("/start")); + Assert.That(service.CallbackQueryHandler, Contains.Key("test_callback")); + Assert.That(service.EditedMessageHandler, Has.Count.EqualTo(1)); + Assert.That(service.InlineHandler, Contains.Key("search")); + Assert.That(service.PreCheckoutHandler, Is.Not.Null); + Assert.That(service.DefaultUpdateHandler, Has.Count.EqualTo(1)); + }); + } + + [Test] + public void IsValidHandlerMethod_ValidMessageHandler_ReturnsTrue() + { + // Arrange + var method = typeof(TestHandler).GetMethod("HandleStart"); + + // Act + var result = TelegramHostedService.IsValidHandlerMethod(method, typeof(Message)); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void IsValidHandlerMethod_InvalidParameters_ReturnsFalse() + { + // Arrange + var method = typeof(TestHandler).GetMethod("HandleStart"); + + // Act + var result = TelegramHostedService.IsValidHandlerMethod(method, typeof(CallbackQuery)); + + // Assert + Assert.That(result, Is.False); + } + } + + [TestFixture] + public class IntegrationTests + { + private Mock _botClientMock; + private TelegramHostedService _hostedService; + private Mock _configMock; + + [SetUp] + public void Setup() + { + var conf = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", false) + .AddEnvironmentVariables() + .Build(); + + _configMock = new Mock(); + _configMock.Setup(c => c.Token).Returns(conf.GetValue("telegram_test_token") ?? throw new Exception("Provide token first")); + _configMock.Setup(c => c.ReceiverOptions).Returns(new ReceiverOptions()); + + var services = new ServiceCollection(); + services.AddSingleton(); + _botClientMock = new Mock(); + + _hostedService = new TelegramHostedService(_configMock.Object, services); + _hostedService.AddAttributes(CancellationToken.None).Wait(); + } + + [Test] + public async Task HandleMessage_ValidCommand_ExecutesHandler() + { + // Arrange + var message = new Message { Text = "/start", Chat = new Chat { Id = 123 } }; + var update = new Update { Message = message }; + + // Act + await _hostedService.StartAsync(CancellationToken.None); + await InvokeUpdateHandler(update); + + // Assert + _botClientMock.Verify(); + } + + [Test] + public async Task HandleCallback_ValidQuery_ExecutesHandler() + { + // Arrange + var callback = new CallbackQuery { Data = "test_callback", Id = "cb_id" }; + var update = new Update { CallbackQuery = callback }; + + // Act + await _hostedService.StartAsync(CancellationToken.None); + await InvokeUpdateHandler(update); + + // Assert + _botClientMock.Verify(); + } + + private async Task InvokeUpdateHandler(Update update) + { + await _hostedService.UpdateHandler(_botClientMock.Object, update, CancellationToken.None); + } + } +} \ No newline at end of file diff --git a/tests/Telegram.Tests/appsettings.json b/tests/Telegram.Tests/appsettings.json new file mode 100644 index 0000000..50a43b4 --- /dev/null +++ b/tests/Telegram.Tests/appsettings.json @@ -0,0 +1,3 @@ +{ + "_telegram_test_token": "PROVIDE_YOUR_TOKEN" +}