diff --git a/src/SanctionsSearch.Worker.Tests/Unit/OfacFileServiceOptionsTests.cs b/src/SanctionsSearch.Worker.Tests/Unit/OfacFileServiceOptionsTests.cs index d4243a0..c0aa0e7 100644 --- a/src/SanctionsSearch.Worker.Tests/Unit/OfacFileServiceOptionsTests.cs +++ b/src/SanctionsSearch.Worker.Tests/Unit/OfacFileServiceOptionsTests.cs @@ -2,4 +2,115 @@ namespace SanctionsSearch.Worker.Tests.Unit; public class OfacFileServiceOptionsTests { + [Fact] + public void GetSdnFileUri_WhenCalled_ReturnsUri() + { + var options = new OfacFileServiceOptions + { + Url = "https://example.com", + SdnFileName = "sdn.csv" + }; + + var uri = options.GetSdnFileUri(); + + uri.Should().Be(new Uri("https://example.com/sdn.csv")); + } + + [Fact] + public void GetAddressFileUri_WhenCalled_ReturnsUri() + { + var options = new OfacFileServiceOptions + { + Url = "https://example.com", + AddressFileName = "address.csv" + }; + + var uri = options.GetAddressFileUri(); + + uri.Should().Be(new Uri("https://example.com/address.csv")); + } + + [Fact] + public void GetAltNamesFileUri_WhenCalled_ReturnsUri() + { + var options = new OfacFileServiceOptions + { + Url = "https://example.com", + AltNamesFileName = "alt_names.csv" + }; + + var uri = options.GetAltNamesFileUri(); + + uri.Should().Be(new Uri("https://example.com/alt_names.csv")); + } + + [Fact] + public void GetCommentsFileUri_WhenCalled_ReturnsUri() + { + var options = new OfacFileServiceOptions + { + Url = "https://example.com", + CommentsFileName = "comments.csv" + }; + + var uri = options.GetCommentsFileUri(); + + uri.Should().Be(new Uri("https://example.com/comments.csv")); + } + + [Fact] + public void GetConPrimaryNameFileUri_WhenCalled_ReturnsUri() + { + var options = new OfacFileServiceOptions + { + Url = "https://example.com", + ConPrimaryNameFileName = "con_primary_name.csv" + }; + + var uri = options.GetConPrimaryNameFileUri(); + + uri.Should().Be(new Uri("https://example.com/con_primary_name.csv")); + } + + [Fact] + public void GetConAddressesFileUri_WhenCalled_ReturnsUri() + { + var options = new OfacFileServiceOptions + { + Url = "https://example.com", + ConAddressesFileName = "con_addresses.csv" + }; + + var uri = options.GetConAddressesFileUri(); + + uri.Should().Be(new Uri("https://example.com/con_addresses.csv")); + } + + [Fact] + public void GetConAltNamesFileUri_WhenCalled_ReturnsUri() + { + var options = new OfacFileServiceOptions + { + Url = "https://example.com", + ConAltNamesFileName = "con_alt_names.csv" + }; + + var uri = options.GetConAltNamesFileUri(); + + uri.Should().Be(new Uri("https://example.com/con_alt_names.csv")); + } + + [Fact] + public void GetConCommentsFileUri_WhenCalled_ReturnsUri() + { + var options = new OfacFileServiceOptions + { + Url = "https://example.com", + ConCommentsFileName = "con_comments.csv" + }; + + var uri = options.GetConCommentsFileUri(); + + uri.Should().Be(new Uri("https://example.com/con_comments.csv")); + } } \ No newline at end of file diff --git a/src/SanctionsSearch.Worker.Tests/Unit/OfacFileServiceTests.cs b/src/SanctionsSearch.Worker.Tests/Unit/OfacFileServiceTests.cs index 79f1999..f77469f 100644 --- a/src/SanctionsSearch.Worker.Tests/Unit/OfacFileServiceTests.cs +++ b/src/SanctionsSearch.Worker.Tests/Unit/OfacFileServiceTests.cs @@ -1,10 +1,47 @@ +using System.Net; +using System.Text; + namespace SanctionsSearch.Worker.Tests.Unit; public class OfacFileServiceTests { + private const string Url = "https://example.com"; + private const string TestCsv = "Name,Address\nJohn Doe,123 Main St\n"; + private static Stream TestStream => new MemoryStream(Encoding.UTF8.GetBytes(TestCsv)); private readonly MockHttpMessageHandler _mockHttp = new(); private readonly Mock> _mockLogger = new(); private readonly Mock> _mockOptions = new(); + private readonly OfacFileService _service; + + public OfacFileServiceTests() + { + _mockOptions + .Setup(x => x.Value) + .Returns(new OfacFileServiceOptions + { + Url = Url, + SdnFileName = "sdn.csv", + AddressFileName = "address.csv", + AltNamesFileName = "alt_names.csv", + CommentsFileName = "comments.csv", + ConPrimaryNameFileName = "con_primary_name.csv", + ConAddressesFileName = "con_addresses.csv", + ConAltNamesFileName = "con_alt_names.csv", + ConCommentsFileName = "con_comments.csv" + }); + + _service = new OfacFileService( + _mockHttp.ToHttpClient(), + _mockLogger.Object, + _mockOptions.Object + ); + } + + private static string GetStreamContent(Stream stream) + { + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } [Fact] public void Constructor_WhenCalledWithNullClient_ThrowsArgumentNullException() @@ -55,4 +92,216 @@ public class OfacFileServiceTests act.Should().Throw(); } + + [Fact] + public async Task GetSdnFileAsync_WhenCalled_ItShouldReturnStream() + { + _mockHttp + .When("https://example.com/sdn.csv") + .Respond("text/csv", TestStream); + + var result = await _service.GetSdnFileAsync(); + + result.IsSuccess.Should().BeTrue(); + GetStreamContent(result.Value).Should().Be(TestCsv); + } + + [Fact] + public async Task GetSdnFileAsync_WhenRequestFails_ItShouldReturnFailure() + { + _mockHttp + .When("https://example.com/sdn.csv") + .Respond(HttpStatusCode.InternalServerError); + + var result = await _service.GetSdnFileAsync(); + + result.IsFailed.Should().BeTrue(); + } + + [Fact] + public async Task GetSdnFileAsync_WhenExceptionThrown_ItShouldReturnFailure() + { + _mockHttp + .When("https://example.com/sdn.csv") + .Throw(new Exception("Test exception")); + + var result = await _service.GetSdnFileAsync(); + + result.IsFailed.Should().BeTrue(); + } + + [Fact] + public async Task GetAddressFileAsync_WhenCalled_ItShouldReturnStream() + { + _mockHttp + .When("https://example.com/address.csv") + .Respond("text/csv", TestStream); + + var result = await _service.GetAddressFileAsync(); + + result.IsSuccess.Should().BeTrue(); + GetStreamContent(result.Value).Should().Be(TestCsv); + } + + [Fact] + public async Task GetAddressFileAsync_WhenRequestFails_ItShouldReturnFailure() + { + _mockHttp + .When("https://example.com/address.csv") + .Respond(HttpStatusCode.InternalServerError); + + var result = await _service.GetAddressFileAsync(); + + result.IsFailed.Should().BeTrue(); + } + + [Fact] + public async Task GetAltNamesFileAsync_WhenCalled_ItShouldReturnStream() + { + _mockHttp + .When("https://example.com/alt_names.csv") + .Respond("text/csv", TestStream); + + var result = await _service.GetAltNamesFileAsync(); + + result.IsSuccess.Should().BeTrue(); + GetStreamContent(result.Value).Should().Be(TestCsv); + } + + [Fact] + public async Task GetAltNamesFileAsync_WhenRequestFails_ItShouldReturnFailure() + { + _mockHttp + .When("https://example.com/alt_names.csv") + .Respond(HttpStatusCode.InternalServerError); + + var result = await _service.GetAltNamesFileAsync(); + + result.IsFailed.Should().BeTrue(); + } + + [Fact] + public async Task GetCommentsFileAsync_WhenCalled_ItShouldReturnStream() + { + _mockHttp + .When("https://example.com/comments.csv") + .Respond("text/csv", TestStream); + + var result = await _service.GetCommentsFileAsync(); + + result.IsSuccess.Should().BeTrue(); + GetStreamContent(result.Value).Should().Be(TestCsv); + } + + [Fact] + public async Task GetCommentsFileAsync_WhenRequestFails_ItShouldReturnFailure() + { + _mockHttp + .When("https://example.com/comments.csv") + .Respond(HttpStatusCode.InternalServerError); + + var result = await _service.GetCommentsFileAsync(); + + result.IsFailed.Should().BeTrue(); + } + + [Fact] + public async Task GetConPrimaryNameFileAsync_WhenCalled_ItShouldReturnStream() + { + _mockHttp + .When("https://example.com/con_primary_name.csv") + .Respond("text/csv", TestStream); + + var result = await _service.GetConPrimaryNameFileAsync(); + + result.IsSuccess.Should().BeTrue(); + GetStreamContent(result.Value).Should().Be(TestCsv); + } + + [Fact] + public async Task GetConPrimaryNameFileAsync_WhenRequestFails_ItShouldReturnFailure() + { + _mockHttp + .When("https://example.com/con_primary_name.csv") + .Respond(HttpStatusCode.InternalServerError); + + var result = await _service.GetConPrimaryNameFileAsync(); + + result.IsFailed.Should().BeTrue(); + } + + [Fact] + public async Task GetConAddressesFileAsync_WhenCalled_ItShouldReturnStream() + { + _mockHttp + .When("https://example.com/con_addresses.csv") + .Respond("text/csv", TestStream); + + var result = await _service.GetConAddressesFileAsync(); + + result.IsSuccess.Should().BeTrue(); + GetStreamContent(result.Value).Should().Be(TestCsv); + } + + [Fact] + public async Task GetConAddressesFileAsync_WhenRequestFails_ItShouldReturnFailure() + { + _mockHttp + .When("https://example.com/con_addresses.csv") + .Respond(HttpStatusCode.InternalServerError); + + var result = await _service.GetConAddressesFileAsync(); + + result.IsFailed.Should().BeTrue(); + } + + [Fact] + public async Task GetConAltNamesFileAsync_WhenCalled_ItShouldReturnStream() + { + _mockHttp + .When("https://example.com/con_alt_names.csv") + .Respond("text/csv", TestStream); + + var result = await _service.GetConAltNamesFileAsync(); + + result.IsSuccess.Should().BeTrue(); + GetStreamContent(result.Value).Should().Be(TestCsv); + } + + [Fact] + public async Task GetConAltNamesFileAsync_WhenRequestFails_ItShouldReturnFailure() + { + _mockHttp + .When("https://example.com/con_alt_names.csv") + .Respond(HttpStatusCode.InternalServerError); + + var result = await _service.GetConAltNamesFileAsync(); + + result.IsFailed.Should().BeTrue(); + } + + [Fact] + public async Task GetConCommentsFileAsync_WhenCalled_ItShouldReturnStream() + { + _mockHttp + .When("https://example.com/con_comments.csv") + .Respond("text/csv", TestStream); + + var result = await _service.GetConCommentsFileAsync(); + + result.IsSuccess.Should().BeTrue(); + GetStreamContent(result.Value).Should().Be(TestCsv); + } + + [Fact] + public async Task GetConCommentsFileAsync_WhenRequestFails_ItShouldReturnFailure() + { + _mockHttp + .When("https://example.com/con_comments.csv") + .Respond(HttpStatusCode.InternalServerError); + + var result = await _service.GetConCommentsFileAsync(); + + result.IsFailed.Should().BeTrue(); + } } \ No newline at end of file diff --git a/src/SanctionsSearch.Worker/Interfaces/IOfacFileService.cs b/src/SanctionsSearch.Worker/Interfaces/IOfacFileService.cs index 9446419..e3a4948 100644 --- a/src/SanctionsSearch.Worker/Interfaces/IOfacFileService.cs +++ b/src/SanctionsSearch.Worker/Interfaces/IOfacFileService.cs @@ -1,5 +1,15 @@ +using FluentResults; + namespace SanctionsSearch.Worker.Interfaces; interface IOfacFileService { + Task> GetSdnFileAsync(); + Task> GetAddressFileAsync(); + Task> GetAltNamesFileAsync(); + Task> GetCommentsFileAsync(); + Task> GetConPrimaryNameFileAsync(); + Task> GetConAddressesFileAsync(); + Task> GetConAltNamesFileAsync(); + Task> GetConCommentsFileAsync(); } diff --git a/src/SanctionsSearch.Worker/Services/OfacFileService.cs b/src/SanctionsSearch.Worker/Services/OfacFileService.cs index 3e79557..aaccd83 100644 --- a/src/SanctionsSearch.Worker/Services/OfacFileService.cs +++ b/src/SanctionsSearch.Worker/Services/OfacFileService.cs @@ -1,3 +1,6 @@ + +using FluentResults; + namespace SanctionsSearch.Worker.Services; class OfacFileService( @@ -9,4 +12,37 @@ class OfacFileService( private readonly HttpClient _client = client ?? throw new ArgumentNullException(nameof(client)); private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly OfacFileServiceOptions _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + private async Task> GetFileAsync(Uri fileUri) + { + try + { + _logger.LogInformation("Downloading file from {FileUri}", fileUri); + + var response = await _client.GetAsync(fileUri); + + if (response.IsSuccessStatusCode is false) + { + _logger.LogError("Failed to download file from {FileUri}", fileUri); + return Result.Fail($"Failed to download file from {fileUri}"); + } + + _logger.LogInformation("Downloaded file from {FileUri}", fileUri); + var stream = await response.Content.ReadAsStreamAsync(); + return Result.Ok(stream); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to download file from {FileUri}", fileUri); + return Result.Fail($"Failed to download file from {fileUri}"); + } + } + + public Task> GetSdnFileAsync() => GetFileAsync(_options.GetSdnFileUri()); + public Task> GetAddressFileAsync() => GetFileAsync(_options.GetAddressFileUri()); + public Task> GetAltNamesFileAsync() => GetFileAsync(_options.GetAltNamesFileUri()); + public Task> GetCommentsFileAsync() => GetFileAsync(_options.GetCommentsFileUri()); + public Task> GetConPrimaryNameFileAsync() => GetFileAsync(_options.GetConPrimaryNameFileUri()); + public Task> GetConAddressesFileAsync() => GetFileAsync(_options.GetConAddressesFileUri()); + public Task> GetConAltNamesFileAsync() => GetFileAsync(_options.GetConAltNamesFileUri()); + public Task> GetConCommentsFileAsync() => GetFileAsync(_options.GetConCommentsFileUri()); } diff --git a/src/SanctionsSearch.Worker/appsettings.Example.json b/src/SanctionsSearch.Worker/appsettings.Example.json index b2dcdb6..3ddce01 100644 --- a/src/SanctionsSearch.Worker/appsettings.Example.json +++ b/src/SanctionsSearch.Worker/appsettings.Example.json @@ -4,5 +4,16 @@ "Default": "Information", "Microsoft.Hosting.Lifetime": "Information" } + }, + "OfacFileServiceOptions": { + "Url": "https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports", + "SdnFileName": "SDN.CSV", + "AddressFileName": "ADD.CSV", + "AltNamesFileName": "ALT.CSV", + "CommentsFileName": "SDN_COMMENTS.CSV", + "ConPrimaryNameFileName": "CONS_PRIM.CSV", + "ConAddressesFileName": "CONS_ADD.CSV", + "ConAltNamesFileName": "CONS_ALT.CSV", + "ConCommentsFileName": "CONS_COMMENTS.CSV" } -} +} \ No newline at end of file