419 lines
22 KiB
C#
419 lines
22 KiB
C#
using MijnBesluit.Uitrol;
|
|
|
|
namespace MijnBesluit.SelfCheck;
|
|
|
|
/// <summary>
|
|
/// De handtekening waarmee de uitrolagent weet dat een opdracht van jou komt.
|
|
///
|
|
/// Dit is het stuk dat je maar één keer fout hoeft te doen. De agent staat op de server waar de
|
|
/// kluizen en de masterkey liggen, en hij installeert wat hij binnenkrijgt. Elke grendel die
|
|
/// hieronder wordt beproefd is er een die anders stilletjes weg kan vallen.
|
|
/// </summary>
|
|
public static class UitrolChecks
|
|
{
|
|
/// <summary>Een geheugen dat doet alsof er op een bepaald moment is uitgerold.</summary>
|
|
private sealed class Proefgeheugen : Uitrolgeheugen
|
|
{
|
|
private readonly DateTime _moment;
|
|
public Proefgeheugen(DateTime moment) => _moment = moment;
|
|
|
|
public override DateTime? Laatste(string onderdeel) => _moment;
|
|
public override void Onthoud(string onderdeel, DateTime nieuwste) { }
|
|
}
|
|
|
|
public static void Register(Runner runner)
|
|
{
|
|
runner.Test("Uitrol: een opdracht zonder het juiste geheim komt er niet in", () =>
|
|
{
|
|
var geheim = Uitrolsleutel.NieuweSleutel();
|
|
var nu = DateTime.UtcNow;
|
|
var tijd = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
var nonce = Uitrolsleutel.NieuweNonce();
|
|
var doel = "pakket:1:ABCDEF";
|
|
|
|
var handtekening = Uitrolsleutel.Teken(geheim, doel, tijd, nonce);
|
|
|
|
Assert.True(Uitrolsleutel.Klopt(geheim, doel, tijd, nonce, handtekening, nu, new HashSet<string>()),
|
|
"met het goede geheim mag het");
|
|
|
|
var ander = Uitrolsleutel.NieuweSleutel();
|
|
Assert.True(!Uitrolsleutel.Klopt(ander, doel, tijd, nonce, handtekening, nu, new HashSet<string>()),
|
|
"met een ander geheim niet");
|
|
|
|
// Een kort geheim is geen geheim. Dit vangt de dag waarop iemand "test" invult om
|
|
// even iets te proberen en dat laat staan.
|
|
Assert.True(!Uitrolsleutel.Klopt("kort", doel, tijd, nonce,
|
|
Uitrolsleutel.Teken("kort", doel, tijd, nonce), nu, new HashSet<string>()),
|
|
"een te kort geheim wordt geweigerd, ook als de handtekening zelf klopt");
|
|
});
|
|
|
|
runner.Test("Uitrol: een ander pakket onder dezelfde handtekening wordt geweigerd", () =>
|
|
{
|
|
// Dit is waar de hash voor is. Zonder deze band tussen handtekening en inhoud kan
|
|
// iemand die een geldige opdracht opvangt er zijn eigen zip achteraan sturen.
|
|
var geheim = Uitrolsleutel.NieuweSleutel();
|
|
var tijd = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
var nonce = Uitrolsleutel.NieuweNonce();
|
|
|
|
var echt = new Uitrolopdracht(Uitrolsoort.Site, "mijnbesluit.zip", 1000, "AAAA", tijd, nonce, "");
|
|
var handtekening = Uitrolsleutel.Teken(geheim, echt.Doel, tijd, nonce);
|
|
|
|
var vervalst = echt with { Sha256 = "BBBB" };
|
|
Assert.True(!Uitrolsleutel.Klopt(geheim, vervalst.Doel, tijd, nonce, handtekening, DateTime.UtcNow,
|
|
new HashSet<string>()),
|
|
"een andere inhoud hoort niet onder deze handtekening te passen");
|
|
|
|
// Ook de soort zit erin: een pakket voor de site hoort niet als databasescript te
|
|
// kunnen binnenkomen.
|
|
var andereSoort = echt with { Soort = Uitrolsoort.Database };
|
|
Assert.True(!Uitrolsleutel.Klopt(geheim, andereSoort.Doel, tijd, nonce, handtekening, DateTime.UtcNow,
|
|
new HashSet<string>()),
|
|
"en een andere soort ook niet");
|
|
});
|
|
|
|
runner.Test("Uitrol: dezelfde opdracht kan geen tweede keer", () =>
|
|
{
|
|
// Zonder dit kan wie een opdracht opvangt hem gewoon nog een keer sturen. Bij een
|
|
// uitrol betekent dat: nog een keer installeren, op een moment dat jij het niet weet.
|
|
var geheim = Uitrolsleutel.NieuweSleutel();
|
|
var tijd = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
var nonce = Uitrolsleutel.NieuweNonce();
|
|
var doel = "pakket:1:ABCDEF";
|
|
var handtekening = Uitrolsleutel.Teken(geheim, doel, tijd, nonce);
|
|
|
|
var gezien = new HashSet<string>();
|
|
|
|
Assert.True(Uitrolsleutel.Klopt(geheim, doel, tijd, nonce, handtekening, DateTime.UtcNow, gezien),
|
|
"de eerste keer mag");
|
|
Assert.True(!Uitrolsleutel.Klopt(geheim, doel, tijd, nonce, handtekening, DateTime.UtcNow, gezien),
|
|
"de tweede keer niet");
|
|
|
|
// En een mislukte poging mag een geldige opdracht niet in de weg zitten: anders kan
|
|
// iemand die alleen het nonce kent jouw uitrol tegenhouden door hem vóór te zijn.
|
|
var vers = Uitrolsleutel.NieuweNonce();
|
|
Assert.True(!Uitrolsleutel.Klopt(geheim, doel, tijd, vers, "ONZIN", DateTime.UtcNow, gezien),
|
|
"een verkeerde handtekening wordt geweigerd");
|
|
Assert.True(Uitrolsleutel.Klopt(geheim, doel, tijd, vers,
|
|
Uitrolsleutel.Teken(geheim, doel, tijd, vers), DateTime.UtcNow, gezien),
|
|
"en datzelfde nonce mag daarna nog wel met de juiste handtekening");
|
|
});
|
|
|
|
runner.Test("Uitrol: een oude opdracht verloopt", () =>
|
|
{
|
|
var geheim = Uitrolsleutel.NieuweSleutel();
|
|
var nu = DateTime.UtcNow;
|
|
var doel = "stand";
|
|
|
|
var lang = new DateTimeOffset(nu.AddMinutes(-10)).ToUnixTimeSeconds();
|
|
var nonce = Uitrolsleutel.NieuweNonce();
|
|
Assert.True(!Uitrolsleutel.Klopt(geheim, doel, lang, nonce,
|
|
Uitrolsleutel.Teken(geheim, doel, lang, nonce), nu, new HashSet<string>()),
|
|
"tien minuten oud is te oud");
|
|
|
|
// En ook niet uit de toekomst: een klok die ver vooruit staat zou anders een opdracht
|
|
// opleveren die uren geldig blijft.
|
|
var straks = new DateTimeOffset(nu.AddMinutes(10)).ToUnixTimeSeconds();
|
|
var nonce2 = Uitrolsleutel.NieuweNonce();
|
|
Assert.True(!Uitrolsleutel.Klopt(geheim, doel, straks, nonce2,
|
|
Uitrolsleutel.Teken(geheim, doel, straks, nonce2), nu, new HashSet<string>()),
|
|
"en tien minuten in de toekomst ook niet");
|
|
|
|
// Een halve minuut speling hoort geen probleem te zijn; niet elke klok loopt gelijk.
|
|
var bijna = new DateTimeOffset(nu.AddSeconds(-30)).ToUnixTimeSeconds();
|
|
var nonce3 = Uitrolsleutel.NieuweNonce();
|
|
Assert.True(Uitrolsleutel.Klopt(geheim, doel, bijna, nonce3,
|
|
Uitrolsleutel.Teken(geheim, doel, bijna, nonce3), nu, new HashSet<string>()),
|
|
"een halve minuut verschil mag wel");
|
|
});
|
|
|
|
runner.Test("Uitrol: de volgorde ligt vast, ook als het briefje anders is opgeschreven", () =>
|
|
{
|
|
// De volgorde is geen smaak: een nieuwe kolom moet er zijn voordat de code erom
|
|
// vraagt, en de SocketServer gaat als laatste omdat het herstarten daarvan alle apps
|
|
// hun verbinding kost. Het venster zet de vinkjes in willekeurige volgorde in het
|
|
// briefje; de agent hoort er niet van af te hangen.
|
|
var brief = new Uitrolbrief("pc", DateTime.UtcNow, new[]
|
|
{
|
|
new Uitrolpakket(Uitrolsoort.SocketServer, "socket.zip", "aa"),
|
|
new Uitrolpakket(Uitrolsoort.Site, "site.zip", "bb"),
|
|
new Uitrolpakket(Uitrolsoort.Database, "db.zip", "cc")
|
|
});
|
|
|
|
var volgorde = brief.InVolgorde.Select(p => p.Soort).ToList();
|
|
|
|
Assert.True(volgorde[0] == Uitrolsoort.Database, "de databasescripts gaan eerst");
|
|
Assert.True(volgorde[1] == Uitrolsoort.Site, "dan de website");
|
|
Assert.True(volgorde[2] == Uitrolsoort.SocketServer, "en de SocketServer als laatste");
|
|
});
|
|
|
|
runner.Test("Uitrol: een briefje overleeft de reis naar de server", () =>
|
|
{
|
|
// Het briefje gaat als JSON over een doorgegeven schijf naar de agent. Als dat aan de
|
|
// overkant niet meer te lezen is, staat er een uitrol stil op iets doms.
|
|
var brief = new Uitrolbrief("MIJN-PC", new DateTime(2026, 9, 17, 12, 0, 0, DateTimeKind.Utc),
|
|
new[] { new Uitrolpakket(Uitrolsoort.Site, "mijnbesluit.zip", "ABCDEF") });
|
|
|
|
var terug = System.Text.Json.JsonSerializer.Deserialize<Uitrolbrief>(
|
|
System.Text.Json.JsonSerializer.Serialize(brief),
|
|
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
|
|
|
Assert.True(terug is not null, "het briefje is terug te lezen");
|
|
Assert.True(terug!.Van == "MIJN-PC", "van welke pc hij komt");
|
|
Assert.True(terug.Pakketten.Count == 1, "met één pakket erin");
|
|
Assert.True(terug.Pakketten[0].Soort == Uitrolsoort.Site, "van de goede soort");
|
|
Assert.True(terug.Pakketten[0].Sha256 == "ABCDEF", "en met de hash erbij");
|
|
});
|
|
|
|
runner.Test("Uitrol: de hash van een bestand is dezelfde als die van zijn inhoud", () =>
|
|
{
|
|
// De agent rekent de hash van het pakket op schijf uit en legt die naast wat er in
|
|
// het briefje staat. Rekenen die twee kanten anders, dan wordt elk pakket geweigerd
|
|
// en is er geen uitrol meer mogelijk.
|
|
var bytes = System.Text.Encoding.UTF8.GetBytes("een pakket dat niet echt een pakket is");
|
|
var tijdelijk = Path.Combine(Path.GetTempPath(), $"uitrolproef-{Guid.NewGuid():N}.bin");
|
|
|
|
try
|
|
{
|
|
File.WriteAllBytes(tijdelijk, bytes);
|
|
|
|
Assert.True(Uitrolsleutel.HashVanBestand(tijdelijk) == Uitrolsleutel.Hash(bytes),
|
|
"van schijf gelezen levert dezelfde hash op");
|
|
|
|
// En één byte verschil hoort een andere hash te geven - anders vangt hij geen
|
|
// half geschreven zip.
|
|
bytes[0]++;
|
|
File.WriteAllBytes(tijdelijk, bytes);
|
|
Assert.True(Uitrolsleutel.HashVanBestand(tijdelijk) != Uitrolsleutel.Hash(
|
|
System.Text.Encoding.UTF8.GetBytes("een pakket dat niet echt een pakket is")),
|
|
"een gewijzigd bestand valt door de mand");
|
|
}
|
|
finally
|
|
{
|
|
try { File.Delete(tijdelijk); } catch (Exception) { }
|
|
}
|
|
});
|
|
|
|
runner.Test("Uitrol: geheimen uit een apart bestand gaan over de instellingen heen", () =>
|
|
{
|
|
// Het wachtwoord van het uitrolaccount hoort niet in uitrolagent.json: die map wordt
|
|
// bij elke publicatie overschreven, en dan ben je het kwijt zonder dat iemand het
|
|
// zegt - tot het databasedeel omvalt. Daarom een tweede bestand buiten die map.
|
|
//
|
|
// Wat hier bewaakt wordt is het gedrag dat makkelijk fout gaat: alleen de sleutels
|
|
// die in dat tweede bestand staan mogen eroverheen. Zou het hele object worden
|
|
// vervangen, dan zet een geheimenbestand met alleen een wachtwoord al je andere
|
|
// instellingen terug op de standaardwaarden.
|
|
var map = Path.Combine(Path.GetTempPath(), $"uitrolproef-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(map);
|
|
|
|
try
|
|
{
|
|
var hoofd = Path.Combine(map, "uitrolagent.json");
|
|
var geheim = Path.Combine(map, "geheimen.json");
|
|
|
|
File.WriteAllText(hoofd, $$"""
|
|
{
|
|
"Poort": 5311,
|
|
"Scriptmap": "C:\\Eigen\\Scripts",
|
|
"DbGebruiker": "",
|
|
"ExtraConfigFile": {{System.Text.Json.JsonSerializer.Serialize(geheim)}}
|
|
}
|
|
""");
|
|
|
|
File.WriteAllText(geheim, """
|
|
{
|
|
"//": "alleen dit hoort eroverheen te gaan",
|
|
"DbGebruiker": "mijnbesluit_uitrol",
|
|
"DbWachtwoord": "geheim"
|
|
}
|
|
""");
|
|
|
|
var uitkomst = MijnBesluit.Uitrolagent.Agentinstellingen.Lees(hoofd);
|
|
|
|
Assert.True(uitkomst.DbGebruiker == "mijnbesluit_uitrol", "het account komt uit het tweede bestand");
|
|
Assert.True(uitkomst.DbWachtwoord == "geheim", "het wachtwoord ook");
|
|
Assert.True(uitkomst.Poort == 5311, "en de poort uit het eerste blijft staan");
|
|
Assert.True(uitkomst.Scriptmap == @"C:\Eigen\Scripts", "net als de scriptmap");
|
|
|
|
// Staat het tweede bestand er niet, dan verandert er niets en klaagt er niets.
|
|
File.Delete(geheim);
|
|
var zonder = MijnBesluit.Uitrolagent.Agentinstellingen.Lees(hoofd);
|
|
Assert.True(zonder.Poort == 5311 && string.IsNullOrEmpty(zonder.DbGebruiker),
|
|
"zonder geheimenbestand blijft de rest gewoon staan");
|
|
}
|
|
finally
|
|
{
|
|
try { Directory.Delete(map, true); } catch (Exception) { }
|
|
}
|
|
});
|
|
|
|
runner.Test("Uitrol: het venster ziet zelf wat er mee moet", () =>
|
|
{
|
|
// Het venster zet de vinkjes op wat er sinds de vorige uitrol is gewijzigd. Die
|
|
// afhankelijkheden zijn het soort ding dat stil gaat afwijken: verplaatst iemand een
|
|
// klasse van Domain naar Application, of komt er een project bij, dan staat er
|
|
// straks een website uitgerold tegen een SocketServer die iets anders verwacht -
|
|
// zonder dat iemand iets fout heeft gedaan.
|
|
var map = Path.Combine(Path.GetTempPath(), $"uitrolproef-{Guid.NewGuid():N}");
|
|
|
|
try
|
|
{
|
|
foreach (var deel in new[]
|
|
{
|
|
@"src/MijnBesluit.Web", @"src/MijnBesluit.Domain", @"src/MijnBesluit.SocketServer",
|
|
@"src/MijnBesluit.Protocol", @"src/MijnBesluit.App", @"db"
|
|
})
|
|
{
|
|
Directory.CreateDirectory(Path.Combine(map, deel));
|
|
File.WriteAllText(Path.Combine(map, deel, "iets.txt"), "oud");
|
|
}
|
|
|
|
// Alles op oud zetten, en dan doen alsof er net is uitgerold.
|
|
var oud = DateTime.UtcNow.AddDays(-1);
|
|
foreach (var bestand in Directory.GetFiles(map, "*", SearchOption.AllDirectories))
|
|
File.SetLastWriteTimeUtc(bestand, oud);
|
|
|
|
var geheugen = new Proefgeheugen(DateTime.UtcNow.AddHours(-1));
|
|
|
|
var stil = Wijzigingen.Bepaal(map, geheugen);
|
|
Assert.True(stil.Values.All(s => !s.Nodig), "zonder wijzigingen hoeft er niets mee");
|
|
|
|
// Eén bestand in het domein: dat zit in de website én in de SocketServer, maar
|
|
// niet in de database en niet in de app.
|
|
File.SetLastWriteTimeUtc(Path.Combine(map, "src/MijnBesluit.Domain", "iets.txt"), DateTime.UtcNow);
|
|
|
|
var na = Wijzigingen.Bepaal(map, geheugen);
|
|
Assert.True(na["site"].Nodig, "een wijziging in het domein raakt de website");
|
|
Assert.True(na["socketserver"].Nodig, "en de SocketServer");
|
|
Assert.True(!na["database"].Nodig, "maar niet de databasescripts");
|
|
Assert.True(!na["apk"].Nodig, "en niet de app");
|
|
|
|
// Het protocol is de taal tussen server en app; de website praat daar niet over.
|
|
File.SetLastWriteTimeUtc(Path.Combine(map, "src/MijnBesluit.Domain", "iets.txt"), oud);
|
|
File.SetLastWriteTimeUtc(Path.Combine(map, "src/MijnBesluit.Protocol", "iets.txt"), DateTime.UtcNow);
|
|
|
|
var protocol = Wijzigingen.Bepaal(map, geheugen);
|
|
Assert.True(protocol["socketserver"].Nodig && protocol["apk"].Nodig,
|
|
"het protocol raakt de SocketServer en de app");
|
|
Assert.True(!protocol["site"].Nodig, "en niet de website");
|
|
|
|
// Een nieuw databasescript raakt alleen de database.
|
|
File.SetLastWriteTimeUtc(Path.Combine(map, "src/MijnBesluit.Protocol", "iets.txt"), oud);
|
|
File.WriteAllText(Path.Combine(map, "db", "99_nieuw.sql"), "SELECT 1;");
|
|
|
|
var db = Wijzigingen.Bepaal(map, geheugen);
|
|
Assert.True(db["database"].Nodig, "een nieuw script raakt de database");
|
|
Assert.True(!db["site"].Nodig && !db["socketserver"].Nodig && !db["apk"].Nodig,
|
|
"en verder niets");
|
|
|
|
// Wat de compiler zelf maakt telt niet mee - anders staat alles altijd aan.
|
|
Directory.CreateDirectory(Path.Combine(map, "src/MijnBesluit.Web/bin"));
|
|
File.WriteAllText(Path.Combine(map, "src/MijnBesluit.Web/bin", "iets.dll"), "x");
|
|
|
|
var gebouwd = Wijzigingen.Bepaal(map, geheugen);
|
|
Assert.True(!gebouwd["site"].Nodig, "een bestand in bin telt niet mee");
|
|
}
|
|
finally
|
|
{
|
|
try { Directory.Delete(map, true); } catch (Exception) { }
|
|
}
|
|
});
|
|
|
|
runner.Test("Uitrol: de agent kent alleen een vaste lijst opdrachten", () =>
|
|
{
|
|
// Er hoort geen "voer dit uit" bij te zitten. Deze controle is er niet om de code te
|
|
// beproeven maar om de dag te markeren waarop iemand die wél toevoegt.
|
|
Assert.True(Uitrolacties.Alle.Count == 5, "vijf opdrachten, niet meer");
|
|
|
|
foreach (var actie in Uitrolacties.Alle)
|
|
Assert.True(actie.StartsWith("uitrol.", StringComparison.Ordinal),
|
|
$"elke opdracht hoort bij het uitrollen ({actie})");
|
|
|
|
Assert.True(!Uitrolacties.Alle.Any(a => a.Contains("exec") || a.Contains("command") ||
|
|
a.Contains("shell") || a.Contains("script")),
|
|
"geen opdracht die code van buiten uitvoert");
|
|
});
|
|
|
|
runner.Test("Uitrol: de wachtstand schrijft niets rechtstreeks naar het scherm", () =>
|
|
{
|
|
// Het agentvenster is een WPF-programma en heeft geen console. Alles wat de wachtstand
|
|
// met Console.WriteLine zou schrijven, verdwijnt daar spoorloos: je kijkt naar een leeg
|
|
// venster terwijl de uitrol wel degelijk loopt. Dat is precies het soort stilte waar in
|
|
// dit project al twee avonden aan zijn opgegaan.
|
|
//
|
|
// Vandaar deze controle. Elke regel in Wachtstand.cs die Console aanroept moet met
|
|
// // standaardluisteraar zijn gemerkt - en die staan alleen in de terugvaloptie die
|
|
// gebruikt wordt als er geen luisteraar is meegegeven, dus in de console-agent.
|
|
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "src"))) map = map.Parent;
|
|
if (map is null) return; // gebouwd buiten de projectmap
|
|
|
|
var bestand = Path.Combine(map.FullName, "src", "MijnBesluit.Uitrolagent", "Wachtstand.cs");
|
|
if (!File.Exists(bestand)) return;
|
|
|
|
var regels = File.ReadAllLines(bestand);
|
|
var mis = new List<string>();
|
|
|
|
for (var i = 0; i < regels.Length; i++)
|
|
{
|
|
var regel = regels[i];
|
|
if (!regel.Contains("Console.", StringComparison.Ordinal)) continue;
|
|
if (regel.TrimStart().StartsWith("///", StringComparison.Ordinal)) continue;
|
|
if (regel.TrimStart().StartsWith("//", StringComparison.Ordinal)) continue;
|
|
if (regel.Contains("// standaardluisteraar", StringComparison.Ordinal)) continue;
|
|
|
|
mis.Add($"regel {i + 1}: {regel.Trim()}");
|
|
}
|
|
|
|
Assert.True(mis.Count == 0,
|
|
"de wachtstand mag alleen via de meegegeven luisteraar praten, anders ziet " +
|
|
"het agentvenster niets: " + string.Join(" | ", mis));
|
|
});
|
|
|
|
runner.Test("Geen projectbestand met een onmogelijk commentaar", () =>
|
|
{
|
|
// Twee streepjes achter elkaar mogen niet in een XML-commentaar. In deze projectmap is
|
|
// dat geen theorie: de uitleg bij een csproj noemt graag de schakelaars van een
|
|
// programma, en die beginnen nu eenmaal met twee streepjes. Het gevolg is MSB4025 - het
|
|
// projectbestand laadt niet eens, dus je krijgt geen foutmelding over je code maar over
|
|
// je commentaar, en dat op het moment dat je wilde publiceren.
|
|
//
|
|
// Deze controle leest elk projectbestand als XML. Dat vangt hetzelfde af als MSBuild,
|
|
// en meteen ook een vergeten sluittag.
|
|
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "src"))) map = map.Parent;
|
|
if (map is null) return; // gebouwd buiten de projectmap
|
|
|
|
var bestanden = new List<string>();
|
|
|
|
foreach (var onder in new[] { "src", "tests" })
|
|
{
|
|
var deel = Path.Combine(map.FullName, onder);
|
|
if (Directory.Exists(deel))
|
|
bestanden.AddRange(Directory.GetFiles(deel, "*.csproj", SearchOption.AllDirectories));
|
|
}
|
|
|
|
bestanden.AddRange(Directory.GetFiles(map.FullName, "*.props", SearchOption.TopDirectoryOnly));
|
|
|
|
var mis = new List<string>();
|
|
|
|
foreach (var bestand in bestanden)
|
|
{
|
|
if (bestand.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}") ||
|
|
bestand.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}")) continue;
|
|
|
|
try
|
|
{
|
|
System.Xml.Linq.XDocument.Load(bestand);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
mis.Add($"{Path.GetFileName(bestand)}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
Assert.True(mis.Count == 0, "een projectbestand is geen geldige XML: " + string.Join(" | ", mis));
|
|
});
|
|
}
|
|
}
|