mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-02-06 03:52:04 -05:00
457 lines
15 KiB
C#
457 lines
15 KiB
C#
using System.Net;
|
|
using Cleanuparr.Infrastructure.Features.Notifications.Telegram;
|
|
using Cleanuparr.Shared.Helpers;
|
|
using Moq;
|
|
using Moq.Protected;
|
|
using Xunit;
|
|
|
|
namespace Cleanuparr.Infrastructure.Tests.Features.Notifications.Telegram;
|
|
|
|
public class TelegramProxyTests
|
|
{
|
|
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
|
|
private readonly Mock<HttpMessageHandler> _httpMessageHandlerMock;
|
|
|
|
public TelegramProxyTests()
|
|
{
|
|
_httpMessageHandlerMock = new Mock<HttpMessageHandler>();
|
|
_httpClientFactoryMock = new Mock<IHttpClientFactory>();
|
|
|
|
var httpClient = new HttpClient(_httpMessageHandlerMock.Object);
|
|
_httpClientFactoryMock
|
|
.Setup(f => f.CreateClient(Constants.HttpClientWithRetryName))
|
|
.Returns(httpClient);
|
|
}
|
|
|
|
private TelegramProxy CreateProxy()
|
|
{
|
|
return new TelegramProxy(_httpClientFactoryMock.Object);
|
|
}
|
|
|
|
private static TelegramPayload CreatePayload(string text = "Test message", string? photoUrl = null)
|
|
{
|
|
return new TelegramPayload
|
|
{
|
|
ChatId = "123456789",
|
|
Text = text,
|
|
PhotoUrl = photoUrl,
|
|
MessageThreadId = null,
|
|
DisableNotification = false
|
|
};
|
|
}
|
|
|
|
#region Constructor Tests
|
|
|
|
[Fact]
|
|
public void Constructor_WithValidFactory_CreatesInstance()
|
|
{
|
|
// Act
|
|
var proxy = CreateProxy();
|
|
|
|
// Assert
|
|
Assert.NotNull(proxy);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_CreatesHttpClientWithCorrectName()
|
|
{
|
|
// Act
|
|
_ = CreateProxy();
|
|
|
|
// Assert
|
|
_httpClientFactoryMock.Verify(f => f.CreateClient(Constants.HttpClientWithRetryName), Times.Once);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region SendNotification Success Tests
|
|
|
|
[Fact]
|
|
public async Task SendNotification_WhenSuccessful_CompletesWithoutException()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
SetupSuccessResponse();
|
|
|
|
// Act & Assert - Should not throw
|
|
await proxy.SendNotification(CreatePayload(), "test-bot-token");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_SendsPostRequest()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
HttpMethod? capturedMethod = null;
|
|
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.Callback<HttpRequestMessage, CancellationToken>((req, _) => capturedMethod = req.Method)
|
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
|
|
|
// Act
|
|
await proxy.SendNotification(CreatePayload(), "test-bot-token");
|
|
|
|
// Assert
|
|
Assert.Equal(HttpMethod.Post, capturedMethod);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_WithoutPhoto_UseSendMessageEndpoint()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
Uri? capturedUri = null;
|
|
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.Callback<HttpRequestMessage, CancellationToken>((req, _) => capturedUri = req.RequestUri)
|
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
|
|
|
// Act
|
|
await proxy.SendNotification(CreatePayload(), "my-bot-token");
|
|
|
|
// Assert
|
|
Assert.NotNull(capturedUri);
|
|
Assert.Contains("/botmy-bot-token/sendMessage", capturedUri.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_WithPhotoAndShortCaption_UsesSendPhotoEndpoint()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
Uri? capturedUri = null;
|
|
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.Callback<HttpRequestMessage, CancellationToken>((req, _) => capturedUri = req.RequestUri)
|
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
|
|
|
var payload = CreatePayload(text: "Short caption", photoUrl: "https://example.com/image.jpg");
|
|
|
|
// Act
|
|
await proxy.SendNotification(payload, "my-bot-token");
|
|
|
|
// Assert
|
|
Assert.NotNull(capturedUri);
|
|
Assert.Contains("/botmy-bot-token/sendPhoto", capturedUri.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_WithPhotoAndLongCaption_UsesSendMessageEndpoint()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
Uri? capturedUri = null;
|
|
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.Callback<HttpRequestMessage, CancellationToken>((req, _) => capturedUri = req.RequestUri)
|
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
|
|
|
// Caption longer than 1024 characters
|
|
var payload = CreatePayload(text: new string('A', 1025), photoUrl: "https://example.com/image.jpg");
|
|
|
|
// Act
|
|
await proxy.SendNotification(payload, "my-bot-token");
|
|
|
|
// Assert
|
|
Assert.NotNull(capturedUri);
|
|
Assert.Contains("/botmy-bot-token/sendMessage", capturedUri.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_SetsJsonContentType()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
string? capturedContentType = null;
|
|
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.Callback<HttpRequestMessage, CancellationToken>((req, _) =>
|
|
capturedContentType = req.Content?.Headers.ContentType?.MediaType)
|
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
|
|
|
// Act
|
|
await proxy.SendNotification(CreatePayload(), "test-bot-token");
|
|
|
|
// Assert
|
|
Assert.Equal("application/json", capturedContentType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_WithPhotoAndLongCaption_IncludesInvisibleImageLink()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
string? capturedContent = null;
|
|
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.Callback<HttpRequestMessage, CancellationToken>(async (req, _) =>
|
|
capturedContent = await req.Content!.ReadAsStringAsync())
|
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
|
|
|
// Caption longer than 1024 characters
|
|
var payload = CreatePayload(text: new string('A', 1025), photoUrl: "https://example.com/image.jpg");
|
|
|
|
// Act
|
|
await proxy.SendNotification(payload, "test-bot-token");
|
|
|
|
// Assert
|
|
Assert.NotNull(capturedContent);
|
|
Assert.Contains("​", capturedContent); // Zero-width space
|
|
Assert.Contains("example.com/image.jpg", capturedContent);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_WithoutPhoto_DisablesWebPagePreview()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
string? capturedContent = null;
|
|
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.Callback<HttpRequestMessage, CancellationToken>(async (req, _) =>
|
|
capturedContent = await req.Content!.ReadAsStringAsync())
|
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
|
|
|
// Act
|
|
await proxy.SendNotification(CreatePayload(), "test-bot-token");
|
|
|
|
// Assert
|
|
Assert.NotNull(capturedContent);
|
|
Assert.Contains("disable_web_page_preview", capturedContent);
|
|
Assert.Contains("true", capturedContent);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_WithMessageThreadId_IncludesThreadIdInBody()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
string? capturedContent = null;
|
|
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.Callback<HttpRequestMessage, CancellationToken>(async (req, _) =>
|
|
capturedContent = await req.Content!.ReadAsStringAsync())
|
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
|
|
|
var payload = new TelegramPayload
|
|
{
|
|
ChatId = "123456789",
|
|
Text = "Test message",
|
|
MessageThreadId = 42
|
|
};
|
|
|
|
// Act
|
|
await proxy.SendNotification(payload, "test-bot-token");
|
|
|
|
// Assert
|
|
Assert.NotNull(capturedContent);
|
|
Assert.Contains("message_thread_id", capturedContent);
|
|
Assert.Contains("42", capturedContent);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_WithDisableNotification_IncludesDisableNotificationInBody()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
string? capturedContent = null;
|
|
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.Callback<HttpRequestMessage, CancellationToken>(async (req, _) =>
|
|
capturedContent = await req.Content!.ReadAsStringAsync())
|
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
|
|
|
var payload = new TelegramPayload
|
|
{
|
|
ChatId = "123456789",
|
|
Text = "Test message",
|
|
DisableNotification = true
|
|
};
|
|
|
|
// Act
|
|
await proxy.SendNotification(payload, "test-bot-token");
|
|
|
|
// Assert
|
|
Assert.NotNull(capturedContent);
|
|
Assert.Contains("disable_notification", capturedContent);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region SendNotification Error Tests
|
|
|
|
[Fact]
|
|
public async Task SendNotification_When400_ThrowsTelegramExceptionWithRejectedMessage()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
SetupErrorResponse(HttpStatusCode.BadRequest, "Bad Request: chat not found");
|
|
|
|
// Act & Assert
|
|
var ex = await Assert.ThrowsAsync<TelegramException>(() =>
|
|
proxy.SendNotification(CreatePayload(), "test-bot-token"));
|
|
Assert.Contains("rejected the request", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_When401_ThrowsTelegramExceptionWithInvalidToken()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
SetupErrorResponse(HttpStatusCode.Unauthorized, "Unauthorized");
|
|
|
|
// Act & Assert
|
|
var ex = await Assert.ThrowsAsync<TelegramException>(() =>
|
|
proxy.SendNotification(CreatePayload(), "test-bot-token"));
|
|
Assert.Contains("bot token is invalid", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_When403_ThrowsTelegramExceptionWithPermissionDenied()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
SetupErrorResponse(HttpStatusCode.Forbidden, "Forbidden: bot was blocked by the user");
|
|
|
|
// Act & Assert
|
|
var ex = await Assert.ThrowsAsync<TelegramException>(() =>
|
|
proxy.SendNotification(CreatePayload(), "test-bot-token"));
|
|
Assert.Contains("permission", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_When429_ThrowsTelegramExceptionWithRateLimitMessage()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
SetupErrorResponse((HttpStatusCode)429, "Too Many Requests");
|
|
|
|
// Act & Assert
|
|
var ex = await Assert.ThrowsAsync<TelegramException>(() =>
|
|
proxy.SendNotification(CreatePayload(), "test-bot-token"));
|
|
Assert.Contains("Rate limited", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_WhenOtherHttpError_ThrowsTelegramExceptionWithStatusCode()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
SetupErrorResponse(HttpStatusCode.InternalServerError, "Internal Server Error");
|
|
|
|
// Act & Assert
|
|
var ex = await Assert.ThrowsAsync<TelegramException>(() =>
|
|
proxy.SendNotification(CreatePayload(), "test-bot-token"));
|
|
Assert.Contains("500", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_WhenNetworkError_ThrowsTelegramException()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.ThrowsAsync(new HttpRequestException("Network error"));
|
|
|
|
// Act & Assert
|
|
var ex = await Assert.ThrowsAsync<TelegramException>(() =>
|
|
proxy.SendNotification(CreatePayload(), "test-bot-token"));
|
|
Assert.Contains("Unable to reach Telegram API", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendNotification_WhenErrorResponseTruncatesLongBody()
|
|
{
|
|
// Arrange
|
|
var proxy = CreateProxy();
|
|
var longErrorBody = new string('X', 600);
|
|
SetupErrorResponse(HttpStatusCode.BadRequest, longErrorBody);
|
|
|
|
// Act & Assert
|
|
var ex = await Assert.ThrowsAsync<TelegramException>(() =>
|
|
proxy.SendNotification(CreatePayload(), "test-bot-token"));
|
|
Assert.True(ex.Message.Length < 600);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private void SetupSuccessResponse()
|
|
{
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
|
}
|
|
|
|
private void SetupErrorResponse(HttpStatusCode statusCode, string body = "")
|
|
{
|
|
_httpMessageHandlerMock
|
|
.Protected()
|
|
.Setup<Task<HttpResponseMessage>>(
|
|
"SendAsync",
|
|
ItExpr.IsAny<HttpRequestMessage>(),
|
|
ItExpr.IsAny<CancellationToken>())
|
|
.ReturnsAsync(new HttpResponseMessage(statusCode)
|
|
{
|
|
Content = new StringContent(body)
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
}
|