diff --git a/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj b/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj
index b079fbe16f..1045c8bef3 100644
--- a/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj
+++ b/NAPS2.Sdk.Tests/NAPS2.Sdk.Tests.csproj
@@ -28,6 +28,7 @@
+
diff --git a/NAPS2.Sdk.Tests/Remoting/ScanServerIntegrationTests.cs b/NAPS2.Sdk.Tests/Remoting/ScanServerIntegrationTests.cs
new file mode 100644
index 0000000000..aa5ffb4a63
--- /dev/null
+++ b/NAPS2.Sdk.Tests/Remoting/ScanServerIntegrationTests.cs
@@ -0,0 +1,166 @@
+using Microsoft.Extensions.Logging;
+using NAPS2.Escl.Server;
+using NAPS2.Remoting.Server;
+using NAPS2.Scan;
+using NAPS2.Scan.Exceptions;
+using NAPS2.Scan.Internal;
+using NAPS2.Sdk.Tests.Asserts;
+using NAPS2.Sdk.Tests.Mocks;
+using NSubstitute;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace NAPS2.Sdk.Tests.Remoting;
+
+public class ScanServerIntegrationTests : ContextualTests
+{
+ private readonly ScanServer _server;
+ private readonly MockScanBridge _bridge;
+ private readonly ScanController _client;
+ private readonly ScanDevice _clientDevice;
+
+ public ScanServerIntegrationTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
+ {
+ _server = new ScanServer(ScanningContext, new EsclServer());
+
+ // Set up a server connecting to a mock scan backend
+ _bridge = new MockScanBridge
+ {
+ MockOutput = [CreateScannedImage()]
+ };
+ var scanBridgeFactory = Substitute.For();
+ scanBridgeFactory.Create(Arg.Any()).Returns(_bridge);
+ _server.ScanController = new ScanController(ScanningContext, scanBridgeFactory);
+
+ // Initialize the server with a single device with a unique ID for the test
+ var displayName = $"testName{Guid.NewGuid()}";
+ ScanningContext.Logger.LogDebug("Display name: {Name}", displayName);
+ var serverDevice = new ScanDevice(ScanOptionsValidator.SystemDefaultDriver, "testID", "testName");
+ var serverSharedDevice = new SharedDevice { Device = serverDevice, Name = displayName };
+ _server.RegisterDevice(serverSharedDevice);
+ _server.Start().Wait();
+
+ // Set up a client ScanController for scanning through EsclScanDriver -> network -> ScanServer
+ _client = new ScanController(ScanningContext);
+ // This device won't match exactly the real device from GetDeviceList but it includes the UUID which is enough
+ // for EsclScanDriver to correctly identify the server for scanning.
+ _clientDevice = new ScanDevice(Driver.Escl, $"|{serverSharedDevice.GetUuid(_server.InstanceId)}", displayName);
+ }
+
+ public override void Dispose()
+ {
+ _server.Dispose();
+ base.Dispose();
+ }
+
+ [Fact]
+ public async Task FindDevice()
+ {
+ var devices = await _client.GetDeviceList(Driver.Escl);
+ // The device name is suffixed with the IP so we just check the prefix matches (and vice versa for ID)
+ Assert.Contains(devices,
+ device => device.Name.StartsWith(_clientDevice.Name) && device.ID.EndsWith(_clientDevice.ID));
+ }
+
+ [Fact]
+ public async Task Scan()
+ {
+ var images = await _client.Scan(new ScanOptions
+ {
+ Device = _clientDevice
+ }).ToListAsync();
+ Assert.Single(images);
+ ImageAsserts.Similar(ImageResources.dog, images[0]);
+ }
+
+ [Fact]
+ public async Task ScanMultiplePages()
+ {
+ _bridge.MockOutput =
+ CreateScannedImages(ImageResources.dog, ImageResources.dog_h_n300, ImageResources.dog_h_p300).ToList();
+ var images = await _client.Scan(new ScanOptions
+ {
+ Device = _clientDevice,
+ PaperSource = PaperSource.Feeder
+ }).ToListAsync();
+ Assert.Equal(3, images.Count);
+ ImageAsserts.Similar(ImageResources.dog, images[0]);
+ ImageAsserts.Similar(ImageResources.dog_h_n300, images[1]);
+ ImageAsserts.Similar(ImageResources.dog_h_p300, images[2]);
+ }
+
+ [Fact]
+ public async Task ScanWithCorrectOptions()
+ {
+ var images = await _client.Scan(new ScanOptions
+ {
+ Device = _clientDevice,
+ BitDepth = BitDepth.Color,
+ Dpi = 100,
+ PaperSource = PaperSource.Flatbed,
+ PageSize = PageSize.Letter,
+ PageAlign = HorizontalAlign.Right
+ }).ToListAsync();
+
+ var opts = _bridge.LastOptions;
+ Assert.Equal(BitDepth.Color, opts.BitDepth);
+ Assert.Equal(100, opts.Dpi);
+ Assert.Equal(PaperSource.Flatbed, opts.PaperSource);
+ Assert.Equal(PageSize.Letter, opts.PageSize);
+ Assert.Equal(HorizontalAlign.Right, opts.PageAlign);
+ Assert.Single(images);
+ ImageAsserts.Similar(ImageResources.dog, images[0]);
+
+ _bridge.MockOutput = CreateScannedImages(ImageResources.dog_gray).ToList();
+ images = await _client.Scan(new ScanOptions
+ {
+ Device = _clientDevice,
+ BitDepth = BitDepth.Grayscale,
+ Dpi = 300,
+ PaperSource = PaperSource.Feeder,
+ PageSize = PageSize.Legal,
+ PageAlign = HorizontalAlign.Center
+ }).ToListAsync();
+
+ opts = _bridge.LastOptions;
+ Assert.Equal(BitDepth.Grayscale, opts.BitDepth);
+ Assert.Equal(300, opts.Dpi);
+ Assert.Equal(PaperSource.Feeder, opts.PaperSource);
+ Assert.Equal(PageSize.Legal, opts.PageSize);
+ Assert.Equal(HorizontalAlign.Center, opts.PageAlign);
+ Assert.Single(images);
+ ImageAsserts.Similar(ImageResources.dog_gray, images[0]);
+
+ _bridge.MockOutput = CreateScannedImages(ImageResources.dog_bw).ToList();
+ images = await _client.Scan(new ScanOptions
+ {
+ Device = _clientDevice,
+ BitDepth = BitDepth.BlackAndWhite,
+ Dpi = 4800,
+ PaperSource = PaperSource.Duplex,
+ PageSize = PageSize.A3,
+ PageAlign = HorizontalAlign.Left
+ }).ToListAsync();
+
+ opts = _bridge.LastOptions;
+ Assert.Equal(BitDepth.BlackAndWhite, opts.BitDepth);
+ Assert.Equal(4800, opts.Dpi);
+ Assert.Equal(PaperSource.Duplex, opts.PaperSource);
+ Assert.Equal(PageSize.A3.WidthInMm, opts.PageSize!.WidthInMm, 1);
+ Assert.Equal(PageSize.A3.HeightInMm, opts.PageSize!.HeightInMm, 1);
+ Assert.Equal(HorizontalAlign.Left, opts.PageAlign);
+ Assert.Single(images);
+ ImageAsserts.Similar(ImageResources.dog_bw, images[0]);
+ }
+
+ [Fact]
+ public async Task ScanWithError()
+ {
+ _bridge.Error = new NoPagesException();
+
+ await Assert.ThrowsAsync(async () => await _client.Scan(new ScanOptions
+ {
+ Device = _clientDevice
+ }).ToListAsync());
+ }
+}
\ No newline at end of file
diff --git a/NAPS2.Sdk/Remoting/Server/ScanServer.cs b/NAPS2.Sdk/Remoting/Server/ScanServer.cs
index c000272a71..309bcf1769 100644
--- a/NAPS2.Sdk/Remoting/Server/ScanServer.cs
+++ b/NAPS2.Sdk/Remoting/Server/ScanServer.cs
@@ -43,7 +43,7 @@ public void RegisterDevice(SharedDevice sharedDevice)
}
public void UnregisterDevice(ScanDevice device, string? displayName = null) =>
- UnregisterDevice(new SharedDevice { Device = device, Name = displayName ?? device.Name, Port = 0 });
+ UnregisterDevice(new SharedDevice { Device = device, Name = displayName ?? device.Name });
public void UnregisterDevice(SharedDevice sharedDevice)
{
@@ -82,7 +82,7 @@ private EsclDeviceConfig MakeEsclDeviceConfig(SharedDevice device)
};
}
- public void Start() => _esclServer.Start();
+ public Task Start() => _esclServer.Start();
public void Stop() => _esclServer.Stop();
diff --git a/NAPS2.Sdk/Remoting/Server/SharedDevice.cs b/NAPS2.Sdk/Remoting/Server/SharedDevice.cs
index 96768fe8a3..8f3c4b4311 100644
--- a/NAPS2.Sdk/Remoting/Server/SharedDevice.cs
+++ b/NAPS2.Sdk/Remoting/Server/SharedDevice.cs
@@ -8,7 +8,7 @@ public record SharedDevice
{
public required string Name { get; init; }
public required ScanDevice Device { get; init; }
- public required int Port { get; init; }
+ public int Port { get; init; }
public string GetUuid(Guid instanceId)
{
diff --git a/NAPS2.Sdk/Scan/ScanController.cs b/NAPS2.Sdk/Scan/ScanController.cs
index 9c5d79b47e..f5e1d149de 100644
--- a/NAPS2.Sdk/Scan/ScanController.cs
+++ b/NAPS2.Sdk/Scan/ScanController.cs
@@ -38,6 +38,12 @@ public ScanController(ScanningContext scanningContext, OcrController ocrControll
{
}
+ internal ScanController(ScanningContext scanningContext, IScanBridgeFactory scanBridgeFactory)
+ : this(scanningContext, new LocalPostProcessor(scanningContext, new OcrController(scanningContext)),
+ new ScanOptionsValidator(), scanBridgeFactory)
+ {
+ }
+
internal ScanController(ScanningContext scanningContext, ILocalPostProcessor localPostProcessor,
ScanOptionsValidator scanOptionsValidator, IScanBridgeFactory scanBridgeFactory)
{