using System.Security.Cryptography; using System.Text; using GmRelay.Shared.Telegram; using GmRelay.Web.Services; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Primitives; namespace GmRelay.Bot.Tests.Web; public sealed class TelegramAuthPayloadBuilderTests { [Fact] public void BuildLoginWidget_ShouldGeneratePayloadAcceptedByAuthService() { const string botToken = "test-bot-token"; var result = TelegramAuthPayloadBuilder.BuildLoginWidget( botToken, 424242L, "Ada", "Lovelace", "ada"); var query = ParseQueryString(result.QueryString); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.Verify(query, out var telegramId, out var name); Assert.True(verified); Assert.Equal(424242L, telegramId); Assert.Equal("Ada Lovelace", name); } [Fact] public void BuildLoginWidget_ShouldBeRejectedWhenTampered() { const string botToken = "test-bot-token"; var result = TelegramAuthPayloadBuilder.BuildLoginWidget(botToken, 424242L, "Ada"); var tamperedQuery = ParseQueryString(result.QueryString.Replace("hash=", "hash=00", StringComparison.Ordinal)); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.Verify(tamperedQuery, out _, out _); Assert.False(verified); } [Fact] public void BuildLoginWidget_ShouldBeRejectedWhenExpired() { const string botToken = "test-bot-token"; var expiredAuthDate = DateTimeOffset.UtcNow.AddDays(-2).ToUnixTimeSeconds(); var result = TelegramAuthPayloadBuilder.BuildLoginWidget( botToken, 424242L, "Ada", authDate: expiredAuthDate); var query = ParseQueryString(result.QueryString); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.Verify(query, out _, out _); Assert.False(verified); } [Fact] public void BuildMiniAppInitData_ShouldGeneratePayloadAcceptedByAuthService() { const string botToken = "test-bot-token"; var result = TelegramAuthPayloadBuilder.BuildMiniAppInitData( botToken, 424242L, "Ada", "Lovelace", "ada"); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.VerifyWebAppInitData(result.InitDataRaw, out var telegramId, out var name); Assert.True(verified); Assert.Equal(424242L, telegramId); Assert.Equal("Ada Lovelace", name); } [Fact] public void BuildMiniAppInitData_ShouldBeRejectedWhenTampered() { const string botToken = "test-bot-token"; var result = TelegramAuthPayloadBuilder.BuildMiniAppInitData(botToken, 424242L, "Ada"); var tamperedInitData = result.InitDataRaw.Replace("hash=", "hash=00", StringComparison.Ordinal); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.VerifyWebAppInitData(tamperedInitData, out _, out _); Assert.False(verified); } [Fact] public void BuildMiniAppInitData_ShouldBeRejectedWhenExpired() { const string botToken = "test-bot-token"; var expiredAuthDate = DateTimeOffset.UtcNow.AddDays(-2).ToUnixTimeSeconds(); var result = TelegramAuthPayloadBuilder.BuildMiniAppInitData( botToken, 424242L, "Ada", authDate: expiredAuthDate); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.VerifyWebAppInitData(result.InitDataRaw, out _, out _); Assert.False(verified); } [Fact] public void BuildMiniAppInitData_ShouldIncludeOptionalFields() { const string botToken = "test-bot-token"; var result = TelegramAuthPayloadBuilder.BuildMiniAppInitData( botToken, 424242L, "Ada", "Lovelace", "ada", photoUrl: "https://t.me/i/userpic/320/ada.jpg", languageCode: "en", isPremium: true, chatId: -1001234567890L, chatType: "supergroup", chatTitle: "Test Club", queryId: "AAHdF6IQAAAAAN0XohDhrOrc", startParam: "ref123"); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.VerifyWebAppInitData(result.InitDataRaw, out var telegramId, out var name); Assert.True(verified); Assert.Equal(424242L, telegramId); Assert.Equal("Ada Lovelace", name); Assert.Contains("start_param=ref123", result.InitDataRaw, StringComparison.Ordinal); Assert.Contains("query_id=", result.InitDataRaw, StringComparison.Ordinal); Assert.Contains("chat=%7B%22id%22%3A-1001234567890", result.InitDataRaw, StringComparison.Ordinal); } [Fact] public void ComputeLoginWidgetHash_ShouldMatchTelegramAuthServiceExpectations() { const string botToken = "test-bot-token"; var values = new Dictionary { ["auth_date"] = "1714300000", ["first_name"] = "Ada", ["id"] = "424242" }; var hash = TelegramAuthPayloadBuilder.ComputeLoginWidgetHash(botToken, values); var recomputed = ComputeLegacyTelegramHash(botToken, values); Assert.Equal(recomputed, hash); } [Fact] public void ComputeMiniAppHash_ShouldMatchTelegramAuthServiceExpectations() { const string botToken = "test-bot-token"; var values = new Dictionary { ["auth_date"] = "1714300000", ["user"] = """{"id":424242,"first_name":"Ada"}""" }; var hash = TelegramAuthPayloadBuilder.ComputeMiniAppHash(botToken, values); var recomputed = ComputeLegacyWebAppHash(botToken, values); Assert.Equal(recomputed, hash); } private static IConfiguration CreateConfiguration(string botToken) => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Telegram:BotToken"] = botToken }) .Build(); private static QueryCollection ParseQueryString(string queryString) { var parsed = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString); return new QueryCollection(parsed.ToDictionary( pair => pair.Key, pair => pair.Value)); } // Legacy inline computation kept to prove the builder matches the original algorithm. private static string ComputeLegacyTelegramHash(string botToken, IReadOnlyDictionary values) { var dataCheckString = string.Join( "\n", values .OrderBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => $"{pair.Key}={pair.Value}")); var secretKey = SHA256.HashData(Encoding.UTF8.GetBytes(botToken)); var hashBytes = HMACSHA256.HashData(secretKey, Encoding.UTF8.GetBytes(dataCheckString)); return Convert.ToHexString(hashBytes).ToLowerInvariant(); } private static string ComputeLegacyWebAppHash(string botToken, IReadOnlyDictionary values) { var dataCheckString = string.Join( "\n", values .OrderBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => $"{pair.Key}={pair.Value}")); var secretKey = HMACSHA256.HashData(Encoding.UTF8.GetBytes("WebAppData"), Encoding.UTF8.GetBytes(botToken)); var hashBytes = HMACSHA256.HashData(secretKey, Encoding.UTF8.GetBytes(dataCheckString)); return Convert.ToHexString(hashBytes).ToLowerInvariant(); } }