test(domain): cover clamd timeout, caller cancellation, null content and defaults (refs #192)
CI / k8s (pull_request) Successful in 18s
CI / build (pull_request) Successful in 5m23s
CI / lint (pull_request) Successful in 5m55s
CI / docs (pull_request) Successful in 1m3s
CI / unit (pull_request) Successful in 1m33s
CI / frontend (pull_request) Successful in 5m31s
CI / mutation (pull_request) Successful in 8m11s
CI / verify-stack (pull_request) Skipped

Domain mutation score 89.87% → 91.98%, back above the 90% break.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
not
2026-10-02 09:53:23 +02:00
co-authored by Claude Opus 5.5
parent 940aac7d25
commit e39a46febc
@@ -17,13 +17,14 @@ public class ClamdDocumentScannerTests
public Task<byte[]> Received { get; }
public int Port => ((IPEndPoint)_listener.LocalEndpoint).Port;
public FakeClamd(string reply)
/// <param name="reply">The answer, or null to accept the request and never answer (a hung clamd).</param>
public FakeClamd(string? reply)
{
_listener.Start();
Received = Serve(reply);
}
private async Task<byte[]> Serve(string reply)
private async Task<byte[]> Serve(string? reply)
{
using var client = await _listener.AcceptTcpClientAsync();
var stream = client.GetStream();
@@ -35,7 +36,10 @@ public class ClamdDocumentScannerTests
if (n == 0) break;
received.Write(buffer, 0, n);
}
await stream.WriteAsync(System.Text.Encoding.ASCII.GetBytes(reply + "\0"));
if (reply is null)
await Task.Delay(TimeSpan.FromSeconds(10)); // long past any test timeout
else
await stream.WriteAsync(System.Text.Encoding.ASCII.GetBytes(reply + "\0"));
return received.ToArray();
}
@@ -82,4 +86,32 @@ public class ClamdDocumentScannerTests
Assert.Equal(ScanVerdict.Unavailable, await ScannerFor(port).ScanAsync([1, 2, 3]));
}
[Fact]
public async Task A_clamd_that_never_answers_is_unavailable_after_the_timeout()
{
using var clamd = new FakeClamd(reply: null);
var scanner = new ClamdDocumentScanner(
new ClamAvOptions { Host = "127.0.0.1", Port = clamd.Port, Timeout = TimeSpan.FromMilliseconds(300) });
Assert.Equal(ScanVerdict.Unavailable, await scanner.ScanAsync([1, 2, 3]));
}
[Fact]
public async Task A_cancelled_request_is_cancelled_not_reported_unavailable()
{
// The caller giving up is not a scanner outage: it propagates instead of becoming a 503.
using var clamd = new FakeClamd(reply: null);
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => ScannerFor(clamd.Port).ScanAsync([1, 2, 3], cts.Token));
}
[Fact]
public async Task Rejects_null_content()
=> await Assert.ThrowsAsync<ArgumentNullException>(() => ScannerFor(1).ScanAsync(null!));
[Fact]
public void Defaults_to_the_clamav_service_on_the_clamd_port()
=> Assert.Equal(("clamav", 3310), (new ClamAvOptions().Host, new ClamAvOptions().Port));
}