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); } }