using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FrameworkBLL.Promotion;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Moq.Protected;
using Xunit;
namespace FrameworkTests;
///
/// PromotionExchangeClient is the only thing in FrameworkBLL that talks to the separate
/// PromotionExchange process over plain HTTP (no shared project reference between the two
/// solutions) — these tests verify the X-Api-Key header is always attached and that every method
/// degrades to null/empty rather than throwing on a transport failure, matching
/// JobEngineScheduleClient's established "non-fatal by design" convention for this class of call.
///
public class PromotionExchangeClientTests
{
private static IConfiguration Config() => new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary
{
["PromotionExchange:BaseUrl"] = "http://exchange.test",
["PromotionExchange:ApiKey"] = "my-secret-key"
})
.Build();
private static (Mock Handler, IHttpClientFactory Factory) NewFactory(
Func respond)
{
var handler = new Mock();
handler.Protected()
.Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
.ReturnsAsync((HttpRequestMessage req, CancellationToken _) => respond(req));
var factory = new Mock();
factory.Setup(f => f.CreateClient("promotionexchange")).Returns(() => new HttpClient(handler.Object));
return (handler, factory.Object);
}
[Fact]
public async Task UploadAsync_AttachesApiKeyHeaderAndReturnsPackageId()
{
HttpRequestMessage? captured = null;
var (_, factory) = NewFactory(req =>
{
captured = req;
var json = JsonSerializer.Serialize(new { packageId = 99 });
return new HttpResponseMessage(HttpStatusCode.OK)
{ Content = new StringContent(json, Encoding.UTF8, "application/json") };
});
var client = new PromotionExchangeClient(factory, Config(), NullLogger.Instance);
var result = await client.UploadAsync(new byte[] { 1, 2, 3 }, "DEST1", CancellationToken.None);
Assert.Equal(99, result);
Assert.NotNull(captured);
Assert.True(captured!.Headers.TryGetValues("X-Api-Key", out var values));
Assert.Equal("my-secret-key", Assert.Single(values!));
Assert.Equal("http://exchange.test/PromotionExchange/UploadPackage", captured.RequestUri!.ToString());
}
[Fact]
public async Task UploadAsync_TransportFailure_ReturnsNullInsteadOfThrowing()
{
var (_, factory) = NewFactory(_ => new HttpResponseMessage(HttpStatusCode.InternalServerError));
var client = new PromotionExchangeClient(factory, Config(), NullLogger.Instance);
var result = await client.UploadAsync(new byte[] { 1 }, "DEST1", CancellationToken.None);
Assert.Null(result);
}
[Fact]
public async Task ListPendingAsync_ParsesListAndAttachesApiKeyHeader()
{
HttpRequestMessage? captured = null;
var (_, factory) = NewFactory(req =>
{
captured = req;
var json = JsonSerializer.Serialize(new[]
{
new { PackageId = 1, OriginEnvironmentCode = "ORIGIN1", PackageSizeBytes = 10, UploadedOn = DateTime.UtcNow }
});
return new HttpResponseMessage(HttpStatusCode.OK)
{ Content = new StringContent(json, Encoding.UTF8, "application/json") };
});
var client = new PromotionExchangeClient(factory, Config(), NullLogger.Instance);
var result = await client.ListPendingAsync(CancellationToken.None);
Assert.Single(result);
Assert.Equal(1, result[0].PackageId);
Assert.Equal("ORIGIN1", result[0].OriginEnvironmentCode);
Assert.True(captured!.Headers.TryGetValues("X-Api-Key", out var values));
Assert.Equal("my-secret-key", Assert.Single(values!));
}
[Fact]
public async Task ListPendingAsync_TransportFailure_ReturnsEmptyInsteadOfThrowing()
{
var (_, factory) = NewFactory(_ => throw new HttpRequestException("network down"));
var client = new PromotionExchangeClient(factory, Config(), NullLogger.Instance);
var result = await client.ListPendingAsync(CancellationToken.None);
Assert.Empty(result);
}
[Fact]
public async Task DownloadAsync_ReturnsBytesOnSuccess()
{
var expectedBytes = new byte[] { 5, 6, 7 };
var (_, factory) = NewFactory(_ => new HttpResponseMessage(HttpStatusCode.OK)
{ Content = new ByteArrayContent(expectedBytes) });
var client = new PromotionExchangeClient(factory, Config(), NullLogger.Instance);
var result = await client.DownloadAsync(7, CancellationToken.None);
Assert.Equal(expectedBytes, result);
}
[Fact]
public async Task DownloadAsync_NotFound_ReturnsNullInsteadOfThrowing()
{
var (_, factory) = NewFactory(_ => new HttpResponseMessage(HttpStatusCode.NotFound));
var client = new PromotionExchangeClient(factory, Config(), NullLogger.Instance);
var result = await client.DownloadAsync(7, CancellationToken.None);
Assert.Null(result);
}
}