diff --git a/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs b/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs index cda488e..58e357b 100644 --- a/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs +++ b/MalwareMultiScan.Api/Attributes/IsHttpUrlAttribute.cs @@ -6,14 +6,15 @@ namespace MalwareMultiScan.Api.Attributes /// /// Validate URI to be an absolute http(s) URL. /// - internal class IsHttpUrlAttribute : ValidationAttribute + public class IsHttpUrlAttribute : ValidationAttribute { /// protected override ValidationResult IsValid(object value, ValidationContext validationContext) { if (!Uri.TryCreate((string)value, UriKind.Absolute, out var uri) || !uri.IsAbsoluteUri - || uri.Scheme != "http" && uri.Scheme != "https") + || uri.Scheme.ToLowerInvariant() != "http" + && uri.Scheme.ToLowerInvariant() != "https") return new ValidationResult("Only absolute http(s) URLs are supported"); return ValidationResult.Success; diff --git a/MalwareMultiScan.Api/Controllers/DownloadController.cs b/MalwareMultiScan.Api/Controllers/DownloadController.cs index b2edfe3..212b357 100644 --- a/MalwareMultiScan.Api/Controllers/DownloadController.cs +++ b/MalwareMultiScan.Api/Controllers/DownloadController.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using MalwareMultiScan.Api.Services; +using MalwareMultiScan.Api.Services.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -10,9 +10,9 @@ namespace MalwareMultiScan.Api.Controllers [Produces("application/octet-stream")] public class DownloadController : Controller { - private readonly ScanResultService _scanResultService; + private readonly IScanResultService _scanResultService; - public DownloadController(ScanResultService scanResultService) + public DownloadController(IScanResultService scanResultService) { _scanResultService = scanResultService; } diff --git a/MalwareMultiScan.Api/Controllers/QueueController.cs b/MalwareMultiScan.Api/Controllers/QueueController.cs index 5122797..a68aa7c 100644 --- a/MalwareMultiScan.Api/Controllers/QueueController.cs +++ b/MalwareMultiScan.Api/Controllers/QueueController.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using MalwareMultiScan.Api.Attributes; using MalwareMultiScan.Api.Data.Models; -using MalwareMultiScan.Api.Services; +using MalwareMultiScan.Api.Services.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -13,9 +13,9 @@ namespace MalwareMultiScan.Api.Controllers [Produces("application/json")] public class QueueController : Controller { - private readonly ScanResultService _scanResultService; + private readonly IScanResultService _scanResultService; - public QueueController(ScanResultService scanResultService) + public QueueController(IScanResultService scanResultService) { _scanResultService = scanResultService; } @@ -31,10 +31,10 @@ namespace MalwareMultiScan.Api.Controllers string storedFileId; await using (var uploadFileStream = file.OpenReadStream()) - storedFileId = await _scanResultService.StoreFile(file.Name, uploadFileStream); + storedFileId = await _scanResultService.StoreFile(file.FileName, uploadFileStream); await _scanResultService.QueueUrlScan(result, Url.Action("Index", "Download", new {id = storedFileId}, - Request.Scheme, Request.Host.Value)); + Request?.Scheme, Request?.Host.Value)); return CreatedAtAction("Index", "ScanResults", new {id = result.Id}, result); } diff --git a/MalwareMultiScan.Api/Controllers/ScanResultsController.cs b/MalwareMultiScan.Api/Controllers/ScanResultsController.cs index b67e1cf..3752062 100644 --- a/MalwareMultiScan.Api/Controllers/ScanResultsController.cs +++ b/MalwareMultiScan.Api/Controllers/ScanResultsController.cs @@ -1,6 +1,8 @@ using System.Threading.Tasks; using MalwareMultiScan.Api.Data.Models; using MalwareMultiScan.Api.Services; +using MalwareMultiScan.Api.Services.Implementations; +using MalwareMultiScan.Api.Services.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -11,9 +13,9 @@ namespace MalwareMultiScan.Api.Controllers [Produces("application/json")] public class ScanResultsController : Controller { - private readonly ScanResultService _scanResultService; + private readonly IScanResultService _scanResultService; - public ScanResultsController(ScanResultService scanResultService) + public ScanResultsController(IScanResultService scanResultService) { _scanResultService = scanResultService; } diff --git a/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs b/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs index feb6a21..4e58e0c 100644 --- a/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs +++ b/MalwareMultiScan.Api/Extensions/ServiceCollectionExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Net; using EasyNetQ; using Microsoft.AspNetCore.Builder; @@ -5,30 +6,30 @@ using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; +using MongoDB.Driver.GridFS; namespace MalwareMultiScan.Api.Extensions { + [ExcludeFromCodeCoverage] internal static class ServiceCollectionExtensions { - public static void AddMongoDb(this IServiceCollection services, IConfiguration configuration) + internal static void AddMongoDb(this IServiceCollection services, IConfiguration configuration) { - services.AddSingleton( - serviceProvider => - { - var db = new MongoClient(configuration.GetConnectionString("Mongo")); + var client = new MongoClient(configuration.GetConnectionString("Mongo")); + var db = client.GetDatabase(configuration.GetValue("DatabaseName")); - return db.GetDatabase( - configuration.GetValue("DatabaseName")); - }); + services.AddSingleton(client); + services.AddSingleton(db); + services.AddSingleton(new GridFSBucket(db)); } - public static void AddRabbitMq(this IServiceCollection services, IConfiguration configuration) + internal static void AddRabbitMq(this IServiceCollection services, IConfiguration configuration) { services.AddSingleton(x => RabbitHutch.CreateBus(configuration.GetConnectionString("RabbitMQ"))); } - public static void AddDockerForwardedHeadersOptions(this IServiceCollection services) + internal static void AddDockerForwardedHeadersOptions(this IServiceCollection services) { services.Configure(options => { diff --git a/MalwareMultiScan.Api/Program.cs b/MalwareMultiScan.Api/Program.cs index acaf861..5211d45 100644 --- a/MalwareMultiScan.Api/Program.cs +++ b/MalwareMultiScan.Api/Program.cs @@ -1,8 +1,10 @@ +using EasyNetQ.LightInject; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; namespace MalwareMultiScan.Api { + [ExcludeFromCodeCoverage] public static class Program { public static void Main(string[] args) diff --git a/MalwareMultiScan.Api/Services/ReceiverHostedService.cs b/MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs similarity index 70% rename from MalwareMultiScan.Api/Services/ReceiverHostedService.cs rename to MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs index af4b2ed..1fa43c0 100644 --- a/MalwareMultiScan.Api/Services/ReceiverHostedService.cs +++ b/MalwareMultiScan.Api/Services/Implementations/ReceiverHostedService.cs @@ -1,31 +1,21 @@ using System.Threading; using System.Threading.Tasks; using EasyNetQ; +using MalwareMultiScan.Api.Services.Interfaces; using MalwareMultiScan.Backends.Messages; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace MalwareMultiScan.Api.Services +namespace MalwareMultiScan.Api.Services.Implementations { - /// - /// Receiver hosted service. - /// - public class ReceiverHostedService : IHostedService + public class ReceiverHostedService : IReceiverHostedService { private readonly IBus _bus; private readonly IConfiguration _configuration; private readonly ILogger _logger; - private readonly ScanResultService _scanResultService; - - /// - /// Create receiver hosted service. - /// - /// Service bus. - /// Configuration. - /// Scan result service. - /// Logger. - public ReceiverHostedService(IBus bus, IConfiguration configuration, ScanResultService scanResultService, + private readonly IScanResultService _scanResultService; + + public ReceiverHostedService(IBus bus, IConfiguration configuration, IScanResultService scanResultService, ILogger logger) { _bus = bus; @@ -33,8 +23,7 @@ namespace MalwareMultiScan.Api.Services _scanResultService = scanResultService; _logger = logger; } - - /// + public Task StartAsync(CancellationToken cancellationToken) { _bus.Receive(_configuration.GetValue("ResultsSubscriptionId"), async message => @@ -55,12 +44,10 @@ namespace MalwareMultiScan.Api.Services return Task.CompletedTask; } - - - /// + public Task StopAsync(CancellationToken cancellationToken) { - _bus.Dispose(); + _bus?.Dispose(); _logger.LogInformation( "Stopped hosted service for receiving scan results"); diff --git a/MalwareMultiScan.Api/Services/ScanBackendService.cs b/MalwareMultiScan.Api/Services/Implementations/ScanBackendService.cs similarity index 66% rename from MalwareMultiScan.Api/Services/ScanBackendService.cs rename to MalwareMultiScan.Api/Services/Implementations/ScanBackendService.cs index 71ca347..83dc419 100644 --- a/MalwareMultiScan.Api/Services/ScanBackendService.cs +++ b/MalwareMultiScan.Api/Services/Implementations/ScanBackendService.cs @@ -4,29 +4,20 @@ using System.Threading.Tasks; using EasyNetQ; using MalwareMultiScan.Api.Data.Configuration; using MalwareMultiScan.Api.Data.Models; +using MalwareMultiScan.Api.Services.Interfaces; using MalwareMultiScan.Backends.Messages; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -namespace MalwareMultiScan.Api.Services +namespace MalwareMultiScan.Api.Services.Implementations { - /// - /// Scan backends service. - /// - public class ScanBackendService + public class ScanBackendService : IScanBackendService { private readonly IBus _bus; private readonly ILogger _logger; - /// - /// Create scan backend service. - /// - /// Configuration. - /// Service bus. - /// Logger. - /// Missing BackendsConfiguration YAML file. public ScanBackendService(IConfiguration configuration, IBus bus, ILogger logger) { _bus = bus; @@ -45,18 +36,9 @@ namespace MalwareMultiScan.Api.Services List = deserializer.Deserialize(configurationContent); } - - /// - /// List of available scan backends. - /// + public ScanBackend[] List { get; } - - /// - /// Queue URL scan. - /// - /// Scan result instance. - /// Scan backend. - /// File download URL. + public async Task QueueUrlScan(ScanResult result, ScanBackend backend, string fileUrl) { _logger.LogInformation( diff --git a/MalwareMultiScan.Api/Services/ScanResultService.cs b/MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs similarity index 53% rename from MalwareMultiScan.Api/Services/ScanResultService.cs rename to MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs index 862b573..50df75e 100644 --- a/MalwareMultiScan.Api/Services/ScanResultService.cs +++ b/MalwareMultiScan.Api/Services/Implementations/ScanResultService.cs @@ -2,40 +2,29 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using MalwareMultiScan.Api.Data.Models; +using MalwareMultiScan.Api.Services.Interfaces; using MongoDB.Bson; using MongoDB.Driver; using MongoDB.Driver.GridFS; -namespace MalwareMultiScan.Api.Services +namespace MalwareMultiScan.Api.Services.Implementations { - /// - /// Scan results service. - /// - public class ScanResultService + public class ScanResultService : IScanResultService { private const string CollectionName = "ScanResults"; - private readonly GridFSBucket _bucket; + private readonly IGridFSBucket _bucket; private readonly IMongoCollection _collection; - private readonly ScanBackendService _scanBackendService; - - /// - /// Create scan result service. - /// - /// Mongo database. - /// Scan backend service. - public ScanResultService(IMongoDatabase db, ScanBackendService scanBackendService) + private readonly IScanBackendService _scanBackendService; + + public ScanResultService(IMongoDatabase db, IGridFSBucket bucket, IScanBackendService scanBackendService) { + _bucket = bucket; _scanBackendService = scanBackendService; - - _bucket = new GridFSBucket(db); + _collection = db.GetCollection(CollectionName); } - - /// - /// Create scan result. - /// - /// Scan result. + public async Task CreateScanResult() { var scanResult = new ScanResult @@ -49,12 +38,7 @@ namespace MalwareMultiScan.Api.Services return scanResult; } - - /// - /// Get scan result. - /// - /// Scan result id. - /// Scan result. + public async Task GetScanResult(string id) { var result = await _collection.FindAsync( @@ -62,16 +46,7 @@ namespace MalwareMultiScan.Api.Services return await result.FirstOrDefaultAsync(); } - - /// - /// Update scan status for the backend. - /// - /// Result id. - /// Backend id. - /// Duration of scan. - /// If the scan has been completed. - /// If the scan has been succeeded. - /// List of found threats. + public async Task UpdateScanResultForBackend(string resultId, string backendId, long duration, bool completed = false, bool succeeded = false, string[] threats = null) { @@ -85,24 +60,13 @@ namespace MalwareMultiScan.Api.Services Threats = threats ?? new string[] { } })); } - - /// - /// Queue URL scan. - /// - /// Scan result instance. - /// File URL. + public async Task QueueUrlScan(ScanResult result, string fileUrl) { foreach (var backend in _scanBackendService.List.Where(b => b.Enabled)) await _scanBackendService.QueueUrlScan(result, backend, fileUrl); } - - /// - /// Store file. - /// - /// File name. - /// File stream. - /// Stored file id. + public async Task StoreFile(string fileName, Stream fileStream) { var objectId = await _bucket.UploadFromStreamAsync( @@ -110,21 +74,23 @@ namespace MalwareMultiScan.Api.Services return objectId.ToString(); } - - /// - /// Obtain stored file stream. - /// - /// File id. - /// File seekable stream. + public async Task ObtainFile(string id) { if (!ObjectId.TryParse(id, out var objectId)) return null; - return await _bucket.OpenDownloadStreamAsync(objectId, new GridFSDownloadOptions + try { - Seekable = true - }); + return await _bucket.OpenDownloadStreamAsync(objectId, new GridFSDownloadOptions + { + Seekable = true + }); + } + catch (GridFSFileNotFoundException) + { + return null; + } } } } \ No newline at end of file diff --git a/MalwareMultiScan.Api/Services/Interfaces/IReceiverHostedService.cs b/MalwareMultiScan.Api/Services/Interfaces/IReceiverHostedService.cs new file mode 100644 index 0000000..7a8f52b --- /dev/null +++ b/MalwareMultiScan.Api/Services/Interfaces/IReceiverHostedService.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.Hosting; + +namespace MalwareMultiScan.Api.Services.Interfaces +{ + public interface IReceiverHostedService : IHostedService + { + + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Services/Interfaces/IScanBackendService.cs b/MalwareMultiScan.Api/Services/Interfaces/IScanBackendService.cs new file mode 100644 index 0000000..66ac044 --- /dev/null +++ b/MalwareMultiScan.Api/Services/Interfaces/IScanBackendService.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using MalwareMultiScan.Api.Data.Configuration; +using MalwareMultiScan.Api.Data.Models; + +namespace MalwareMultiScan.Api.Services.Interfaces +{ + public interface IScanBackendService + { + ScanBackend[] List { get; } + + Task QueueUrlScan(ScanResult result, ScanBackend backend, string fileUrl); + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs b/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs new file mode 100644 index 0000000..172ab87 --- /dev/null +++ b/MalwareMultiScan.Api/Services/Interfaces/IScanResultService.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Threading.Tasks; +using MalwareMultiScan.Api.Data.Models; + +namespace MalwareMultiScan.Api.Services.Interfaces +{ + public interface IScanResultService + { + Task CreateScanResult(); + + Task GetScanResult(string id); + + Task UpdateScanResultForBackend(string resultId, string backendId, long duration, + bool completed = false, bool succeeded = false, string[] threats = null); + + Task QueueUrlScan(ScanResult result, string fileUrl); + + Task StoreFile(string fileName, Stream fileStream); + + Task ObtainFile(string id); + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Api/Startup.cs b/MalwareMultiScan.Api/Startup.cs index 1472e95..d3a6905 100644 --- a/MalwareMultiScan.Api/Startup.cs +++ b/MalwareMultiScan.Api/Startup.cs @@ -1,5 +1,7 @@ +using System.Diagnostics.CodeAnalysis; using MalwareMultiScan.Api.Extensions; -using MalwareMultiScan.Api.Services; +using MalwareMultiScan.Api.Services.Implementations; +using MalwareMultiScan.Api.Services.Interfaces; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; @@ -7,7 +9,8 @@ using Microsoft.Extensions.DependencyInjection; namespace MalwareMultiScan.Api { - public class Startup + [ExcludeFromCodeCoverage] + internal class Startup { private readonly IConfiguration _configuration; @@ -28,8 +31,8 @@ namespace MalwareMultiScan.Api services.AddMongoDb(_configuration); services.AddRabbitMq(_configuration); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddControllers(); diff --git a/MalwareMultiScan.Tests/AttributesTests.cs b/MalwareMultiScan.Tests/AttributesTests.cs new file mode 100644 index 0000000..e44675f --- /dev/null +++ b/MalwareMultiScan.Tests/AttributesTests.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using MalwareMultiScan.Api.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Moq; +using NUnit.Framework; + +namespace MalwareMultiScan.Tests +{ + public class AttributesTests + { + [Test] + public void TestIsHttpUrlAttribute() + { + var attribute = new IsHttpUrlAttribute(); + + Assert.True(attribute.IsValid("http://test.com/file.zip")); + Assert.True(attribute.IsValid("https://test.com/file.zip")); + Assert.True(attribute.IsValid("HTTPS://test.com")); + Assert.False(attribute.IsValid(null)); + Assert.False(attribute.IsValid(string.Empty)); + Assert.False(attribute.IsValid("test")); + Assert.False(attribute.IsValid("ftp://test.com")); + } + + [Test] + public void TestMaxFileSizeAttribute() + { + var attribute = new MaxFileSizeAttribute(); + + var configuration = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource()) + }) + { + ["MaxFileSize"] = "100" + }; + + var context = new ValidationContext( + string.Empty, Mock.Of(x => + x.GetService(typeof(IConfiguration)) == configuration + ), null); + + Assert.True(attribute.GetValidationResult( + Mock.Of(x => x.Length == 10), context) == ValidationResult.Success); + + Assert.True(attribute.GetValidationResult( + Mock.Of(x => x.Length == 100), context) == ValidationResult.Success); + + Assert.False(attribute.GetValidationResult( + Mock.Of(x => x.Length == 101), context) == ValidationResult.Success); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Tests/ControllersTests.cs b/MalwareMultiScan.Tests/ControllersTests.cs new file mode 100644 index 0000000..685ec08 --- /dev/null +++ b/MalwareMultiScan.Tests/ControllersTests.cs @@ -0,0 +1,103 @@ +using System.IO; +using System.Threading.Tasks; +using MalwareMultiScan.Api.Controllers; +using MalwareMultiScan.Api.Data.Models; +using MalwareMultiScan.Api.Services.Interfaces; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using NUnit.Framework; + +namespace MalwareMultiScan.Tests +{ + public class ControllersTests + { + private DownloadController _downloadController; + private QueueController _queueController; + private ScanResultsController _scanResultsController; + private Mock _scanResultServiceMock; + + [SetUp] + public void SetUp() + { + _scanResultServiceMock = new Mock(); + + _scanResultServiceMock + .Setup(x => x.ObtainFile("existing")) + .Returns(Task.FromResult(new MemoryStream() as Stream)); + + _scanResultServiceMock + .Setup(x => x.ObtainFile("non-existing")) + .Returns(Task.FromResult((Stream) null)); + + _scanResultServiceMock + .Setup(x => x.GetScanResult("existing")) + .Returns(Task.FromResult(new ScanResult + { + Id = "existing" + })); + + _scanResultServiceMock + .Setup(x => x.GetScanResult("non-existing")) + .Returns(Task.FromResult((ScanResult) null)); + + _scanResultServiceMock + .Setup(x => x.ObtainFile("non-existing")) + .Returns(Task.FromResult((Stream) null)); + + _scanResultServiceMock + .Setup(x => x.CreateScanResult()) + .Returns(Task.FromResult(new ScanResult())); + + _downloadController = new DownloadController(_scanResultServiceMock.Object); + _scanResultsController = new ScanResultsController(_scanResultServiceMock.Object); + + _queueController = new QueueController(_scanResultServiceMock.Object) + { + Url = Mock.Of() + }; + } + + [Test] + public async Task TestFileDownload() + { + Assert.True(await _downloadController.Index("existing") is FileStreamResult); + Assert.True(await _downloadController.Index("non-existing") is NotFoundResult); + } + + [Test] + public async Task TestScanResults() + { + Assert.True( + await _scanResultsController.Index("existing") is OkObjectResult okObjectResult && + okObjectResult.Value is ScanResult scanResult && scanResult.Id == "existing"); + + Assert.True(await _scanResultsController.Index("non-existing") is NotFoundResult); + } + + [Test] + public async Task TestFileQueue() + { + Assert.True(await _queueController.ScanFile( + Mock.Of(x => + x.FileName == "test.exe" && + x.OpenReadStream() == new MemoryStream()) + ) is CreatedAtActionResult); + + _scanResultServiceMock.Verify( + x => x.StoreFile("test.exe", It.IsAny()), Times.Once); + + _scanResultServiceMock.Verify( + x => x.QueueUrlScan(It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task TestUrlQueue() + { + Assert.True(await _queueController.ScanUrl("http://test.com") is CreatedAtActionResult); + + _scanResultServiceMock.Verify( + x => x.QueueUrlScan(It.IsAny(), "http://test.com"), Times.Once); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Tests/MalwareMultiScan.Tests.csproj b/MalwareMultiScan.Tests/MalwareMultiScan.Tests.csproj new file mode 100644 index 0000000..ac5f5d8 --- /dev/null +++ b/MalwareMultiScan.Tests/MalwareMultiScan.Tests.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + diff --git a/MalwareMultiScan.Tests/ReceiverHostedServiceTests.cs b/MalwareMultiScan.Tests/ReceiverHostedServiceTests.cs new file mode 100644 index 0000000..9cb976e --- /dev/null +++ b/MalwareMultiScan.Tests/ReceiverHostedServiceTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using EasyNetQ; +using MalwareMultiScan.Api.Services.Implementations; +using MalwareMultiScan.Api.Services.Interfaces; +using MalwareMultiScan.Backends.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; + +namespace MalwareMultiScan.Tests +{ + public class ReceiverHostedServiceTests + { + private IReceiverHostedService _receiverHostedService; + private Mock _busMock; + private Mock _scanResultServiceMock; + + [SetUp] + public void SetUp() + { + _busMock = new Mock(); + + _busMock + .Setup(x => x.Receive("mms.results", It.IsAny>())) + .Callback>((s, func) => + { + var task = func.Invoke(new ScanResultMessage + { + Id = "test", + Backend = "dummy", + Duration = 100, + Succeeded = true, + Threats = new[] {"Test"} + }); + + task.Wait(); + }); + + _scanResultServiceMock = new Mock(); + + var configuration = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource()) + }) + { + ["ResultsSubscriptionId"] = "mms.results" + }; + + _receiverHostedService = new ReceiverHostedService( + _busMock.Object, + configuration, + _scanResultServiceMock.Object, + Mock.Of>()); + } + + [Test] + public async Task TestBusReceiveScanResultMessage() + { + await _receiverHostedService.StartAsync(default); + + _scanResultServiceMock.Verify(x => x.UpdateScanResultForBackend( + "test", "dummy", 100, true, true, new[] {"Test"}), Times.Once); + } + + [Test] + public async Task TestBusIsDisposedOnStop() + { + await _receiverHostedService.StopAsync(default); + + _busMock.Verify(x => x.Dispose(), Times.Once); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Tests/ScanBackendServiceTests.cs b/MalwareMultiScan.Tests/ScanBackendServiceTests.cs new file mode 100644 index 0000000..8af896b --- /dev/null +++ b/MalwareMultiScan.Tests/ScanBackendServiceTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using EasyNetQ; +using MalwareMultiScan.Api.Data.Configuration; +using MalwareMultiScan.Api.Data.Models; +using MalwareMultiScan.Api.Services.Implementations; +using MalwareMultiScan.Api.Services.Interfaces; +using MalwareMultiScan.Backends.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; + +namespace MalwareMultiScan.Tests +{ + public class ScanBackendServiceTests + { + private IScanBackendService _scanBackendService; + private Mock _busMock; + + [SetUp] + public void SetUp() + { + var configuration = new ConfigurationRoot(new List + { + new MemoryConfigurationProvider(new MemoryConfigurationSource()) + }) + { + ["BackendsConfiguration"] = "backends.yaml" + }; + + _busMock = new Mock(); + + _scanBackendService = new ScanBackendService( + configuration, + _busMock.Object, + Mock.Of>()); + } + + [Test] + public void TestBackendsAreNotEmpty() + { + Assert.IsNotEmpty(_scanBackendService.List); + } + + [Test] + public async Task TestQueueUrlScan() + { + await _scanBackendService.QueueUrlScan( + new ScanResult { Id = "test"}, + new ScanBackend { Id = "dummy"}, + "http://test.com" + ); + + _busMock.Verify(x => x.SendAsync( + "dummy", It.Is(r => + r.Id == "test" && + r.Uri == new Uri("http://test.com"))), Times.Once); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.Tests/ScanResultServiceTests.cs b/MalwareMultiScan.Tests/ScanResultServiceTests.cs new file mode 100644 index 0000000..48cf5f0 --- /dev/null +++ b/MalwareMultiScan.Tests/ScanResultServiceTests.cs @@ -0,0 +1,119 @@ +using System.IO; +using System.Threading.Tasks; +using MalwareMultiScan.Api.Data.Configuration; +using MalwareMultiScan.Api.Data.Models; +using MalwareMultiScan.Api.Services.Implementations; +using MalwareMultiScan.Api.Services.Interfaces; +using Mongo2Go; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.GridFS; +using Moq; +using NUnit.Framework; + +namespace MalwareMultiScan.Tests +{ + public class ScanResultServiceTest + { + private IScanResultService _resultService; + private MongoDbRunner _mongoDbRunner; + private Mock _scanBackendService; + + [SetUp] + public void SetUp() + { + _mongoDbRunner = MongoDbRunner.Start(); + + var connection = new MongoClient(_mongoDbRunner.ConnectionString); + var database = connection.GetDatabase("Test"); + var gridFsBucket = new GridFSBucket(database); + + _scanBackendService = new Mock(); + + _scanBackendService + .SetupGet(x => x.List) + .Returns(() => new[] + { + new ScanBackend {Id = "dummy", Enabled = true}, + new ScanBackend {Id = "clamav", Enabled = true}, + new ScanBackend {Id = "disabled", Enabled = false}, + }); + + _resultService = new ScanResultService( + database, gridFsBucket, _scanBackendService.Object); + } + + [TearDown] + public void TearDown() + { + _mongoDbRunner.Dispose(); + } + + [Test] + public async Task TestFileStorageStoreObtain() + { + var originalData = new byte[] {0xDE, 0xAD, 0xBE, 0xEF}; + + string fileId; + + await using (var dataStream = new MemoryStream(originalData)) + fileId = await _resultService.StoreFile("test.txt", dataStream); + + Assert.NotNull(fileId); + + Assert.IsNull(await _resultService.ObtainFile("wrong-id")); + Assert.IsNull(await _resultService.ObtainFile(ObjectId.GenerateNewId().ToString())); + + await using var fileSteam = await _resultService.ObtainFile(fileId); + + var readData = new[] + { + (byte)fileSteam.ReadByte(), + (byte)fileSteam.ReadByte(), + (byte)fileSteam.ReadByte(), + (byte)fileSteam.ReadByte() + }; + + Assert.That(readData, Is.EquivalentTo(originalData)); + } + + [Test] + public async Task TestScanResultCreateGet() + { + var result = await _resultService.CreateScanResult(); + + Assert.NotNull(result.Id); + Assert.That(result.Results, Contains.Key("dummy")); + + await _resultService.UpdateScanResultForBackend( + result.Id, "dummy", 100, true, true, new[] {"Test"}); + + result = await _resultService.GetScanResult(result.Id); + + Assert.NotNull(result.Id); + Assert.That(result.Results, Contains.Key("dummy")); + + var dummyResult = result.Results["dummy"]; + + Assert.IsTrue(dummyResult.Completed); + Assert.IsTrue(dummyResult.Succeeded); + Assert.AreEqual(dummyResult.Duration, 100); + Assert.IsNotEmpty(dummyResult.Threats); + Assert.That(dummyResult.Threats, Is.EquivalentTo(new[] {"Test"})); + } + + [Test] + public async Task TestQueueUrlScan() + { + var result = await _resultService.CreateScanResult(); + + await _resultService.QueueUrlScan( + result, "http://url.com"); + + _scanBackendService.Verify(x => x.QueueUrlScan( + It.IsAny(), + It.IsAny(), + "http://url.com"), Times.Exactly(2)); + } + } +} \ No newline at end of file diff --git a/MalwareMultiScan.sln b/MalwareMultiScan.sln index 7136b4f..be584ea 100644 --- a/MalwareMultiScan.sln +++ b/MalwareMultiScan.sln @@ -13,6 +13,8 @@ ProjectSection(SolutionItems) = preProject docker-compose.yaml = docker-compose.yaml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MalwareMultiScan.Tests", "MalwareMultiScan.Tests\MalwareMultiScan.Tests.csproj", "{9896162D-8FC7-4911-933F-A78C94128923}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,5 +33,9 @@ Global {8A16A3C4-2AE3-4F63-8280-635FF7878080}.Debug|Any CPU.Build.0 = Debug|Any CPU {8A16A3C4-2AE3-4F63-8280-635FF7878080}.Release|Any CPU.ActiveCfg = Release|Any CPU {8A16A3C4-2AE3-4F63-8280-635FF7878080}.Release|Any CPU.Build.0 = Release|Any CPU + {9896162D-8FC7-4911-933F-A78C94128923}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9896162D-8FC7-4911-933F-A78C94128923}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9896162D-8FC7-4911-933F-A78C94128923}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9896162D-8FC7-4911-933F-A78C94128923}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal