diff --git a/Cmdline/packages.config b/Cmdline/packages.config
index cf073cef53..3cb2f585f3 100644
--- a/Cmdline/packages.config
+++ b/Cmdline/packages.config
@@ -1,5 +1,6 @@
+
\ No newline at end of file
diff --git a/Netkan/CKAN-netkan.csproj b/Netkan/CKAN-netkan.csproj
index baaec58b8d..0b5ce137f1 100644
--- a/Netkan/CKAN-netkan.csproj
+++ b/Netkan/CKAN-netkan.csproj
@@ -52,6 +52,12 @@
..\_build\lib\nuget\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll
+
+ ..\_build\lib\nuget\NJsonSchema.10.0.19\lib\net45\NJsonSchema.dll
+
+
+ ..\_build\lib\nuget\Namotion.Reflection.1.0.5\lib\net45\Namotion.Reflection.dll
+
@@ -131,6 +137,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Netkan/Model/Metadata.cs b/Netkan/Model/Metadata.cs
index c3bc929c0a..8e2ef10806 100644
--- a/Netkan/Model/Metadata.cs
+++ b/Netkan/Model/Metadata.cs
@@ -15,14 +15,13 @@ internal sealed class Metadata
private readonly JObject _json;
- // FIXME: Alignment
- public string Identifier { get { return (string)_json["identifier"]; } }
- public RemoteRef Kref { get; private set; }
- public RemoteRef Vref { get; private set; }
- public ModuleVersion SpecVersion { get; private set; }
- public ModuleVersion Version { get; private set; }
- public Uri Download { get; private set; }
- public DateTime? RemoteTimestamp { get; private set; }
+ public string Identifier { get { return (string)_json["identifier"]; } }
+ public RemoteRef Kref { get; private set; }
+ public RemoteRef Vref { get; private set; }
+ public ModuleVersion SpecVersion { get; private set; }
+ public ModuleVersion Version { get; private set; }
+ public Uri Download { get; private set; }
+ public DateTime? RemoteTimestamp { get; private set; }
public Metadata(JObject json)
{
diff --git a/Netkan/Program.cs b/Netkan/Program.cs
index 1624c1947f..ebbb76ba3c 100644
--- a/Netkan/Program.cs
+++ b/Netkan/Program.cs
@@ -64,7 +64,7 @@ public static int Main(string[] args)
var netkan = ReadNetkan();
Log.Info("Finished reading input");
- new NetkanValidator().Validate(netkan);
+ new NetkanValidator(Options.File).Validate(netkan);
Log.Info("Input successfully passed pre-validation");
var transformer = new NetkanTransformer(
diff --git a/Netkan/Validators/AlphaNumericIdentifierValidator.cs b/Netkan/Validators/AlphaNumericIdentifierValidator.cs
new file mode 100644
index 0000000000..fb13c2fc21
--- /dev/null
+++ b/Netkan/Validators/AlphaNumericIdentifierValidator.cs
@@ -0,0 +1,21 @@
+using System.Text.RegularExpressions;
+using CKAN.NetKAN.Model;
+
+namespace CKAN.NetKAN.Validators
+{
+ internal sealed class AlphaNumericIdentifierValidator : IValidator
+ {
+ public void Validate(Metadata metadata)
+ {
+ if (!alphanumeric.IsMatch(metadata.Identifier))
+ {
+ throw new Kraken("CKAN identifiers must consist only of letters, numbers, and dashes, and must start with a letter or number.");
+ }
+ }
+
+ private static readonly Regex alphanumeric = new Regex(
+ @"^[A-Za-z0-9-]+$",
+ RegexOptions.Compiled
+ );
+ }
+}
diff --git a/Netkan/Validators/CkanValidator.cs b/Netkan/Validators/CkanValidator.cs
index a430acea1a..8fd3f5a78e 100644
--- a/Netkan/Validators/CkanValidator.cs
+++ b/Netkan/Validators/CkanValidator.cs
@@ -14,7 +14,9 @@ public CkanValidator(Metadata netkan, IHttpService downloader, IModuleService mo
{
new IsCkanModuleValidator(),
new MatchingIdentifiersValidator(netkan.Identifier),
- new InstallsFilesValidator(downloader, moduleService)
+ new InstallsFilesValidator(downloader, moduleService),
+ new MatchesKnownGameVersionsValidator(),
+ new ObeysCKANSchemaValidator()
};
}
diff --git a/Netkan/Validators/DownloadVersionValidator.cs b/Netkan/Validators/DownloadVersionValidator.cs
new file mode 100644
index 0000000000..3b3e8cf2ce
--- /dev/null
+++ b/Netkan/Validators/DownloadVersionValidator.cs
@@ -0,0 +1,16 @@
+using CKAN.NetKAN.Model;
+
+namespace CKAN.NetKAN.Validators
+{
+ internal sealed class DownloadVersionValidator : IValidator
+ {
+ public void Validate(Metadata metadata)
+ {
+ var json = metadata.Json();
+ if (json.ContainsKey("download") && !json.ContainsKey("version"))
+ {
+ throw new Kraken($"{metadata.Identifier} expects a version when a download url is provided");
+ }
+ }
+ }
+}
diff --git a/Netkan/Validators/InstallValidator.cs b/Netkan/Validators/InstallValidator.cs
new file mode 100644
index 0000000000..a62196ed94
--- /dev/null
+++ b/Netkan/Validators/InstallValidator.cs
@@ -0,0 +1,56 @@
+using Newtonsoft.Json.Linq;
+using CKAN.Versioning;
+using CKAN.NetKAN.Model;
+
+namespace CKAN.NetKAN.Validators
+{
+ internal sealed class InstallValidator : IValidator
+ {
+ public void Validate(Metadata metadata)
+ {
+ var json = metadata.Json();
+ if (json.ContainsKey("install"))
+ {
+ foreach (JObject stanza in json["install"])
+ {
+ string install_to = (string)stanza["install_to"];
+ if (metadata.SpecVersion < v1p2 && install_to.StartsWith("GameData/"))
+ {
+ throw new Kraken("spec_version v1.2+ required for GameData with path");
+ }
+ if (metadata.SpecVersion < v1p12 && install_to.StartsWith("Ships/"))
+ {
+ throw new Kraken("spec_version v1.12+ required to install to Ships/ with path");
+ }
+ if (metadata.SpecVersion < v1p16 && install_to.StartsWith("Ships/@thumbs"))
+ {
+ throw new Kraken("spec_version v1.16+ required to install to Ships/@thumbs with path");
+ }
+ if (metadata.SpecVersion < v1p4 && stanza.ContainsKey("find"))
+ {
+ throw new Kraken("spec_version v1.4+ required for install with 'find'");
+ }
+ if (metadata.SpecVersion < v1p10 && stanza.ContainsKey("find_regexp"))
+ {
+ throw new Kraken("spec_version v1.10+ required for install with 'find_regexp'");
+ }
+ if (metadata.SpecVersion < v1p16 && stanza.ContainsKey("find_matches_files"))
+ {
+ throw new Kraken("spec_version v1.16+ required for 'find_matches_files'");
+ }
+ if (metadata.SpecVersion < v1p18 && stanza.ContainsKey("as"))
+ {
+ throw new Kraken("spec_version v1.18+ required for 'as'");
+ }
+ }
+ }
+ }
+
+ private static readonly ModuleVersion v1p2 = new ModuleVersion("v1.2");
+ private static readonly ModuleVersion v1p4 = new ModuleVersion("v1.4");
+ private static readonly ModuleVersion v1p10 = new ModuleVersion("v1.10");
+ private static readonly ModuleVersion v1p12 = new ModuleVersion("v1.12");
+ private static readonly ModuleVersion v1p16 = new ModuleVersion("v1.16");
+ private static readonly ModuleVersion v1p18 = new ModuleVersion("v1.18");
+ }
+}
diff --git a/Netkan/Validators/KrefDownloadMutexValidator.cs b/Netkan/Validators/KrefDownloadMutexValidator.cs
new file mode 100644
index 0000000000..d941bcff0a
--- /dev/null
+++ b/Netkan/Validators/KrefDownloadMutexValidator.cs
@@ -0,0 +1,20 @@
+using CKAN.NetKAN.Model;
+
+namespace CKAN.NetKAN.Validators
+{
+ internal sealed class KrefDownloadMutexValidator : IValidator
+ {
+ public void Validate(Metadata metadata)
+ {
+ var json = metadata.Json();
+ if (json.ContainsKey("download") && json.ContainsKey("$kref"))
+ {
+ throw new Kraken($"{metadata.Identifier} has a $kref and a download field, this is likely incorrect");
+ }
+ if (!json.ContainsKey("download") && !json.ContainsKey("$kref"))
+ {
+ throw new Kraken($"{metadata.Identifier} has no $kref field, this is required when no download url is specified");
+ }
+ }
+ }
+}
diff --git a/Netkan/Validators/LicensesValidator.cs b/Netkan/Validators/LicensesValidator.cs
new file mode 100644
index 0000000000..eb444a55c8
--- /dev/null
+++ b/Netkan/Validators/LicensesValidator.cs
@@ -0,0 +1,60 @@
+using System.Text.RegularExpressions;
+using Newtonsoft.Json.Linq;
+using CKAN.Versioning;
+using CKAN.NetKAN.Model;
+
+namespace CKAN.NetKAN.Validators
+{
+ internal sealed class LicensesValidator : IValidator
+ {
+ public void Validate(Metadata metadata)
+ {
+ var json = metadata.Json();
+ JArray licenses = !json.ContainsKey("license") ? null
+ : json["license"] is JArray
+ ? (JArray)json["license"]
+ : new JArray() { json["license"] };
+ if (licenses != null)
+ {
+ foreach (var lic in licenses)
+ {
+ if (metadata.SpecVersion < v1p2 && (string)lic == "WTFPL")
+ {
+ throw new Kraken("spec_version v1.2+ required for license 'WTFPL'");
+ }
+ if (metadata.SpecVersion < v1p18 && (string)lic == "Unlicense")
+ {
+ throw new Kraken("spec_version v1.18+ required for license 'Unlicense'");
+ }
+ }
+ }
+ var kref = (string)json["$kref"] ?? "";
+ if (!metanetkan.IsMatch(kref) && !json.ContainsKey("x_netkan_license_ok"))
+ {
+ if (licenses == null || licenses.Count < 1)
+ {
+ throw new Kraken("License should match spec. Set `x_netkan_license_ok` to supress");
+ }
+ else foreach (var lic in licenses)
+ {
+ try
+ {
+ // This will throw BadMetadataKraken if the license isn't known
+ new CKAN.License((string)lic);
+ }
+ catch
+ {
+ throw new Kraken($"License {lic} should match spec. Set `x_netkan_license_ok` to supress");
+ }
+ }
+ }
+ }
+
+ private static readonly Regex metanetkan = new Regex(
+ @"^#/ckan/netkan/",
+ RegexOptions.Compiled
+ );
+ private static readonly ModuleVersion v1p2 = new ModuleVersion("v1.2");
+ private static readonly ModuleVersion v1p18 = new ModuleVersion("v1.18");
+ }
+}
diff --git a/Netkan/Validators/MatchesKnownGameVersionsValidator.cs b/Netkan/Validators/MatchesKnownGameVersionsValidator.cs
new file mode 100644
index 0000000000..b26d50054b
--- /dev/null
+++ b/Netkan/Validators/MatchesKnownGameVersionsValidator.cs
@@ -0,0 +1,19 @@
+using CKAN.GameVersionProviders;
+using CKAN.Versioning;
+using CKAN.NetKAN.Model;
+
+namespace CKAN.NetKAN.Validators
+{
+ internal sealed class MatchesKnownGameVersionsValidator : IValidator
+ {
+ public void Validate(Metadata metadata)
+ {
+ var mod = CkanModule.FromJson(metadata.Json().ToString());
+ var knownVersions = new KspBuildMap(new Win32Registry()).KnownVersions;
+ if (!mod.IsCompatibleKSP(new KspVersionCriteria(null, knownVersions)))
+ {
+ throw new Kraken($"{metadata.Identifier} doesn't match any valid game version");
+ }
+ }
+ }
+}
diff --git a/Netkan/Validators/NetkanValidator.cs b/Netkan/Validators/NetkanValidator.cs
index b480860953..73be2d988d 100644
--- a/Netkan/Validators/NetkanValidator.cs
+++ b/Netkan/Validators/NetkanValidator.cs
@@ -1,3 +1,4 @@
+using System.IO;
using System.Collections.Generic;
using CKAN.NetKAN.Model;
@@ -7,12 +8,23 @@ internal sealed class NetkanValidator : IValidator
{
private readonly List _validators;
- public NetkanValidator()
+ public NetkanValidator(string filename)
{
- _validators = new List
+ _validators = new List()
{
+ new SpecVersionFormatValidator(),
new HasIdentifierValidator(),
- new KrefValidator()
+ new KrefValidator(),
+ new MatchingIdentifiersValidator(Path.GetFileNameWithoutExtension(filename)),
+ new AlphaNumericIdentifierValidator(),
+ new RelationshipsValidator(),
+ new LicensesValidator(),
+ new KrefDownloadMutexValidator(),
+ new DownloadVersionValidator(),
+ new OverrideValidator(),
+ new VersionStrictValidator(),
+ new ReplacedByValidator(),
+ new InstallValidator(),
};
}
diff --git a/Netkan/Validators/ObeysCKANSchemaValidator.cs b/Netkan/Validators/ObeysCKANSchemaValidator.cs
new file mode 100644
index 0000000000..4643ed0ec1
--- /dev/null
+++ b/Netkan/Validators/ObeysCKANSchemaValidator.cs
@@ -0,0 +1,36 @@
+using System.IO;
+using System.Reflection;
+using System.Collections.Generic;
+using System.Linq;
+using NJsonSchema;
+using CKAN.NetKAN.Model;
+
+namespace CKAN.NetKAN.Validators
+{
+ internal sealed class ObeysCKANSchemaValidator : IValidator
+ {
+ static ObeysCKANSchemaValidator()
+ {
+ var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(embeddedSchema);
+ using (var reader = new StreamReader(resourceStream))
+ {
+ schema = JsonSchema.FromJsonAsync(reader.ReadToEnd()).Result;
+ }
+ }
+
+ public void Validate(Metadata metadata)
+ {
+ var errors = schema.Validate(metadata.Json());
+ if (errors.Any())
+ {
+ string msg = errors
+ .Select(err => $"{err.Path}: {err.Kind}")
+ .Aggregate((a, b) => $"{a}\r\n{b}");
+ throw new Kraken($"Schema validation failed: {msg}");
+ }
+ }
+
+ private static readonly JsonSchema schema;
+ private const string embeddedSchema = "CKAN.NetKAN.CKAN.schema";
+ }
+}
diff --git a/Netkan/Validators/OverrideValidator.cs b/Netkan/Validators/OverrideValidator.cs
new file mode 100644
index 0000000000..9b90fa919d
--- /dev/null
+++ b/Netkan/Validators/OverrideValidator.cs
@@ -0,0 +1,32 @@
+using Newtonsoft.Json.Linq;
+using CKAN.NetKAN.Model;
+
+namespace CKAN.NetKAN.Validators
+{
+ internal sealed class OverrideValidator : IValidator
+ {
+ public void Validate(Metadata metadata)
+ {
+ var json = metadata.Json();
+ var overrides = json["x_netkan_override"];
+ if (overrides != null)
+ {
+ if (!(overrides is JArray))
+ {
+ throw new Kraken("Netkan overrides require an array");
+ }
+ foreach (JObject ovr in overrides)
+ {
+ if (!ovr.ContainsKey("version"))
+ {
+ throw new Kraken("Netkan overrides require a version");
+ }
+ if (!ovr.ContainsKey("delete") && !ovr.ContainsKey("override"))
+ {
+ throw new Kraken("Netkan overrides require a delete or override section");
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Netkan/Validators/RelationshipsValidator.cs b/Netkan/Validators/RelationshipsValidator.cs
new file mode 100644
index 0000000000..3d69af77b6
--- /dev/null
+++ b/Netkan/Validators/RelationshipsValidator.cs
@@ -0,0 +1,62 @@
+using System.Text.RegularExpressions;
+using Newtonsoft.Json.Linq;
+using CKAN.Versioning;
+using CKAN.NetKAN.Model;
+
+namespace CKAN.NetKAN.Validators
+{
+ internal sealed class RelationshipsValidator : IValidator
+ {
+ public void Validate(Metadata metadata)
+ {
+ var json = metadata.Json();
+ foreach (string relName in relProps)
+ {
+ if (json.ContainsKey(relName))
+ {
+ foreach (JObject rel in json[relName])
+ {
+ if (rel.ContainsKey("any_of"))
+ {
+ if (metadata.SpecVersion < v1p26)
+ {
+ throw new Kraken("spec_version v1.26+ required for 'any_of'");
+ }
+ foreach (JObject opt in rel["any_of"])
+ {
+ string name = (string)opt["name"];
+ if (!alphanumeric.IsMatch(name))
+ {
+ throw new Kraken($"{name} in {relName} any_of is not a valid CKAN identifier");
+ }
+ }
+ }
+ else
+ {
+ string name = (string)rel["name"];
+ if (!alphanumeric.IsMatch(name))
+ {
+ throw new Kraken($"{name} in {relName} is not a valid CKAN identifier");
+ }
+ }
+ }
+ }
+ }
+
+ }
+
+ private static readonly string[] relProps = new string[]
+ {
+ "depends",
+ "recommends",
+ "suggests",
+ "conflicts",
+ "supports"
+ };
+ private static readonly Regex alphanumeric = new Regex(
+ @"^[A-Za-z0-9-]+$",
+ RegexOptions.Compiled
+ );
+ private static readonly ModuleVersion v1p26 = new ModuleVersion("v1.26");
+ }
+}
diff --git a/Netkan/Validators/ReplacedByValidator.cs b/Netkan/Validators/ReplacedByValidator.cs
new file mode 100644
index 0000000000..a1c5dac9a4
--- /dev/null
+++ b/Netkan/Validators/ReplacedByValidator.cs
@@ -0,0 +1,19 @@
+using CKAN.Versioning;
+using CKAN.NetKAN.Model;
+
+namespace CKAN.NetKAN.Validators
+{
+ internal sealed class ReplacedByValidator : IValidator
+ {
+ public void Validate(Metadata metadata)
+ {
+ var json = metadata.Json();
+ if (metadata.SpecVersion < v1p26 && json.ContainsKey("replaced_by"))
+ {
+ throw new Kraken("spec_version v1.26+ required for 'replaced_by'");
+ }
+ }
+
+ private static readonly ModuleVersion v1p26 = new ModuleVersion("v1.26");
+ }
+}
diff --git a/Netkan/Validators/SpecVersionFormatValidator.cs b/Netkan/Validators/SpecVersionFormatValidator.cs
new file mode 100644
index 0000000000..504cd97304
--- /dev/null
+++ b/Netkan/Validators/SpecVersionFormatValidator.cs
@@ -0,0 +1,23 @@
+using System.Text.RegularExpressions;
+using CKAN.NetKAN.Model;
+
+namespace CKAN.NetKAN.Validators
+{
+ internal sealed class SpecVersionFormatValidator : IValidator
+ {
+ public void Validate(Metadata metadata)
+ {
+ var json = metadata.Json();
+ if (json["spec_version"] == null
+ || !specVersionFormat.IsMatch((string)json["spec_version"]))
+ {
+ throw new Kraken("spec version must be 1 or in the 'vX.X' format");
+ }
+ }
+
+ private static readonly Regex specVersionFormat = new Regex(
+ @"^1$|^v\d\.\d\d?$",
+ RegexOptions.Compiled
+ );
+ }
+}
diff --git a/Netkan/Validators/VersionStrictValidator.cs b/Netkan/Validators/VersionStrictValidator.cs
new file mode 100644
index 0000000000..5b3be7cf92
--- /dev/null
+++ b/Netkan/Validators/VersionStrictValidator.cs
@@ -0,0 +1,19 @@
+using CKAN.Versioning;
+using CKAN.NetKAN.Model;
+
+namespace CKAN.NetKAN.Validators
+{
+ internal sealed class VersionStrictValidator : IValidator
+ {
+ public void Validate(Metadata metadata)
+ {
+ var json = metadata.Json();
+ if (metadata.SpecVersion < v1p16 && json.ContainsKey("ksp_version_strict"))
+ {
+ throw new Kraken("spec_version v1.16+ required for 'ksp_version_strict'");
+ }
+ }
+
+ private static readonly ModuleVersion v1p16 = new ModuleVersion("v1.16");
+ }
+}
diff --git a/Netkan/packages.config b/Netkan/packages.config
index a161ad7051..a89dcc8725 100644
--- a/Netkan/packages.config
+++ b/Netkan/packages.config
@@ -4,4 +4,6 @@
-
\ No newline at end of file
+
+
+
diff --git a/Tests/NetKAN/Validators/AlphaNumericIdentifierValidatorTests.cs b/Tests/NetKAN/Validators/AlphaNumericIdentifierValidatorTests.cs
new file mode 100644
index 0000000000..7a715231e8
--- /dev/null
+++ b/Tests/NetKAN/Validators/AlphaNumericIdentifierValidatorTests.cs
@@ -0,0 +1,44 @@
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using CKAN.NetKAN.Model;
+using CKAN.NetKAN.Validators;
+
+namespace Tests.NetKAN.Validators
+{
+ [TestFixture]
+ public sealed class AlphaNumericIdentifierValidatorTests
+ {
+ [Test,
+ TestCase("Normal"),
+ TestCase("Has-Dash"),
+ TestCase("ALLUPPER"),
+ TestCase("alllower"),
+ ]
+ public void Validate_ValidIdentifier_DoesNotThrow(string identifier)
+ {
+ Assert.DoesNotThrow(() => TryId(identifier));
+ }
+
+ [Test,
+ TestCase("#HashTag"),
+ TestCase("Under_Score"),
+ TestCase("Dot.Dot"),
+ ]
+ public void Validate_BadIdentifier_Throws(string identifier)
+ {
+ Assert.Throws(() => TryId(identifier));
+ }
+
+ private void TryId(string identifier)
+ {
+ // Arrange
+ var json = new JObject();
+ json["spec_version"] = 1;
+ json["identifier"] = identifier;
+
+ // Act
+ var val = new AlphaNumericIdentifierValidator();
+ val.Validate(new Metadata(json));
+ }
+ }
+}
diff --git a/Tests/NetKAN/Validators/CkanValidatorTests.cs b/Tests/NetKAN/Validators/CkanValidatorTests.cs
index ffd2b2d978..e4389fdb69 100644
--- a/Tests/NetKAN/Validators/CkanValidatorTests.cs
+++ b/Tests/NetKAN/Validators/CkanValidatorTests.cs
@@ -18,6 +18,9 @@ public void SetUp()
{
ValidCkan["spec_version"] = 1;
ValidCkan["identifier"] = "AwesomeMod";
+ ValidCkan["name"] = "Awesome Mod";
+ ValidCkan["abstract"] = "A great mod";
+ ValidCkan["license"] = "GPL-3.0";
ValidCkan["version"] = "1.0.0";
ValidCkan["download"] = "https://www.awesome-mod.example/AwesomeMod.zip";
}
@@ -32,11 +35,14 @@ public void DoesNotThrowOnValidCkan()
mModuleService.Setup(i => i.HasInstallableFiles(It.IsAny(), It.IsAny()))
.Returns(true);
- var netkan = new JObject();
- netkan["spec_version"] = 1;
- netkan["identifier"] = "AwesomeMod";
+ var ckan = new JObject();
+ ckan["spec_version"] = 1;
+ ckan["identifier"] = "AwesomeMod";
+ ckan["name"] = "Awesome Mod";
+ ckan["abstract"] = "A great mod";
+ ckan["license"] = "GPL-3.0";
- var sut = new CkanValidator(new Metadata(netkan), mHttp.Object, mModuleService.Object);
+ var sut = new CkanValidator(new Metadata(ckan), mHttp.Object, mModuleService.Object);
var json = (JObject)ValidCkan.DeepClone();
// Act
diff --git a/Tests/NetKAN/Validators/DownloadVersionValidatorTests.cs b/Tests/NetKAN/Validators/DownloadVersionValidatorTests.cs
new file mode 100644
index 0000000000..3dedcee40a
--- /dev/null
+++ b/Tests/NetKAN/Validators/DownloadVersionValidatorTests.cs
@@ -0,0 +1,49 @@
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using CKAN.NetKAN.Model;
+using CKAN.NetKAN.Validators;
+
+namespace Tests.NetKAN.Validators
+{
+ [TestFixture]
+ public sealed class DownloadVersionValidatorTests
+ {
+ [Test,
+ TestCase("https://mysite.org/mymod.zip", "1.2.3"),
+ TestCase(null, null),
+ TestCase(null, "4.5.6"),
+ ]
+ public void Validate_NoDownloadWithoutVersion_DoesNotThrow(string download, string version)
+ {
+ Assert.DoesNotThrow(() => TryDownloadVersion(download, version));
+ }
+
+ [Test,
+ TestCase("https://mysite.org/mymod.zip", null),
+ ]
+ public void Validate_DownloadWithoutVersion_Throws(string download, string version)
+ {
+ Assert.Throws(() => TryDownloadVersion(download, version));
+ }
+
+ private void TryDownloadVersion(string download, string version)
+ {
+ // Arrange
+ var json = new JObject();
+ json["spec_version"] = 1;
+ json["identifier"] = "AwesomeMod";
+ if (download != null)
+ {
+ json["download"] = download;
+ }
+ if (version != null)
+ {
+ json["version"] = version;
+ }
+
+ // Act
+ var val = new DownloadVersionValidator();
+ val.Validate(new Metadata(json));
+ }
+ }
+}
diff --git a/Tests/NetKAN/Validators/InstallValidatorTests.cs b/Tests/NetKAN/Validators/InstallValidatorTests.cs
new file mode 100644
index 0000000000..17f5fca49f
--- /dev/null
+++ b/Tests/NetKAN/Validators/InstallValidatorTests.cs
@@ -0,0 +1,86 @@
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using CKAN.NetKAN.Model;
+using CKAN.NetKAN.Validators;
+
+namespace Tests.NetKAN.Validators
+{
+ [TestFixture]
+ public sealed class InstallValidatorTests
+ {
+ [Test,
+ TestCase("v1.2", "GameData/something"),
+ TestCase("v1.12", "Ships/something"),
+ TestCase("v1.16", "Ships/@thumbs"),
+ ]
+ public void Validate_GoodSpecVersionInstallTo_DoesNotThrow(string spec_version, string install_to)
+ {
+ Assert.DoesNotThrow(() => TryInstallTo(spec_version, install_to));
+ }
+
+ [Test,
+ TestCase("1", "GameData/something"),
+ TestCase("v1.11", "Ships/something"),
+ TestCase("v1.15", "Ships/@thumbs"),
+ ]
+ public void Validate_BadSpecVersionInstallTo_Throws(string spec_version, string install_to)
+ {
+ Assert.Throws(() => TryInstallTo(spec_version, install_to));
+ }
+
+ [Test,
+ TestCase("v1.4", "{ \"find\": \"something\", \"install_to\": \"GameData\" }"),
+ TestCase("v1.10", "{ \"find_regexp\": \"something\", \"install_to\": \"GameData\" }"),
+ TestCase("v1.16", "{ \"find_matches_files\": true, \"find\": \"something\", \"install_to\": \"GameData\" }"),
+ TestCase("v1.18", "{ \"as\": \"somethingelse\", \"find\": \"something\", \"install_to\": \"GameData\" }"),
+ ]
+ public void Validate_GoodSpecVersionInstallStanza_DoesNotThrow(string spec_version, string install_stanza)
+ {
+ Assert.DoesNotThrow(() => TryInstallStanza(spec_version, install_stanza));
+ }
+
+ [Test,
+ TestCase("v1.3", "{ \"find\": \"something\", \"install_to\": \"GameData\" }"),
+ TestCase("v1.9", "{ \"find_regexp\": \"something\", \"install_to\": \"GameData\" }"),
+ TestCase("v1.15", "{ \"find_matches_files\": true, \"find\": \"something\", \"install_to\": \"GameData\" }"),
+ TestCase("v1.17", "{ \"as\": \"somethingelse\", \"find\": \"something\", \"install_to\": \"GameData\" }"),
+ ]
+ public void Validate_BadSpecVersionInstallStanza_Throws(string spec_version, string install_stanza)
+ {
+ Assert.Throws(() => TryInstallStanza(spec_version, install_stanza));
+ }
+
+ private void TryInstallTo(string spec_version, string install_to)
+ {
+ // Arrange
+ var json = new JObject();
+ json["spec_version"] = spec_version;
+ json["identifier"] = "AwesomeMod";
+ json["install"] = new JArray() {
+ new JObject() {
+ { "file", "something" },
+ { "install_to", install_to }
+ }
+ };
+
+ // Act
+ var val = new InstallValidator();
+ val.Validate(new Metadata(json));
+ }
+
+ private void TryInstallStanza(string spec_version, string install_stanza)
+ {
+ // Arrange
+ var json = new JObject();
+ json["spec_version"] = spec_version;
+ json["identifier"] = "AwesomeMod";
+ json["install"] = new JArray() {
+ JObject.Parse(install_stanza)
+ };
+
+ // Act
+ var val = new InstallValidator();
+ val.Validate(new Metadata(json));
+ }
+ }
+}
diff --git a/Tests/NetKAN/Validators/KrefDownloadMutexValidatorTests.cs b/Tests/NetKAN/Validators/KrefDownloadMutexValidatorTests.cs
new file mode 100644
index 0000000000..b13e78df0c
--- /dev/null
+++ b/Tests/NetKAN/Validators/KrefDownloadMutexValidatorTests.cs
@@ -0,0 +1,49 @@
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using CKAN.NetKAN.Model;
+using CKAN.NetKAN.Validators;
+
+namespace Tests.NetKAN.Validators
+{
+ [TestFixture]
+ public sealed class KrefDownloadMutexValidatorTests
+ {
+ [Test,
+ TestCase(null, "https://mysite.org/mymod.zip"),
+ TestCase("#/ckan/spacedock/1", null),
+ ]
+ public void Validate_OneWithoutTheOther_DoesNotThrow(string kref, string download)
+ {
+ Assert.DoesNotThrow(() => TryKrefDownload(kref, download));
+ }
+
+ [Test,
+ TestCase(null, null),
+ TestCase("#/ckan/spacedock/1", "https://mysite.org/mymod.zip"),
+ ]
+ public void Validate_NeitherOrBoth_Throws(string kref, string download)
+ {
+ Assert.Throws(() => TryKrefDownload(kref, download));
+ }
+
+ private void TryKrefDownload(string kref, string download)
+ {
+ // Arrange
+ var json = new JObject();
+ json["spec_version"] = 1;
+ json["identifier"] = "AwesomeMod";
+ if (kref != null)
+ {
+ json["$kref"] = kref;
+ }
+ if (download != null)
+ {
+ json["download"] = download;
+ }
+
+ // Act
+ var val = new KrefDownloadMutexValidator();
+ val.Validate(new Metadata(json));
+ }
+ }
+}
diff --git a/Tests/NetKAN/Validators/LicensesValidatorTests.cs b/Tests/NetKAN/Validators/LicensesValidatorTests.cs
new file mode 100644
index 0000000000..4958716d67
--- /dev/null
+++ b/Tests/NetKAN/Validators/LicensesValidatorTests.cs
@@ -0,0 +1,46 @@
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using CKAN.NetKAN.Model;
+using CKAN.NetKAN.Validators;
+
+namespace Tests.NetKAN.Validators
+{
+ [TestFixture]
+ public sealed class LicensesValidatorTests
+ {
+ [Test,
+ TestCase("v1.2", @"""WTFPL"""),
+ TestCase("v1.18", @"""Unlicense"""),
+ TestCase("v1.4", @"[ ""GPL-3.0"", ""MIT"" ]"),
+ ]
+ public void Validate_GoodSpecVersionLicense_DoesNotThrow(string spec_version, string license)
+ {
+ Assert.DoesNotThrow(() => TryLicense(spec_version, license));
+ }
+
+ [Test,
+ TestCase("1", @"""WTFPL"""),
+ TestCase("v1.17", @"""Unlicense"""),
+ TestCase("v1.4", @"""NotARealLicense"""),
+ TestCase("v1.4", @"[ ""GPL-3.0"", ""Unlicense"" ]"),
+ TestCase("v1.4", @"[ ""GPL-3.0"", ""NotARealLicense"" ]"),
+ ]
+ public void Validate_BadSpecVersionLicense_Throws(string spec_version, string license)
+ {
+ Assert.Throws(() => TryLicense(spec_version, license));
+ }
+
+ private void TryLicense(string spec_version, string license)
+ {
+ // Arrange
+ var json = new JObject();
+ json["spec_version"] = spec_version;
+ json["identifier"] = "AwesomeMod";
+ json["license"] = JToken.Parse(license);
+
+ // Act
+ var val = new LicensesValidator();
+ val.Validate(new Metadata(json));
+ }
+ }
+}
diff --git a/Tests/NetKAN/Validators/MatchesKnownGameVersionsValidatorTests.cs b/Tests/NetKAN/Validators/MatchesKnownGameVersionsValidatorTests.cs
new file mode 100644
index 0000000000..fecd7f4c5c
--- /dev/null
+++ b/Tests/NetKAN/Validators/MatchesKnownGameVersionsValidatorTests.cs
@@ -0,0 +1,62 @@
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using CKAN.NetKAN.Model;
+using CKAN.NetKAN.Validators;
+
+namespace Tests.NetKAN.Validators
+{
+ [TestFixture]
+ public sealed class MatchesKnownGameVersionsValidatorTests
+ {
+ [Test,
+ TestCase(null, null, null),
+ TestCase("0.90", null, null),
+ TestCase("1.2", null, null),
+ TestCase("1.7.2", null, null),
+ TestCase(null, "1.3.1", "1.3.1"),
+ TestCase(null, "1.1.10", "1.2.20"),
+ TestCase(null, "1.6", "1.7"),
+ ]
+ public void Validate_KnownVersions_DoesNotThrow(string ksp_version, string ksp_version_min, string ksp_version_max)
+ {
+ Assert.DoesNotThrow(() => TryVersion(ksp_version, ksp_version_min, ksp_version_max));
+ }
+
+ [Test,
+ TestCase("0.26.0", null, null),
+ TestCase("1.4.99", null, null),
+ TestCase(null, "1.0.10", "1.0.99"),
+ TestCase(null, "1.99.0", "1.99.99"),
+ ]
+ public void Validate_UnknownVersions_Throws(string ksp_version, string ksp_version_min, string ksp_version_max)
+ {
+ Assert.Throws(() => TryVersion(ksp_version, ksp_version_min, ksp_version_max));
+ }
+
+ private void TryVersion(string ksp_version, string ksp_version_min, string ksp_version_max)
+ {
+ // Arrange
+ var json = new JObject();
+ json["spec_version"] = 1;
+ json["version"] = "1.0";
+ json["identifier"] = "AwesomeMod";
+ json["download"] = "https://mysite.org/mymod.zip";
+ if (ksp_version != null)
+ {
+ json["ksp_version"] = ksp_version;
+ }
+ if (ksp_version_min != null)
+ {
+ json["ksp_version_min"] = ksp_version_min;
+ }
+ if (ksp_version_max != null)
+ {
+ json["ksp_version_max"] = ksp_version_max;
+ }
+
+ // Act
+ var val = new MatchesKnownGameVersionsValidator();
+ val.Validate(new Metadata(json));
+ }
+ }
+}
diff --git a/Tests/NetKAN/Validators/NetkanValidatorTests.cs b/Tests/NetKAN/Validators/NetkanValidatorTests.cs
index 7ebd27ee84..31538f5adf 100644
--- a/Tests/NetKAN/Validators/NetkanValidatorTests.cs
+++ b/Tests/NetKAN/Validators/NetkanValidatorTests.cs
@@ -12,10 +12,12 @@ public sealed class NetkanValidatorTests
public void DoesNotThrowWhenIdentifierPresent()
{
// Arrange
- var sut = new NetkanValidator();
+ var sut = new NetkanValidator("AwesomeMod.netkan");
var json = new JObject();
json["spec_version"] = 1;
json["identifier"] = "AwesomeMod";
+ json["$kref"] = "#/ckan/github/AwesomeModder/AwesomeMod";
+ json["license"] = "GPL-3.0";
// Act
TestDelegate act = () => sut.Validate(new Metadata(json));
@@ -30,7 +32,7 @@ public void DoesNotThrowWhenIdentifierPresent()
public void DoesThrowWhenIdentifierMissing()
{
// Arrange
- var sut = new NetkanValidator();
+ var sut = new NetkanValidator("AwesomeMod.netkan");
var json = new JObject();
json["spec_version"] = 1;
diff --git a/Tests/NetKAN/Validators/ObeysCKANSchemaValidatorTests.cs b/Tests/NetKAN/Validators/ObeysCKANSchemaValidatorTests.cs
new file mode 100644
index 0000000000..be90cb2e51
--- /dev/null
+++ b/Tests/NetKAN/Validators/ObeysCKANSchemaValidatorTests.cs
@@ -0,0 +1,82 @@
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using CKAN.NetKAN.Model;
+using CKAN.NetKAN.Validators;
+
+namespace Tests.NetKAN.Validators
+{
+ [TestFixture]
+ public sealed class ObeysCKANSchemaValidatorTests
+ {
+ [Test,
+ TestCase(boringModule),
+ ]
+ public void Validate_Obeys_DoesNotThrow(string json)
+ {
+ Assert.DoesNotThrow(() => TryModule(json));
+ }
+
+ [Test,
+ TestCase(boringModule, "spec_version"),
+ TestCase(boringModule, "identifier"),
+ TestCase(boringModule, "name"),
+ TestCase(boringModule, "version"),
+ TestCase(boringModule, "license"),
+ TestCase(boringModule, "download"),
+ ]
+ public void Validate_MissingProperty_Throws(string json, string removeProperty)
+ {
+ Assert.Throws(() => TryModule(json, removeProperty));
+ }
+
+ [Test,
+ TestCase(boringModule, @"[ ""en-us"" ]"),
+ ]
+ public void Validate_UniqueLocalizations_DoesNotThrow(string json, string localizations)
+ {
+ Assert.DoesNotThrow(
+ () => TryModule(json, null, "localizations", JArray.Parse(localizations))
+ );
+ }
+
+ [Test,
+ TestCase(boringModule, @"[ ""en-us"", ""en-us"" ]"),
+ TestCase(boringModule, @"[ ""en-us"", ""es-es"", ""en-us"" ]"),
+ TestCase(boringModule, @"[ ""en-us"", ""de-de"", ""fr-fr"", ""de-de"" ]"),
+ ]
+ public void Validate_DuplicateLocalizations_Throws(string json, string localizations)
+ {
+ Assert.Throws(
+ () => TryModule(json, null, "localizations", JArray.Parse(localizations))
+ );
+ }
+
+ private void TryModule(string json, string removeProperty = null, string addProperty = null, JToken addPropertyValue = null)
+ {
+ // Arrange
+ var jObj = JObject.Parse(json);
+ if (removeProperty != null)
+ {
+ jObj.Remove(removeProperty);
+ }
+ if (addProperty != null && addPropertyValue != null)
+ {
+ jObj[addProperty] = addPropertyValue;
+ }
+
+ // Act
+ var val = new ObeysCKANSchemaValidator();
+ val.Validate(new Metadata(jObj));
+ }
+
+ private const string boringModule = @"{
+ ""spec_version"": 1,
+ ""identifier"": ""BoringModule"",
+ ""name"": ""Boring Module"",
+ ""abstract"": ""A minimal module that obeys CKAN.schema"",
+ ""version"": ""1.0.0"",
+ ""license"": ""MIT"",
+ ""download"": ""https://mysite.org/mymod.zip""
+ }";
+ }
+}
diff --git a/Tests/NetKAN/Validators/OverrideValidatorTests.cs b/Tests/NetKAN/Validators/OverrideValidatorTests.cs
new file mode 100644
index 0000000000..c1ff577276
--- /dev/null
+++ b/Tests/NetKAN/Validators/OverrideValidatorTests.cs
@@ -0,0 +1,48 @@
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using CKAN.NetKAN.Model;
+using CKAN.NetKAN.Validators;
+
+namespace Tests.NetKAN.Validators
+{
+ [TestFixture]
+ public sealed class OverrideValidatorTests
+ {
+ [Test,
+ TestCase(null),
+ TestCase(@"[ { ""version"": ""1.0"", ""delete"": ""ksp_version"" } ]"),
+ TestCase(@"[ { ""version"": ""1.0"", ""override"": { ""ksp_version"": ""0.90"" } } ]"),
+ ]
+ public void Validate_ValidOverride_DoesNotThrow(string json)
+ {
+ Assert.DoesNotThrow(() => TryOverride(json));
+ }
+
+ [Test,
+ TestCase(@"{ ""version"": ""1.0"", ""delete"": ""ksp_version"" }"),
+ TestCase(@"[ { ""version"": ""1.0"" } ]"),
+ TestCase(@"[ { ""delete"": ""identifier"" } ]"),
+ ]
+ public void Validate_BadOverride_Throws(string json)
+ {
+ Assert.Throws(() => TryOverride(json));
+ }
+
+ private void TryOverride(string ovr)
+ {
+ // Arrange
+ var json = new JObject();
+ json["spec_version"] = 1;
+ json["identifier"] = "AwesomeMod";
+ if (ovr != null)
+ {
+ json["x_netkan_override"] = JToken.Parse(ovr);
+ }
+
+ // Act
+ var val = new OverrideValidator();
+ val.Validate(new Metadata(json));
+ }
+
+ }
+}
diff --git a/Tests/NetKAN/Validators/RelationshipsValidatorTests.cs b/Tests/NetKAN/Validators/RelationshipsValidatorTests.cs
new file mode 100644
index 0000000000..2c7785712a
--- /dev/null
+++ b/Tests/NetKAN/Validators/RelationshipsValidatorTests.cs
@@ -0,0 +1,47 @@
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using CKAN.NetKAN.Model;
+using CKAN.NetKAN.Validators;
+
+namespace Tests.NetKAN.Validators
+{
+ [TestFixture]
+ public sealed class RelationshipsValidatorTests
+ {
+ [Test,
+ TestCase("v1.4", null, null),
+ TestCase("v1.4", "depends", @"[ { ""name"": ""ModuleManager"" } ]"),
+ TestCase("v1.26", "depends", @"[ { ""any_of"": [ { ""name"": ""ModuleManager"" } ] } ]"),
+ ]
+ public void Validate_ValidRelationships_DoesNotThrow(string spec_version, string relationName, string relationValue)
+ {
+ Assert.DoesNotThrow(() => TryRelationships(spec_version, relationName, relationValue));
+ }
+
+ [Test,
+ TestCase("v1.4", "depends", @"[ { ""name"": ""Module Manager"" } ]"),
+ TestCase("v1.25", "depends", @"[ { ""any_of"": [ { ""name"": ""ModuleManager"" } ] } ]"),
+ TestCase("v1.26", "depends", @"[ { ""any_of"": [ { ""name"": ""Module Manager"" } ] } ]"),
+ ]
+ public void Validate_BadRelationships_Throws(string spec_version, string relationName, string relationValue)
+ {
+ Assert.Throws(() => TryRelationships(spec_version, relationName, relationValue));
+ }
+
+ private void TryRelationships(string spec_version, string relationName, string relationValue)
+ {
+ // Arrange
+ var json = new JObject();
+ json["spec_version"] = spec_version;
+ json["identifier"] = "AwesomeMod";
+ if (relationName != null && relationValue != null)
+ {
+ json[relationName] = JToken.Parse(relationValue);
+ }
+
+ // Act
+ var val = new RelationshipsValidator();
+ val.Validate(new Metadata(json));
+ }
+ }
+}
diff --git a/Tests/NetKAN/Validators/ReplacedByValidatorTests.cs b/Tests/NetKAN/Validators/ReplacedByValidatorTests.cs
new file mode 100644
index 0000000000..de50209fb2
--- /dev/null
+++ b/Tests/NetKAN/Validators/ReplacedByValidatorTests.cs
@@ -0,0 +1,44 @@
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using CKAN.NetKAN.Model;
+using CKAN.NetKAN.Validators;
+
+namespace Tests.NetKAN.Validators
+{
+ [TestFixture]
+ public sealed class ReplacedByValidatorTests
+ {
+ [Test,
+ TestCase("v1.4", null),
+ TestCase("v1.26", @"{ ""name"": ""AwesomeModContinued"" }"),
+ ]
+ public void Validate_ValidReplacement_DoesNotThrow(string spec_version, string replacement)
+ {
+ Assert.DoesNotThrow(() => TryRelationships(spec_version, replacement));
+ }
+
+ [Test,
+ TestCase("v1.25", @"{ ""name"": ""AwesomeModContinued"" }"),
+ ]
+ public void Validate_BadReplacement_Throws(string spec_version, string replacement)
+ {
+ Assert.Throws(() => TryRelationships(spec_version, replacement));
+ }
+
+ private void TryRelationships(string spec_version, string replacement)
+ {
+ // Arrange
+ var json = new JObject();
+ json["spec_version"] = spec_version;
+ json["identifier"] = "AwesomeMod";
+ if (replacement != null)
+ {
+ json["replaced_by"] = JToken.Parse(replacement);
+ }
+
+ // Act
+ var val = new ReplacedByValidator();
+ val.Validate(new Metadata(json));
+ }
+ }
+}
diff --git a/Tests/NetKAN/Validators/SpecVersionFormatValidatorTests.cs b/Tests/NetKAN/Validators/SpecVersionFormatValidatorTests.cs
new file mode 100644
index 0000000000..3b99f80920
--- /dev/null
+++ b/Tests/NetKAN/Validators/SpecVersionFormatValidatorTests.cs
@@ -0,0 +1,45 @@
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using CKAN.NetKAN.Model;
+using CKAN.NetKAN.Validators;
+
+namespace Tests.NetKAN.Validators
+{
+ [TestFixture]
+ public sealed class SpecVersionFormatValidatorTests
+ {
+ [Test,
+ TestCase("1"),
+ TestCase("v1.4"),
+ TestCase("v1.26"),
+ ]
+ public void Validate_ValidSpecVersion_DoesNotThrow(string spec_version)
+ {
+ Assert.DoesNotThrow(() => TrySpecVersion(spec_version));
+ }
+
+ [Test,
+ //TestCase(null), // NetKAN.Model.Metadata can't handle this, so we can't test null
+ TestCase(""),
+ TestCase("0"),
+ TestCase("2"),
+ TestCase("1.4"),
+ TestCase("v1.4.1"),
+ ]
+ public void Validate_BadSpecVersion_Throws(string spec_version)
+ {
+ Assert.Throws(() => TrySpecVersion(spec_version));
+ }
+
+ private void TrySpecVersion(string spec_version)
+ {
+ // Arrange
+ var json = new JObject();
+ json["spec_version"] = spec_version;
+
+ // Act
+ var val = new SpecVersionFormatValidator();
+ val.Validate(new Metadata(json));
+ }
+ }
+}
diff --git a/Tests/NetKAN/Validators/VersionStrictValidatorTests.cs b/Tests/NetKAN/Validators/VersionStrictValidatorTests.cs
new file mode 100644
index 0000000000..9ce8565132
--- /dev/null
+++ b/Tests/NetKAN/Validators/VersionStrictValidatorTests.cs
@@ -0,0 +1,44 @@
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using CKAN.NetKAN.Model;
+using CKAN.NetKAN.Validators;
+
+namespace Tests.NetKAN.Validators
+{
+ [TestFixture]
+ public sealed class VersionStrictValidatorTests
+ {
+ [Test,
+ TestCase("v1.2", false),
+ TestCase("v1.16", true),
+ ]
+ public void Validate_GoodStrictVersion_DoesNotThrow(string spec_version, bool strict)
+ {
+ Assert.DoesNotThrow(() => TryVersionStrict(spec_version, strict));
+ }
+
+ [Test,
+ TestCase("v1.15", true),
+ ]
+ public void Validate_BadStrictVersion_Throws(string spec_version, bool strict)
+ {
+ Assert.Throws(() => TryVersionStrict(spec_version, strict));
+ }
+
+ private void TryVersionStrict(string spec_version, bool strict)
+ {
+ // Arrange
+ var json = new JObject();
+ json["spec_version"] = spec_version;
+ json["identifier"] = "AwesomeMod";
+ if (strict)
+ {
+ json["ksp_version_strict"] = true;
+ }
+
+ // Act
+ var val = new VersionStrictValidator();
+ val.Validate(new Metadata(json));
+ }
+ }
+}
diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj
index 5bd00ace61..6b8d6f141b 100644
--- a/Tests/Tests.csproj
+++ b/Tests/Tests.csproj
@@ -136,6 +136,18 @@
+
+
+
+
+
+
+
+
+
+
+
+