diff --git a/.gitignore b/.gitignore index 983b1f9..28690ca 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ nuget.config build +coverage.json # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/AMWD.Common.Tests/AMWD.Common.Tests.csproj b/AMWD.Common.Tests/AMWD.Common.Tests.csproj index 63f846d..5e7b74c 100644 --- a/AMWD.Common.Tests/AMWD.Common.Tests.csproj +++ b/AMWD.Common.Tests/AMWD.Common.Tests.csproj @@ -3,12 +3,18 @@ net6.0 false + true + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/AMWD.Common.Tests/Extensions/CryptographyHelperExtensionsTests.cs b/AMWD.Common.Tests/Extensions/CryptographyHelperExtensionsTests.cs new file mode 100644 index 0000000..c366996 --- /dev/null +++ b/AMWD.Common.Tests/Extensions/CryptographyHelperExtensionsTests.cs @@ -0,0 +1,75 @@ +using System; +using AMWD.Common.Tests.Utils; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Security.Cryptography; + +namespace AMWD.Common.Tests.Extensions +{ + [TestClass] + public class CryptographyHelperExtensionsTests + { + [TestMethod] + public void ShouldReturnMd5Hash() + { + // arrange + string str = "Hello World!"; + byte[] bytes = new byte[] { 0xaf, 0xfe }; + + // act + string strHash = str.Md5(); + string byteHash = bytes.Md5(); + + // assert + Assert.AreEqual("ed076287532e86365e841e92bfc50d8c", strHash); + Assert.AreEqual("63c983de427ce9e2430ba8554d2a822f", byteHash); + } + + [TestMethod] + public void ShouldReturnSha1Hash() + { + // arrange + string str = "Hello World!"; + byte[] bytes = new byte[] { 0xaf, 0xfe }; + + // act + string strHash = str.Sha1(); + string byteHash = bytes.Sha1(); + + // assert + Assert.AreEqual("2ef7bde608ce5404e97d5f042f95f89f1c232871", strHash); + Assert.AreEqual("ec2c39d500316044fa49f6c8f471ddec8b4fb9d1", byteHash); + } + + [TestMethod] + public void ShouldReturnSha256Hash() + { + // arrange + string str = "Hello World!"; + byte[] bytes = new byte[] { 0xaf, 0xfe }; + + // act + string strHash = str.Sha256(); + string byteHash = bytes.Sha256(); + + // assert + Assert.AreEqual("7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069", strHash); + Assert.AreEqual("4e0da689dc7a51957be426d6cfb1bd860169cb25dd1ac946a2f141228217804a", byteHash); + } + + [TestMethod] + public void ShouldReturnSha512Hash() + { + // arrange + string str = "Hello World!"; + byte[] bytes = new byte[] { 0xaf, 0xfe }; + + // act + string strHash = str.Sha512(); + string byteHash = bytes.Sha512(); + + // assert + Assert.AreEqual("861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8", strHash); + Assert.AreEqual("591098c5d470a09f0ff48a4fdb7769ab89f803eae9e23b6f9f69dd228cca46c074bbc11a5fceaa8a5f48d14d2bf19a83a629266c2c5b7d9ef34623b64cb2f8e7", byteHash); + } + } +} diff --git a/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs b/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs index c32b41a..ad9878a 100644 --- a/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs +++ b/AMWD.Common.Tests/Extensions/DateTimeExtensionsTests.cs @@ -1,4 +1,5 @@ using System; +using AMWD.Common.Tests.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace AMWD.Common.Tests.Extensions @@ -10,6 +11,9 @@ namespace AMWD.Common.Tests.Extensions public void ShouldReturnUtc() { // arrange + var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin"); + using var mock = TimeZoneInfoLocalMock.Create(timeZoneInfo); + var utc = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Utc); var local = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Local); var unspecified = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Unspecified); @@ -33,6 +37,9 @@ namespace AMWD.Common.Tests.Extensions public void ShouldReturnLocal() { // arrange + var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin"); + using var mock = TimeZoneInfoLocalMock.Create(timeZoneInfo); + var utc = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Utc); var local = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Local); var unspecified = new DateTime(2021, 11, 15, 11, 22, 33, DateTimeKind.Unspecified); @@ -122,6 +129,9 @@ namespace AMWD.Common.Tests.Extensions public void ShouldReturnCorrectLocalAlignmentDaylightSavingEnd() { // arrange + var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin"); + using var mock = TimeZoneInfoLocalMock.Create(timeZoneInfo); + var utcNow = new DateTime(2021, 10, 30, 12, 15, 30, 45, DateTimeKind.Local); var intervalThreeSeconds = TimeSpan.FromSeconds(3); @@ -155,6 +165,9 @@ namespace AMWD.Common.Tests.Extensions public void ShouldReturnCorrectLocalAlignmentDaylightSavingStart() { // arrange + var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin"); + using var mock = TimeZoneInfoLocalMock.Create(timeZoneInfo); + var utcNow = new DateTime(2022, 3, 26, 12, 15, 30, 45, DateTimeKind.Local); var intervalThreeSeconds = TimeSpan.FromSeconds(3); diff --git a/AMWD.Common.Tests/Extensions/JsonExtensionsTests.cs b/AMWD.Common.Tests/Extensions/JsonExtensionsTests.cs new file mode 100644 index 0000000..ca36604 --- /dev/null +++ b/AMWD.Common.Tests/Extensions/JsonExtensionsTests.cs @@ -0,0 +1,307 @@ +using System; +using AMWD.Common.Tests.Utils; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AMWD.Common.Tests.Extensions +{ + [TestClass] + public class JsonExtensionsTests + { + [TestMethod] + public void ShouldReturnJson() + { + // arrange + var testObject = new JsonTestClass(); + string expected = @"{""stringValue"":""Hello World!"",""isBoolTrue"":true,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":42.21,""localTimestamp"":""2021-11-16T20:15:34+01:00"",""utcTimestamp"":""2021-11-16T20:15:34Z"",""object"":{""integerValue"":42,""stringValue"":""Foo-Bar""}}"; + + // act + string json = testObject.SerializeJson(); + + // assert + Assert.IsFalse(string.IsNullOrWhiteSpace(json)); + Assert.AreEqual(expected, json); + } + + [TestMethod] + public void ShouldReturnJsonIndented() + { + // arrange + var testObject = new JsonTestClass(); + string expected = @"{ + ""stringValue"": ""Hello World!"", + ""isBoolTrue"": true, + ""floatValue"": 12.34, + ""doubleValue"": 21.42, + ""decimalValue"": 42.21, + ""localTimestamp"": ""2021-11-16T20:15:34+01:00"", + ""utcTimestamp"": ""2021-11-16T20:15:34Z"", + ""object"": { + ""integerValue"": 42, + ""stringValue"": ""Foo-Bar"" + } +}"; + + // act + string json = testObject.SerializeJson(indented: true); + + // assert + Assert.IsFalse(string.IsNullOrWhiteSpace(json)); + Assert.AreEqual(expected.Replace("\r", ""), json.Replace("\r", "")); + } + + [TestMethod] + public void ShouldReturnJsonWithSingleQuotes() + { + // arrange + var testObject = new JsonTestClass + { + StringValue = "Sam's Pub" + }; + string expected = "{'stringValue':'Sam\\'s Pub','isBoolTrue':true,'floatValue':12.34,'doubleValue':21.42,'decimalValue':42.21,'localTimestamp':'2021-11-16T20:15:34+01:00','utcTimestamp':'2021-11-16T20:15:34Z','object':{'integerValue':42,'stringValue':'Foo-Bar'}}"; + + // act + string json = testObject.SerializeJson(useSingleQuotes: true); + + // assert + Assert.IsFalse(string.IsNullOrWhiteSpace(json)); + Assert.AreEqual(expected, json); + } + + [TestMethod] + public void ShouldReturnJsonWithoutCamelCase() + { + // arrange + var testObject = new JsonTestClass(); + string expected = @"{""StringValue"":""Hello World!"",""IsBoolTrue"":true,""FloatValue"":12.34,""DoubleValue"":21.42,""DecimalValue"":42.21,""LocalTimestamp"":""2021-11-16T20:15:34+01:00"",""UtcTimestamp"":""2021-11-16T20:15:34Z"",""Object"":{""IntegerValue"":42,""StringValue"":""Foo-Bar""}}"; + + // act + string json = testObject.SerializeJson(useCamelCase: false); + + // assert + Assert.IsFalse(string.IsNullOrWhiteSpace(json)); + Assert.AreEqual(expected, json); + } + + [TestMethod] + public void ShouldReturnJsonWithType() + { + // arrange + var testObject = new JsonTestClass(); + string expected = @"{""$type"":""AMWD.Common.Tests.Utils.JsonTestClass, AMWD.Common.Tests"",""stringValue"":""Hello World!"",""isBoolTrue"":true,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":42.21,""localTimestamp"":""2021-11-16T20:15:34+01:00"",""utcTimestamp"":""2021-11-16T20:15:34Z"",""object"":{""$type"":""AMWD.Common.Tests.Utils.JsonTestSubClass, AMWD.Common.Tests"",""integerValue"":42,""stringValue"":""Foo-Bar""}}"; + + // act + string json = testObject.SerializeJson(includeType: true); + + // assert + Assert.IsFalse(string.IsNullOrWhiteSpace(json)); + Assert.AreEqual(expected, json); + } + + [TestMethod] + public void ShouldDeserializeWithoutFallback() + { + // arrange + string workingJson = @"{""stringValue"":""Some fancy string"",""isBoolTrue"":false,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":123.45,""localTimestamp"":""2021-11-15T20:15:34+01:00"",""utcTimestamp"":""2021-10-16T20:15:34Z"",""object"":{""integerValue"":21,""stringValue"":""FooBar""}}"; + string brokenJson = @"{""strValue"":""Some fancy string"",""isBoolTrue"":false,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":123.45,""localTimestamp"":""2021-11-15T20:15:34+01:00"",""utcTimestamp"":""2021-10-16T20:15:34Z"",""object"":{""integerValue"":21.12,""stringValue"":""FooBar""}}"; + string emptyString = ""; + + // act + var workingObj = workingJson.DeserializeJson(); + var emptyObj = emptyString.DeserializeJson(); + + try + { + var brokenObj = brokenJson.DeserializeJson(); + Assert.Fail(); + } + catch (JsonReaderException) + { } + + // assert + Assert.IsNotNull(workingObj); + Assert.IsNull(emptyObj); + + Assert.AreEqual("Some fancy string", workingObj.StringValue); + Assert.IsFalse(workingObj.IsBoolTrue); + Assert.AreEqual(123.45m, workingObj.DecimalValue); + Assert.AreEqual(DateTimeKind.Local, workingObj.LocalTimestamp.Kind); + Assert.AreEqual("15.11.2021 20:15:34", workingObj.LocalTimestamp.ToString("dd.MM.yyyy HH:mm:ss")); + Assert.AreEqual(DateTimeKind.Utc, workingObj.UtcTimestamp.Kind); + Assert.AreEqual("16.10.2021 20:15:34", workingObj.UtcTimestamp.ToString("dd.MM.yyyy HH:mm:ss")); + } + + [TestMethod] + public void ShouldDeserializeWithFallback() + { + // arrange + string workingJson = @"{""stringValue"":""Some fancy string"",""isBoolTrue"":false,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":123.45,""localTimestamp"":""2021-11-15T20:15:34+01:00"",""utcTimestamp"":""2021-10-16T20:15:34Z"",""object"":{""integerValue"":21,""stringValue"":""FooBar""}}"; + string brokenJson = @"{""strValue"":""Some fancy string"",""isBoolTrue"":false,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":123.45,""localTimestamp"":""2021-11-15T20:15:34+01:00"",""utcTimestamp"":""2021-10-16T20:15:34Z"",""object"":{""integerValue"":21.12,""stringValue"":""FooBar""}}"; + string emptyString = ""; + var fallback = new JsonTestClass + { + DoubleValue = 0.815 + }; + + // act + var workingObj = workingJson.DeserializeJson(fallback); + var brokenObj = brokenJson.DeserializeJson(fallback); + var emptyObj = emptyString.DeserializeJson(fallback); + + // assert + Assert.IsNotNull(workingObj); + Assert.IsNotNull(brokenObj); + Assert.IsNull(emptyObj); + + Assert.AreEqual("Some fancy string", workingObj.StringValue); + Assert.IsFalse(workingObj.IsBoolTrue); + Assert.AreEqual(123.45m, workingObj.DecimalValue); + Assert.AreEqual(21.42, workingObj.DoubleValue); + Assert.AreEqual(DateTimeKind.Local, workingObj.LocalTimestamp.Kind); + Assert.AreEqual("15.11.2021 20:15:34", workingObj.LocalTimestamp.ToString("dd.MM.yyyy HH:mm:ss")); + Assert.AreEqual(DateTimeKind.Utc, workingObj.UtcTimestamp.Kind); + Assert.AreEqual("16.10.2021 20:15:34", workingObj.UtcTimestamp.ToString("dd.MM.yyyy HH:mm:ss")); + + Assert.AreEqual(fallback.StringValue, brokenObj.StringValue); + Assert.AreEqual(fallback.IsBoolTrue, brokenObj.IsBoolTrue); + Assert.AreEqual(fallback.DecimalValue, brokenObj.DecimalValue); + Assert.AreEqual(fallback.DoubleValue, brokenObj.DoubleValue); + Assert.AreEqual(fallback.LocalTimestamp.Kind, brokenObj.LocalTimestamp.Kind); + Assert.AreEqual(fallback.LocalTimestamp, brokenObj.LocalTimestamp); + Assert.AreEqual(fallback.UtcTimestamp.Kind, brokenObj.UtcTimestamp.Kind); + Assert.AreEqual(fallback.UtcTimestamp, brokenObj.UtcTimestamp); + } + + [TestMethod] + public void ShouldDeserializeUsingPopulate() + { + // arrange + string json = @"{""stringValue"":""Some fancy string"",""isBoolTrue"":false,""floatValue"":12.34,""doubleValue"":21.42,""decimalValue"":123.45,""localTimestamp"":""2021-11-15T20:15:34+01:00"",""utcTimestamp"":""2021-10-16T20:15:34Z"",""object"":{""integerValue"":21,""stringValue"":""FooBar""}}"; + var obj = new JsonTestClass(); + + // act + obj.DeserializeJson(json); + + // assert + Assert.AreEqual("Some fancy string", obj.StringValue); + Assert.IsFalse(obj.IsBoolTrue); + Assert.AreEqual(123.45m, obj.DecimalValue); + Assert.AreEqual(21.42, obj.DoubleValue); + Assert.AreEqual(DateTimeKind.Local, obj.LocalTimestamp.Kind); + Assert.AreEqual("15.11.2021 20:15:34", obj.LocalTimestamp.ToString("dd.MM.yyyy HH:mm:ss")); + Assert.AreEqual(DateTimeKind.Utc, obj.UtcTimestamp.Kind); + Assert.AreEqual("16.10.2021 20:15:34", obj.UtcTimestamp.ToString("dd.MM.yyyy HH:mm:ss")); + } + + [TestMethod] + public void ShouldConvertToJObject() + { + // arrange + var obj = new JsonTestClass + { + StringValue = "Hello JSON", + DecimalValue = 0.815m + }; + + // act + var jObj = obj.ConvertToJObject(); + + // assert + Assert.IsNotNull(jObj); + Assert.AreEqual(typeof(JObject), jObj.GetType()); + Assert.AreEqual(obj.StringValue, jObj.Value("stringValue")); + Assert.AreEqual(obj.DecimalValue, jObj.Value("decimalValue")); + Assert.AreEqual(obj.LocalTimestamp.Kind, jObj.Value("localTimestamp").Kind); + Assert.AreEqual(obj.LocalTimestamp, jObj.Value("localTimestamp")); + Assert.AreEqual(obj.UtcTimestamp.Kind, jObj.Value("utcTimestamp").Kind); + Assert.AreEqual(obj.UtcTimestamp, jObj.Value("utcTimestamp")); + } + + [TestMethod] + public void ShouldConvertToJArray() + { + // arrange + string[] stringArray = new[] { "one", "two", "three" }; + var objectArray = new[] + { + new JsonTestClass { StringValue = "One" }, + new JsonTestClass { StringValue = "Two" }, + new JsonTestClass { StringValue = "Three" } + }; + + // act + var stringJArray = stringArray.ConvertToJArray(); + var objectJArray = objectArray.ConvertToJArray(); + + // assert + Assert.IsNotNull(stringJArray); + Assert.AreEqual(typeof(JArray), stringJArray.GetType()); + Assert.AreEqual(stringArray[0], stringJArray[0]); + Assert.AreEqual(stringArray[1], stringJArray[1]); + Assert.AreEqual(stringArray[2], stringJArray[2]); + + Assert.IsNotNull(objectJArray); + Assert.AreEqual(typeof(JArray), objectJArray.GetType()); + Assert.AreEqual(objectArray[0].StringValue, objectJArray[0].Value("stringValue")); + Assert.AreEqual(objectArray[1].StringValue, objectJArray[1].Value("stringValue")); + Assert.AreEqual(objectArray[2].StringValue, objectJArray[2].Value("stringValue")); + } + + [TestMethod] + public void ShouldRunJsonTreeWithoutDefault() + { + // arrange + var obj = new JsonTestClass { StringValue = "Running Json", Object = new JsonTestSubClass { IntegerValue = 4711 } }; + var jObj = obj.ConvertToJObject(); + + // act + string topLevelString = jObj.GetValue("stringValue"); + decimal topLevelDecimal = jObj.GetValue("decimalValue"); + int subLevelInteger = jObj.GetValue("object:integerValue"); + string subLevelString = jObj.GetValue("object:stringValue"); + + string notExistingOnTopLevel = jObj.GetValue("fancyValue"); + string notExistingOnSubLevel = jObj.GetValue("object:fancyValue"); + int? notExistingLevel = jObj.GetValue("fancy:int"); + + // assert + Assert.AreEqual(obj.StringValue, topLevelString); + Assert.AreEqual(obj.DecimalValue, topLevelDecimal); + Assert.AreEqual(obj.Object.IntegerValue, subLevelInteger); + Assert.AreEqual(obj.Object.StringValue, subLevelString); + + Assert.IsNull(notExistingOnTopLevel); + Assert.IsNull(notExistingOnSubLevel); + Assert.IsNull(notExistingLevel); + } + + [TestMethod] + public void ShouldRunJsonTreeWithDefault() + { + // arrange + var obj = new JsonTestClass { StringValue = "Running Json", Object = new JsonTestSubClass { IntegerValue = 4711 } }; + var jObj = obj.ConvertToJObject(); + + // act + string topLevelString = jObj.GetValue("stringValue", "Test String"); + decimal topLevelDecimal = jObj.GetValue("decimalValue", 13.24m); + int subLevelInteger = jObj.GetValue("object:integerValue", 55); + string subLevelString = jObj.GetValue("object:stringValue", "Yeah!"); + + string notExistingOnTopLevel = jObj.GetValue("fancyValue", "Party!"); + string notExistingOnSubLevel = jObj.GetValue("object:fancyValue", "Well Done"); + int? notExistingLevel = jObj.GetValue("fancy:int", 13); + + // assert + Assert.AreEqual(obj.StringValue, topLevelString); + Assert.AreEqual(obj.DecimalValue, topLevelDecimal); + Assert.AreEqual(obj.Object.IntegerValue, subLevelInteger); + Assert.AreEqual(obj.Object.StringValue, subLevelString); + + Assert.AreEqual("Party!", notExistingOnTopLevel); + Assert.AreEqual("Well Done", notExistingOnSubLevel); + Assert.AreEqual(13, notExistingLevel); + } + } +} diff --git a/AMWD.Common.Tests/Extensions/ReaderWriterLockSlimExtensionsTests.cs b/AMWD.Common.Tests/Extensions/ReaderWriterLockSlimExtensionsTests.cs new file mode 100644 index 0000000..ca839ce --- /dev/null +++ b/AMWD.Common.Tests/Extensions/ReaderWriterLockSlimExtensionsTests.cs @@ -0,0 +1,98 @@ +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace AMWD.Common.Tests.Extensions +{ + [TestClass] + public class ReaderWriterLockSlimExtensionsTests + { + [TestMethod] + public void ShouldEnterReadLock() + { + // arrange + var rwLock = new ReaderWriterLockSlim(); + + // act + using var disposable = rwLock.GetReadLock(); + + // assert + Assert.IsNotNull(disposable); + Assert.IsTrue(rwLock.IsReadLockHeld); + Assert.IsFalse(rwLock.IsUpgradeableReadLockHeld); + Assert.IsFalse(rwLock.IsWriteLockHeld); + } + + [TestMethod] + public void ShouldEnterUpgradeableReadLock() + { + // arrange + var rwLock = new ReaderWriterLockSlim(); + + // act + using var disposable = rwLock.GetUpgradeableReadLock(); + + // assert + Assert.IsNotNull(disposable); + Assert.IsFalse(rwLock.IsReadLockHeld); + Assert.IsTrue(rwLock.IsUpgradeableReadLockHeld); + Assert.IsFalse(rwLock.IsWriteLockHeld); + } + + [TestMethod] + public void ShouldEnterWriteLock() + { + // arrange + var rwLock = new ReaderWriterLockSlim(); + + // act + using var disposable = rwLock.GetWriteLock(); + + // assert + Assert.IsNotNull(disposable); + Assert.IsFalse(rwLock.IsReadLockHeld); + Assert.IsFalse(rwLock.IsUpgradeableReadLockHeld); + Assert.IsTrue(rwLock.IsWriteLockHeld); + } + + [TestMethod] + public void ShouldNotAllowWriteLockAfterReadLock() + { + // arrange + var rwLock = new ReaderWriterLockSlim(); + + // act + using var disposableRead = rwLock.GetReadLock(); + try + { + using var disposaleWrite = rwLock.GetWriteLock(); + Assert.Fail(); + } + catch (LockRecursionException) + { } + + // assert + Assert.IsNotNull(disposableRead); + Assert.IsTrue(rwLock.IsReadLockHeld); + Assert.IsFalse(rwLock.IsUpgradeableReadLockHeld); + Assert.IsFalse(rwLock.IsWriteLockHeld); + } + + [TestMethod] + public void ShouldAllowWriteLockAfterUpgradeableReadLock() + { + // arrange + var rwLock = new ReaderWriterLockSlim(); + + // act + using var disposableRead = rwLock.GetUpgradeableReadLock(); + using var disposaleWrite = rwLock.GetWriteLock(); + + // assert + Assert.IsNotNull(disposableRead); + Assert.IsNotNull(disposaleWrite); + Assert.IsFalse(rwLock.IsReadLockHeld); + Assert.IsTrue(rwLock.IsUpgradeableReadLockHeld); + Assert.IsTrue(rwLock.IsWriteLockHeld); + } + } +} diff --git a/AMWD.Common.Tests/Extensions/StringExtensionsTests.cs b/AMWD.Common.Tests/Extensions/StringExtensionsTests.cs new file mode 100644 index 0000000..5f8f501 --- /dev/null +++ b/AMWD.Common.Tests/Extensions/StringExtensionsTests.cs @@ -0,0 +1,249 @@ +using System; +using System.Linq; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace AMWD.Common.Tests.Extensions +{ + [TestClass] + public class StringExtensionsTests + { + [TestMethod] + public void ShouldReturnEmptyList() + { + // arrange + string hex = ""; + + // act + var bytes = hex.HexToBytes(); + + // assert + Assert.IsNotNull(bytes); + Assert.IsFalse(bytes.Any()); + } + + [TestMethod] + public void ShouldReturnEmptyListWhenInvalid() + { + // arrange + string hex1 = "aff"; + string hex2 = "de:ad:be:e"; + string hex3 = "hell"; + + // act + var bytes1 = hex1.HexToBytes(); + var bytes2 = hex2.HexToBytes(":"); + var bytes3 = hex3.HexToBytes(); + + // assert + Assert.IsNotNull(bytes1); + Assert.IsFalse(bytes1.Any()); + + Assert.IsNotNull(bytes2); + Assert.IsFalse(bytes2.Any()); + + Assert.IsNotNull(bytes3); + Assert.IsFalse(bytes3.Any()); + } + + [TestMethod] + public void ShouldConvertHexToBytes() + { + // arrange + string hex = "deadbeef"; + + // act + var bytes = hex.HexToBytes(); + + // assert + Assert.IsNotNull(bytes); + Assert.AreEqual(4, bytes.Count()); + Assert.AreEqual(0xde, bytes.ElementAt(0)); + Assert.AreEqual(0xad, bytes.ElementAt(1)); + Assert.AreEqual(0xbe, bytes.ElementAt(2)); + Assert.AreEqual(0xef, bytes.ElementAt(3)); + } + + [TestMethod] + public void ShouldConvertHexToBytesWithDelimiter() + { + // arrange + string hex = "af:fe"; + + // act + var bytes = hex.HexToBytes(":"); + + // assert + Assert.IsNotNull(bytes); + Assert.AreEqual(2, bytes.Count()); + Assert.AreEqual(0xaf, bytes.ElementAt(0)); + Assert.AreEqual(0xfe, bytes.ElementAt(1)); + } + + [TestMethod] + public void ShouldReturnNullWhenInvalid() + { + // arrange + byte[] bytes1 = null; + byte[] bytes2 = Array.Empty(); + + // act + string hex1 = bytes1.BytesToHex(); + string hex2 = bytes2.BytesToHex(); + + // assert + Assert.IsNull(hex1); + Assert.IsNull(hex2); + } + + [TestMethod] + public void ShouldReturnHexString() + { + // arrange + byte[] bytes = new byte[] { 0xaf, 0xfe }; + + // act + string hex = bytes.BytesToHex(); + + // assert + Assert.IsNotNull(hex); + Assert.AreEqual("affe", hex); + } + + [TestMethod] + public void ShouldReturnHexStringWithDelimiter() + { + // arrange + byte[] bytes = new byte[] { 0xde, 0xad, 0xbe, 0xef }; + + // act + string hex = bytes.BytesToHex("_"); + + // assert + Assert.IsNotNull(hex); + Assert.AreEqual("de_ad_be_ef", hex); + } + + [TestMethod] + public void ShouldEncodeStringToHex() + { + // arrange + string plain = "Hello"; + + // act + string hex = plain.HexEncode(Encoding.UTF8); + + // assert + Assert.AreEqual("48656c6c6f", hex); + } + + [TestMethod] + public void ShouldDecodeStringFromHex() + { + // arrange + string hex = "48656c6c6f"; + + // act + string plain = hex.HexDecode(Encoding.UTF8); + + // assert + Assert.AreEqual("Hello", plain); + } + + [TestMethod] + public void ShouldEncodeStringToBase64() + { + // arrange + string plain = "Hello"; + + // act + string base64 = plain.Base64Encode(Encoding.UTF8); + + // assert + Assert.AreEqual("SGVsbG8=", base64); + } + + [TestMethod] + public void ShouldDecodeStringFromBase64() + { + // arrange + string base64 = "SGVsbG8="; + + // act + string plain = base64.Base64Decode(Encoding.UTF8); + + // assert + Assert.AreEqual("Hello", plain); + } + + [TestMethod] + public void ShouldReplaceStart() + { + // arrange + string str = "Hello World!"; + + // act + string test1 = str.ReplaceStart("Hello", "Bye"); + string test2 = str.ReplaceStart("World!", "Mars?"); + + // assert + Assert.AreNotEqual(str, test1); + Assert.AreEqual("Bye World!", test1); + + Assert.AreEqual(str, test2); + Assert.AreNotEqual("Hello Mars?", test2); + } + + [TestMethod] + public void ShouldReplaceEnd() + { + // arrange + string str = "Hello World!"; + + // act + string test1 = str.ReplaceEnd("Hello", "Bye"); + string test2 = str.ReplaceEnd("World!", "Mars?"); + + // assert + Assert.AreEqual(str, test1); + Assert.AreNotEqual("Bye World!", test1); + + Assert.AreNotEqual(str, test2); + Assert.AreEqual("Hello Mars?", test2); + } + + [TestMethod] + public void ShouldParseDecimal() + { + // arrange + decimal number = 1234.56m; + string stringNumberEn = "1234.56"; + string stringNumberDe = "1234,56"; + + // act + decimal numberEn = stringNumberEn.ParseDecimal(); + decimal numberDe = stringNumberDe.ParseDecimal(); + + // assert + Assert.AreEqual(number, numberEn); + Assert.AreEqual(number, numberDe); + } + + [TestMethod] + public void ShouldParseDecimalWithThousandsSeparator() + { + // arrange + decimal number = 1234.56m; + string stringNumberEn = "1,234.56"; + string stringNumberDe = "1.234,56"; + + // act + decimal numberEn = stringNumberEn.ParseDecimal(); + decimal numberDe = stringNumberDe.ParseDecimal(); + + // assert + Assert.AreEqual(number, numberEn); + Assert.AreEqual(number, numberDe); + } + } +} diff --git a/AMWD.Common.Tests/Utils/CustomMultipleAttribute.cs b/AMWD.Common.Tests/Utils/CustomMultipleAttribute.cs index 413fae0..e59fbbb 100644 --- a/AMWD.Common.Tests/Utils/CustomMultipleAttribute.cs +++ b/AMWD.Common.Tests/Utils/CustomMultipleAttribute.cs @@ -2,6 +2,7 @@ namespace AMWD.Common.Tests.Utils { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] internal class CustomMultipleAttribute : Attribute { diff --git a/AMWD.Common.Tests/Utils/JsonTestClass.cs b/AMWD.Common.Tests/Utils/JsonTestClass.cs new file mode 100644 index 0000000..9d1ed3b --- /dev/null +++ b/AMWD.Common.Tests/Utils/JsonTestClass.cs @@ -0,0 +1,32 @@ +using System; + +namespace AMWD.Common.Tests.Utils +{ + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal class JsonTestClass + { + public string StringValue { get; set; } = "Hello World!"; + + public bool IsBoolTrue { get; set; } = true; + + public float FloatValue { get; set; } = 12.34f; + + public double DoubleValue { get; set; } = 21.42; + + public decimal DecimalValue { get; set; } = 42.21m; + + public DateTime LocalTimestamp { get; set; } = new(2021, 11, 16, 20, 15, 34, DateTimeKind.Local); + + public DateTime UtcTimestamp { get; set; } = new(2021, 11, 16, 20, 15, 34, DateTimeKind.Utc); + + public JsonTestSubClass Object { get; set; } = new(); + } + + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal class JsonTestSubClass + { + public int IntegerValue { get; set; } = 42; + + public string StringValue { get; set; } = "Foo-Bar"; + } +} diff --git a/AMWD.Common.Tests/Utils/TimeZoneInfoLocalMock.cs b/AMWD.Common.Tests/Utils/TimeZoneInfoLocalMock.cs new file mode 100644 index 0000000..1b6d5a9 --- /dev/null +++ b/AMWD.Common.Tests/Utils/TimeZoneInfoLocalMock.cs @@ -0,0 +1,26 @@ +using System; +using ReflectionMagic; + +namespace AMWD.Common.Tests.Utils +{ + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class TimeZoneInfoLocalMock : IDisposable + { + private readonly TimeZoneInfo localTimeZoneInfo; + + private TimeZoneInfoLocalMock(TimeZoneInfo timeZoneInfo) + { + localTimeZoneInfo = TimeZoneInfo.Local; + SetLocalTimeZone(timeZoneInfo); + } + + public static IDisposable Create(TimeZoneInfo mockTimeZoneInfo) + => new TimeZoneInfoLocalMock(mockTimeZoneInfo); + + public void Dispose() + => SetLocalTimeZone(localTimeZoneInfo); + + private static void SetLocalTimeZone(TimeZoneInfo timeZoneInfo) + => typeof(TimeZoneInfo).AsDynamicType().s_cachedData._localTimeZone = timeZoneInfo; + } +} diff --git a/AMWD.Common/Extensions/CryptographyHelperExtensions.cs b/AMWD.Common/Extensions/CryptographyHelperExtensions.cs index 9a8ec49..2e7e6d7 100644 --- a/AMWD.Common/Extensions/CryptographyHelperExtensions.cs +++ b/AMWD.Common/Extensions/CryptographyHelperExtensions.cs @@ -50,14 +50,14 @@ /// /// The string to hash, using UTF-8 encoding. /// The SHA-256 hash value, in hexadecimal notation. - public static string Sha256(string str) => CryptographyHelper.Sha256(str); + public static string Sha256(this string str) => CryptographyHelper.Sha256(str); /// /// Computes a hash from a byte array value using the SHA-256 algorithm. /// /// The byte array. /// The SHA-256 hash value, in hexadecimal notation. - public static string Sha256(byte[] bytes) => CryptographyHelper.Sha256(bytes); + public static string Sha256(this byte[] bytes) => CryptographyHelper.Sha256(bytes); #endregion SHA-256 diff --git a/AMWD.Common/Extensions/ExceptionExtensions.cs b/AMWD.Common/Extensions/ExceptionExtensions.cs index d1a0aa3..bece785 100644 --- a/AMWD.Common/Extensions/ExceptionExtensions.cs +++ b/AMWD.Common/Extensions/ExceptionExtensions.cs @@ -5,6 +5,7 @@ namespace System /// /// Provides extension methods for exceptions. /// + [Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public static class ExceptionExtensions { /// diff --git a/AMWD.Common/Extensions/JsonExtensions.cs b/AMWD.Common/Extensions/JsonExtensions.cs index 7694833..95dfaa1 100644 --- a/AMWD.Common/Extensions/JsonExtensions.cs +++ b/AMWD.Common/Extensions/JsonExtensions.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections; +using System.Collections; using System.Globalization; using System.IO; using System.Text; @@ -36,6 +35,34 @@ namespace Newtonsoft.Json JsonConvert.PopulateObject(json, target, jsonSerializerSettings); } + /// + /// Deserializes a JSON string into a new instance. + /// + /// The type of the instance to deserialize. + /// The JSON string to read the values from. + /// A new instance of with the deserialized values. + public static T DeserializeJson(this string json) + => JsonConvert.DeserializeObject(json, jsonSerializerSettings); + + /// + /// Deserializes a JSON string into a new instance or using the fallback value. + /// + /// The type of the instance to deserialize. + /// The JSON string to read the values from. + /// A fallback value when deserialization fails. + /// A new instance of with the deserialized values or the fallback value. + public static T DeserializeJson(this string json, T fallbackValue) + { + try + { + return JsonConvert.DeserializeObject(json, jsonSerializerSettings); + } + catch + { + return fallbackValue; + } + } + /// /// Serializes an instance to a JSON string. /// @@ -63,29 +90,13 @@ namespace Newtonsoft.Json a.ErrorContext.Handled = true; }; - if (includeType) - serializer.TypeNameHandling = TypeNameHandling.Auto; + serializer.TypeNameHandling = includeType ? TypeNameHandling.All : TypeNameHandling.None; serializer.Serialize(jw, source, typeof(T)); } return sb.ToString().Trim(); } - /// - /// Deserializes a JSON string into a new instance. - /// - /// The type of the instance to deserialize. - /// The JSON string to read the values from. - /// A fallback value. - /// A new instance of with the deserialized values. - public static T DeserializeJson(this string json, T fallback = default) - { - if (!string.IsNullOrWhiteSpace(json)) - return JsonConvert.DeserializeObject(json, jsonSerializerSettings); - - return fallback; - } - /// /// Converts an object into a JObject using the custom serializer settings. /// @@ -141,10 +152,7 @@ namespace Newtonsoft.Json if (lvlObj == null) return defaultValue; - if (typeof(T) == typeof(string)) - return (T)Convert.ChangeType(lvlObj, typeof(T)); - - return DeepConvert.ChangeType(lvlObj); + return DeepConvert.ChangeType(lvlObj is JValue ? ((JValue)lvlObj).Value : lvlObj.Value()); } /// diff --git a/AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs b/AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs index 75158bc..40de6cb 100644 --- a/AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs +++ b/AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs @@ -1,4 +1,6 @@ -namespace System.Threading +//[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("AMWD.Common.Tests")] + +namespace System.Threading { /// /// Provides extension methods for the . @@ -18,7 +20,7 @@ if (!rwLock.TryEnterReadLock(timeoutMilliseconds)) throw new TimeoutException("The read lock could not be acquired."); - return new RWLockDisposable(rwLock, 1); + return new DisposableReadWriteLock(rwLock, LockMode.Read); } /// @@ -37,7 +39,7 @@ if (!rwLock.TryEnterUpgradeableReadLock(timeoutMilliseconds)) throw new TimeoutException("The upgradeable read lock could not be acquired."); - return new RWLockDisposable(rwLock, 2); + return new DisposableReadWriteLock(rwLock, LockMode.Upgradable); } /// @@ -53,15 +55,15 @@ if (!rwLock.TryEnterWriteLock(timeoutMilliseconds)) throw new TimeoutException("The write lock could not be acquired."); - return new RWLockDisposable(rwLock, 3); + return new DisposableReadWriteLock(rwLock, LockMode.Write); } - private struct RWLockDisposable : IDisposable + private struct DisposableReadWriteLock : IDisposable { private readonly ReaderWriterLockSlim rwLock; - private int lockMode; + private LockMode lockMode; - public RWLockDisposable(ReaderWriterLockSlim rwLock, int lockMode) + public DisposableReadWriteLock(ReaderWriterLockSlim rwLock, LockMode lockMode) { this.rwLock = rwLock; this.lockMode = lockMode; @@ -69,16 +71,28 @@ public void Dispose() { - if (lockMode == 1) + if (lockMode == LockMode.Read) rwLock.ExitReadLock(); - if (lockMode == 2 && rwLock.IsWriteLockHeld) // Upgraded with EnterWriteLock alone + + if (lockMode == LockMode.Upgradable && rwLock.IsWriteLockHeld) // Upgraded with EnterWriteLock alone rwLock.ExitWriteLock(); - if (lockMode == 2) + + if (lockMode == LockMode.Upgradable) rwLock.ExitUpgradeableReadLock(); - if (lockMode == 3) + + if (lockMode == LockMode.Write) rwLock.ExitWriteLock(); - lockMode = 0; + + lockMode = LockMode.None; } } + + private enum LockMode + { + None = 0, + Read = 1, + Upgradable = 2, + Write = 3, + } } } diff --git a/AMWD.Common/Extensions/StringExtensions.cs b/AMWD.Common/Extensions/StringExtensions.cs index c86e52c..19b72ca 100644 --- a/AMWD.Common/Extensions/StringExtensions.cs +++ b/AMWD.Common/Extensions/StringExtensions.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Linq; using System.Text; +using System.Text.RegularExpressions; namespace System { @@ -25,6 +26,9 @@ namespace System if (str.Length % 2 == 1) yield break; + if (Regex.IsMatch(str, "[^0-9a-fA-F]")) + yield break; + for (int i = 0; i < str.Length; i += 2) yield return Convert.ToByte(str.Substring(i, 2), 16); } diff --git a/AMWD.Common/Utilities/NetworkHelper.cs b/AMWD.Common/Utilities/NetworkHelper.cs index f69df64..8896468 100644 --- a/AMWD.Common/Utilities/NetworkHelper.cs +++ b/AMWD.Common/Utilities/NetworkHelper.cs @@ -49,6 +49,7 @@ namespace AMWD.Common.Utilities /// The interface name to resolve. /// An address family to use (available: and ). /// The resolved es or an empty list. + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // not possible to define interfaces on unit tests public static List ResolveInterface(string interfaceName, AddressFamily addressFamily = default) { if (string.IsNullOrWhiteSpace(interfaceName))