Files
GmRelayBot/tests/GmRelay.Bot.Tests/Web/TelegramAuthPayloadBuilderTests.cs
T
Toutsu 5319592964 feat(e2e): shared initData / Login Widget payload builder for E2E tests
- Add TelegramAuthPayloadBuilder in GmRelay.Shared for C# tests.
- Refactor TelegramAuthServiceTests to use the shared builder.
- Add Python equivalent (telegram_init_data.py) for E2E runner.
- Add self-contained Python tests and E2E README.

Closes #144

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 11:53:22 +03:00

224 lines
7.9 KiB
C#

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<string, string>
{
["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<string, string>
{
["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<string, string?>
{
["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<string, string> 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<string, string> 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();
}
}