using System.Net; using MijnBesluit.Protocol; namespace MijnBesluit.SelfCheck; /// Controleert de SocketServer met een echte verbinding over localhost. public static class SocketChecks { public static void Register(Runner runner) { runner.TestAsync("SocketServer: verzoek en antwoord over een echte verbinding", async () => { await using var server = new SocketListener( new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 }, (req, conn, ct) => Task.FromResult(SocketResponse.Ok(req.RequestId, new { echo = req.Action }))); server.Start(); await using var client = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false); var response = await client.SendAsync(SocketActions.Ping); Assert.True(response.Status == "Ok", "de server antwoordt met Ok"); }); runner.TestAsync("SocketServer: meerdere verzoeken tegelijk komen bij het juiste antwoord terecht", async () => { await using var server = new SocketListener( new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 }, async (req, conn, ct) => { // Antwoorden bewust door elkaar laten lopen. var nummer = req.PayloadAs>()!["n"]; await Task.Delay(50 - nummer * 5, ct); return SocketResponse.Ok(req.RequestId, new { n = nummer }); }); server.Start(); await using var client = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false); await client.ConnectAsync(); var taken = Enumerable.Range(1, 8) .Select(n => client.SendAsync(SocketActions.Ping, new { n })) .ToArray(); var antwoorden = await Task.WhenAll(taken); for (var i = 0; i < antwoorden.Length; i++) { var terug = System.Text.Json.JsonSerializer .Deserialize>( System.Text.Json.JsonSerializer.Serialize(antwoorden[i].Payload, SocketFraming.Json), SocketFraming.Json)!["n"]; Assert.True(terug == i + 1, $"antwoord {i + 1} hoort bij het juiste verzoek"); } }); runner.TestAsync("SocketServer: inhoud met '**BYE**' erin komt ongeschonden aan", async () => { // Precies het randgeval waar een markering-gebaseerd protocol op stukloopt. var lastig = new string('A', 200_000) + "**BYE**" + new string('B', 200_000) + "BYE"; await using var server = new SocketListener( new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 }, (req, conn, ct) => Task.FromResult(SocketResponse.Ok(req.RequestId, new { tekst = req.PayloadAs>()!["tekst"] }))); server.Start(); await using var client = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false); var response = await client.SendAsync(SocketActions.Ping, new { tekst = lastig }); var terug = System.Text.Json.JsonSerializer .Deserialize>( System.Text.Json.JsonSerializer.Serialize(response.Payload, SocketFraming.Json), SocketFraming.Json)!["tekst"]; Assert.True(response.Status == "Ok", "de server antwoordt"); Assert.True(terug.Length == lastig.Length, "de lengte klopt nog"); Assert.True(terug == lastig, "de inhoud is niet afgekapt op de markering"); }); runner.TestAsync("SocketServer: een fout in de afhandeling lekt geen interne details", async () => { await using var server = new SocketListener( new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 }, (req, conn, ct) => throw new InvalidOperationException("verbinding met database mislukt op server01")); server.Start(); await using var client = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false); var response = await client.SendAsync(SocketActions.Ping); Assert.True(response.Status == "Error", "de app krijgt een foutantwoord"); Assert.True(!response.Message!.Contains("server01"), "zonder interne details erin"); }); runner.TestAsync("SocketServer: de server kan uit zichzelf een melding sturen", async () => { SocketConnection? verbinding = null; await using var server = new SocketListener( new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 }, (req, conn, ct) => { verbinding = conn; conn.UserId = 42; return Task.FromResult(SocketResponse.Ok(req.RequestId)); }); server.Start(); await using var client = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false); var ontvangen = new TaskCompletionSource(); client.PushReceived += r => ontvangen.TrySetResult(r); await client.SendAsync(SocketActions.Ping); Assert.True(verbinding is not null, "de server kent de verbinding"); await server.PushToUserAsync(42, new SocketResponse { Status = "Ok", Message = "Er staat een bericht voor je klaar." }); var melding = await ontvangen.Task.WaitAsync(TimeSpan.FromSeconds(5)); Assert.True(melding.Message!.Contains("bericht voor je klaar"), "de app ontvangt de melding"); }); runner.TestAsync("SocketServer: elke verbinding krijgt een eigen thread", async () => { var threadsPerVerbinding = new System.Collections.Concurrent.ConcurrentDictionary(); await using var server = new SocketListener( new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 }, (req, conn, ct) => { threadsPerVerbinding[conn.Id] = conn.ThreadName ?? "?"; return Task.FromResult(SocketResponse.Ok(req.RequestId, new { thread = conn.ThreadName })); }); server.Start(); await using var app1 = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false); await using var app2 = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false); await app1.SendAsync(SocketActions.Ping); await app2.SendAsync(SocketActions.Ping); Assert.True(threadsPerVerbinding.Count == 2, "de server ziet twee aparte verbindingen"); Assert.True(threadsPerVerbinding.Values.Distinct().Count() == 2, "elke verbinding wordt door een eigen thread bediend"); Assert.True(threadsPerVerbinding.Values.All(n => n.StartsWith("MijnBesluit-verbinding-")), "de thread draagt de naam van de verbinding, handig bij foutzoeken"); }); runner.TestAsync("SocketServer: een traag verzoek houdt de volgende niet op", async () => { // Drie verzoeken van dezelfde verbinding die elk een halve seconde blijven hangen. // Op een gedeelde thread zou dat anderhalve seconde duren; elk op een eigen thread // hooguit iets meer dan een halve. var gebruikteThreads = new System.Collections.Concurrent.ConcurrentBag(); await using var server = new SocketListener( new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 }, (req, conn, ct) => { gebruikteThreads.Add(Environment.CurrentManagedThreadId); Thread.Sleep(500); return Task.FromResult(SocketResponse.Ok(req.RequestId)); }); server.Start(); await using var app = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false); await app.ConnectAsync(); var klok = System.Diagnostics.Stopwatch.StartNew(); var antwoorden = await Task.WhenAll( app.SendAsync(SocketActions.Ping), app.SendAsync(SocketActions.Ping), app.SendAsync(SocketActions.Ping)); klok.Stop(); Assert.True(antwoorden.All(a => a.Status == "Ok"), "alle drie de verzoeken worden beantwoord"); Assert.True(gebruikteThreads.Distinct().Count() == 3, "elk verzoek liep op zijn eigen thread"); Assert.True(klok.ElapsedMilliseconds < 1200, $"ze liepen naast elkaar (duurde {klok.ElapsedMilliseconds} ms, na elkaar zou 1500 ms zijn)"); }); runner.TestAsync("SocketServer: boven de grens wachten verzoeken netjes op hun beurt", async () => { var tegelijk = 0; var hoogste = 0; var slot = new object(); await using var server = new SocketListener( new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0, MaxRequestsPerConnection = 2 }, (req, conn, ct) => { lock (slot) { tegelijk++; hoogste = Math.Max(hoogste, tegelijk); } Thread.Sleep(150); lock (slot) { tegelijk--; } return Task.FromResult(SocketResponse.Ok(req.RequestId)); }); server.Start(); await using var app = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false); await app.ConnectAsync(); var antwoorden = await Task.WhenAll(Enumerable.Range(0, 6) .Select(_ => app.SendAsync(SocketActions.Ping, timeout: TimeSpan.FromSeconds(20)))); Assert.True(antwoorden.All(a => a.Status == "Ok"), "alle zes verzoeken worden alsnog beantwoord"); Assert.True(hoogste <= 2, $"er liepen er nooit meer dan twee tegelijk (gemeten: {hoogste})"); }); runner.Test("Bestandsoverdracht: stukken moeten op volgorde en binnen de maat blijven", () => { var klok = new TestClock(); var overdracht = new MijnBesluit.Server.Bestandsoverdracht(klok); var id = overdracht.Begin(gebruikerId: 1, "foto.jpg", "image/jpeg", null, totaalBytes: 10); overdracht.Stuk(id, 1, 0, new byte[4]); Assert.Throws(() => overdracht.Stuk(id, 1, 0, new byte[4]), "hetzelfde stuk twee keer wordt geweigerd"); Assert.Throws(() => overdracht.Stuk(id, 1, 5, new byte[4]), "een stuk dat overslaat wordt geweigerd"); Assert.Throws(() => overdracht.Stuk(id, 1, 1, new byte[99]), "meer dan aangekondigd wordt geweigerd"); Assert.Throws(() => overdracht.Klaar(id, 1), "afsluiten met te weinig bytes wordt geweigerd"); overdracht.Stuk(id, 1, 1, new byte[6]); var (naam, soort, item, inhoud) = overdracht.Klaar(id, 1); Assert.True(naam == "foto.jpg" && soort == "image/jpeg" && item is null, "de gegevens komen terug"); Assert.True(inhoud.Length == 10, "en de volledige inhoud"); Assert.True(overdracht.Lopend == 0, "de overdracht is opgeruimd"); }); runner.Test("Bestandsoverdracht: van een ander, of te lang blijven liggen", () => { var klok = new TestClock(); var overdracht = new MijnBesluit.Server.Bestandsoverdracht(klok); var id = overdracht.Begin(gebruikerId: 1, "polis.pdf", "application/pdf", null, totaalBytes: 100); // Gebruiker 2 mag er niet bij, ook niet als hij het kenmerk op de een of andere manier kent. Assert.Throws(() => overdracht.Stuk(id, 2, 0, new byte[10]), "de upload van iemand anders bestaat voor jou niet"); overdracht.Afbreken(id, 2); Assert.True(overdracht.Lopend == 1, "en is door hem ook niet af te breken"); // Blijft hij te lang liggen, dan is hij weg. klok.Advance(MijnBesluit.Server.Bestandsoverdracht.Verlooptijd + TimeSpan.FromMinutes(1)); Assert.Throws(() => overdracht.Stuk(id, 1, 0, new byte[10]), "een upload die te lang stillag wordt geweigerd"); }); runner.Test("Bestandsoverdracht: niet eindeloos veel tegelijk", () => { var overdracht = new MijnBesluit.Server.Bestandsoverdracht(new TestClock()); for (var i = 0; i < MijnBesluit.Server.Bestandsoverdracht.MaxPerGebruiker; i++) overdracht.Begin(1, $"bestand{i}.bin", "application/octet-stream", null, 100); Assert.Throws( () => overdracht.Begin(1, "nog een.bin", "application/octet-stream", null, 100), "boven de grens wordt een nieuwe upload geweigerd"); // Iemand anders heeft daar geen last van. overdracht.Begin(2, "van piet.bin", "application/octet-stream", null, 100); Assert.True(overdracht.Lopend == MijnBesluit.Server.Bestandsoverdracht.MaxPerGebruiker + 1, "de grens geldt per gebruiker"); }); } }