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