From 72b96d292b47ceac11bf2a913d863b3c9400e075 Mon Sep 17 00:00:00 2001 From: Andy Howard Date: Mon, 14 Sep 2026 11:16:48 +0100 Subject: [PATCH] Update Common package patches and tests --- .../Base/Dbas/DatabaseProvider_Tester.cs | 110 +++++++++++++++++ .../Standard/Base/Utils/ImageUtils_Tester.cs | 113 +++++++++++++++++ .../Base/Utils/NewtonsoftJsonUtil_Tester.cs | 116 ++++++++++++++++++ SacsStandardBase/SacsStandardBase.csproj | 4 +- SacsStandardDbas/SacsStandardDbas.csproj | 2 +- docs/component-reviews.md | 7 +- docs/topics.md | 3 +- 7 files changed, 349 insertions(+), 6 deletions(-) create mode 100644 SacsCoreUnitTests/Standard/Base/Dbas/DatabaseProvider_Tester.cs create mode 100644 SacsCoreUnitTests/Standard/Base/Utils/ImageUtils_Tester.cs create mode 100644 SacsCoreUnitTests/Standard/Base/Utils/NewtonsoftJsonUtil_Tester.cs diff --git a/SacsCoreUnitTests/Standard/Base/Dbas/DatabaseProvider_Tester.cs b/SacsCoreUnitTests/Standard/Base/Dbas/DatabaseProvider_Tester.cs new file mode 100644 index 0000000..6928a9b --- /dev/null +++ b/SacsCoreUnitTests/Standard/Base/Dbas/DatabaseProvider_Tester.cs @@ -0,0 +1,110 @@ +using Microsoft.Data.SqlClient; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Sarand.Net.Standard.Base.Dbas; +using Sarand.Net.Standard.Dbas.Base; +using Sarand.Net.Standard.Dbas.Data; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Data.Odbc; + +namespace Sacs.UnitTests.Net.Standard.Base.Dbas +{ + [TestClass] + public class DatabaseProvider_Tester + { + private class ParameterTestItem : DbDataItem + { + public ParameterTestItem() : base() { } + public override string SchemaName => "dbo"; + public override string TableName => "TestItem"; + public override void PopulateFrom(DbCommand dbCommand, string tableAlias, string filter) { } + public override void PopulateFrom(DbDataReader dbDataReader) { } + public override bool ExecuteInsertCommand(DbCommand dbCommand) => false; + public void AddParameter(DbCommand command, string name, T value) => AddCommandParameter(command, name, value); + } + + // --- Provider Selection --- + + [TestMethod] + public void OpenOdbcDatabase_CreatesSqlClientAndForcedOdbcConnections() + { + SacsDatabaseConsole.OdbcConfig config = new SacsDatabaseConsole.OdbcConfig + { + Dbms = DbmsType.MsSql, + DatabaseName = "TestDb", + Location = "MsSql:localhost\\TestDb", + ConnectionString = "Driver={SQL Server};Server=localhost;Database=TestDb;Trusted_Connection=Yes;" + }; + + SacsDatabase database = SacsDatabaseConsole.OpenOdbcDatabase(config); + using DbConnection sqlConnection = database.CreateConnection(); + using DbConnection odbcConnection = database.CreateConnection(true); + + Assert.IsInstanceOfType(sqlConnection, typeof(SqlConnection)); + StringAssert.Contains(sqlConnection.ConnectionString, "Server=localhost"); + StringAssert.Contains(sqlConnection.ConnectionString, "Database=TestDb"); + Assert.IsInstanceOfType(odbcConnection, typeof(OdbcConnection)); + Assert.AreEqual(config.ConnectionString, odbcConnection.ConnectionString); + } + + // --- Command Parameters --- + + [TestMethod] + public void AddCommandParameter_AssignsValuesNullsAndExplicitTypes() + { + ParameterTestItem item = new ParameterTestItem(); + using SqlCommand command = new SqlCommand(); + + item.AddParameter(command, "@Count", 12); + item.AddParameter(command, "@MissingCount", null); + item.AddParameter(command, "@Data", null); + + Assert.AreEqual("@Count", command.Parameters[0].ParameterName); + Assert.AreEqual(12, command.Parameters[0].Value); + Assert.AreEqual(DbType.Int32, command.Parameters[0].DbType); + Assert.AreEqual(DBNull.Value, command.Parameters[1].Value); + Assert.AreEqual(DbType.Int32, command.Parameters[1].DbType); + Assert.AreEqual(DBNull.Value, command.Parameters[2].Value); + Assert.AreEqual(DbType.Binary, command.Parameters[2].DbType); + } + + [TestMethod] + public void AddCommandParameter_NormalisesSqlAndOdbcDates() + { + ParameterTestItem item = new ParameterTestItem(); + DateTime value = new DateTime(2026, 9, 14, 10, 20, 30, 456, DateTimeKind.Utc); + using SqlCommand sqlCommand = new SqlCommand(); + using OdbcCommand odbcCommand = new OdbcCommand(); + + item.AddParameter(sqlCommand, "@When", value); + item.AddParameter(odbcCommand, "When", value); + + Assert.AreEqual(DbType.DateTime2, sqlCommand.Parameters[0].DbType); + Assert.AreEqual(DbType.DateTime, odbcCommand.Parameters[0].DbType); + Assert.AreEqual(value.AddMilliseconds(-456), sqlCommand.Parameters[0].Value); + Assert.AreEqual(value.AddMilliseconds(-456), odbcCommand.Parameters[0].Value); + } + + [TestMethod] + public void AddCommandParameter_MapsSentinelDatesToDatabaseNull() + { + ParameterTestItem item = new ParameterTestItem(); + using SqlCommand command = new SqlCommand(); + + item.AddParameter(command, "@Minimum", DateTime.MinValue); + item.AddParameter(command, "@Maximum", DateTime.MaxValue); + item.AddParameter(command, "@Missing", null); + + Assert.AreEqual(DBNull.Value, command.Parameters[0].Value); + Assert.AreEqual(DBNull.Value, command.Parameters[1].Value); + Assert.AreEqual(DBNull.Value, command.Parameters[2].Value); + Assert.AreEqual(DbType.DateTime2, command.Parameters[0].DbType); + Assert.AreEqual(DbType.DateTime2, command.Parameters[1].DbType); + Assert.AreEqual(DbType.DateTime2, command.Parameters[2].DbType); + } + + // --- The End --- + } +} diff --git a/SacsCoreUnitTests/Standard/Base/Utils/ImageUtils_Tester.cs b/SacsCoreUnitTests/Standard/Base/Utils/ImageUtils_Tester.cs new file mode 100644 index 0000000..7c7bcd6 --- /dev/null +++ b/SacsCoreUnitTests/Standard/Base/Utils/ImageUtils_Tester.cs @@ -0,0 +1,113 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Sarand.Net.Standard.Base.Utils; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using System; +using System.IO; +using System.Text; + +namespace Sacs.UnitTests.Net.Standard.Base.Utils +{ + [TestClass] + public class ImageUtils_Tester + { + // --- Image Recognition --- + + [TestMethod] + public void VerifyImageData_RecognisesPngAndJpegDimensions() + { + byte[] pngData = CreateImageData(7, 5, new PngEncoder()); + byte[] jpegData = CreateImageData(9, 4, new JpegEncoder()); + + ImageUtils.ImageConfig pngConfig = ImageUtils.VerifyImageData(pngData); + ImageUtils.ImageConfig jpegConfig = ImageUtils.VerifyImageData(jpegData); + + Assert.IsTrue(pngConfig.ValidImage); + Assert.AreEqual("PNG", pngConfig.ImageFormatName); + Assert.AreEqual("image/png", pngConfig.DefaultMimeType); + Assert.AreEqual(7, pngConfig.Width); + Assert.AreEqual(5, pngConfig.Height); + Assert.IsTrue(jpegConfig.ValidImage); + Assert.AreEqual("JPEG", jpegConfig.ImageFormatName); + Assert.AreEqual("image/jpeg", jpegConfig.DefaultMimeType); + Assert.AreEqual(9, jpegConfig.Width); + Assert.AreEqual(4, jpegConfig.Height); + } + + [TestMethod] + public void VerifyImageData_RejectsInvalidAndTruncatedData() + { + byte[] pngData = CreateImageData(3, 2, new PngEncoder()); + byte[] truncatedData = new byte[Math.Min(12, pngData.Length)]; + Array.Copy(pngData, truncatedData, truncatedData.Length); + + Assert.IsFalse(ImageUtils.VerifyImageData(Array.Empty()).ValidImage); + Assert.IsFalse(ImageUtils.VerifyImageData(Encoding.UTF8.GetBytes("not an image")).ValidImage); + Assert.IsFalse(ImageUtils.VerifyImageData(truncatedData).ValidImage); + } + + [TestMethod] + public void IsSvg_RecognisesSvgAndRejectsFalsePositives() + { + byte[] svgData = Encoding.UTF8.GetBytes(""); + byte[] missingNamespace = Encoding.UTF8.GetBytes(""); + + Assert.IsTrue(ImageUtils.IsSvg(svgData)); + Assert.IsFalse(ImageUtils.IsSvg(missingNamespace)); + Assert.IsFalse(ImageUtils.IsSvg(Encoding.UTF8.GetBytes("svg xmlns=\"http://www.w3.org/2000/svg\""))); + Assert.IsFalse(ImageUtils.IsSvg(null)); + } + + // --- Encoding and Resizing --- + + [TestMethod] + public void LoadAndEncodeImage_PreservesFormatAndDimensions() + { + byte[] originalData = CreateImageData(8, 6, new PngEncoder()); + + using Image image = ImageUtils.LoadImage(originalData, out IImageFormat format); + byte[] encodedData = image.GetImageData(format); + ImageUtils.ImageConfig encodedConfig = ImageUtils.VerifyImageData(encodedData); + + Assert.AreEqual("PNG", format.Name); + Assert.IsTrue(encodedConfig.ValidImage); + Assert.AreEqual(8, encodedConfig.Width); + Assert.AreEqual(6, encodedConfig.Height); + } + + [TestMethod] + public void ConstrainImage_ResizesLandscapeAndPortraitWithoutUpscaling() + { + ImageUtils.ImageConfig landscape = ImageUtils.VerifyImageData(CreateImageData(20, 10, new PngEncoder())); + ImageUtils.ImageConfig portrait = ImageUtils.VerifyImageData(CreateImageData(10, 20, new PngEncoder())); + ImageUtils.ImageConfig withinLimits = ImageUtils.VerifyImageData(CreateImageData(4, 3, new PngEncoder())); + byte[] originalData = withinLimits.ImageData; + + Assert.IsTrue(ImageUtils.ConstrainImage(landscape, 8, 8)); + Assert.AreEqual(8, landscape.Width); + Assert.AreEqual(4, landscape.Height); + Assert.IsTrue(ImageUtils.ConstrainImage(portrait, 8, 8)); + Assert.AreEqual(4, portrait.Width); + Assert.AreEqual(8, portrait.Height); + Assert.IsTrue(ImageUtils.ConstrainImage(withinLimits, 8, 8)); + Assert.AreSame(originalData, withinLimits.ImageData); + Assert.IsFalse(ImageUtils.ConstrainImage(landscape, 0, 8)); + Assert.IsFalse(ImageUtils.ConstrainImage(portrait, 8, 0)); + } + + // --- Utilities --- + + private static byte[] CreateImageData(int width, int height, IImageEncoder encoder) + { + using Image image = new Image(width, height, new Rgba32(32, 96, 160, 255)); + using MemoryStream stream = new MemoryStream(); + image.Save(stream, encoder); + return stream.ToArray(); + } + + // --- The End --- + } +} diff --git a/SacsCoreUnitTests/Standard/Base/Utils/NewtonsoftJsonUtil_Tester.cs b/SacsCoreUnitTests/Standard/Base/Utils/NewtonsoftJsonUtil_Tester.cs new file mode 100644 index 0000000..86b0fad --- /dev/null +++ b/SacsCoreUnitTests/Standard/Base/Utils/NewtonsoftJsonUtil_Tester.cs @@ -0,0 +1,116 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Sarand.Net.Standard.Base.Bus; +using Sarand.Net.Standard.Base.Helpers; +using Sarand.Net.Standard.Base.Utils; +using System; +using System.Collections.Generic; + +namespace Sacs.UnitTests.Net.Standard.Base.Utils +{ + [TestClass] + public class NewtonsoftJsonUtil_Tester + { + private class JsonData + { + public string Name { get; set; } + public int Count { get; set; } + public string Optional { get; set; } + public List Values { get; set; } = new List { "constructor value" }; + } + + // --- Serialization --- + + [TestMethod] + public void SerializeObject_HandlesObjectsArraysNullsAndSettings() + { + JsonData data = new JsonData { Name = "alpha", Count = 3, Optional = null }; + JsonSerializerSettings settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; + + string objectJson = NewtonsoftJsonUtil.SerializeObject(data, settings); + string arrayJson = NewtonsoftJsonUtil.SerializeObject(new[] { 1, 2, 3 }); + + StringAssert.Contains(objectJson, "\"Name\":\"alpha\""); + Assert.IsFalse(objectJson.Contains("Optional")); + Assert.AreEqual("[1,2,3]", arrayJson); + Assert.AreEqual("null", NewtonsoftJsonUtil.SerializeObject(null)); + } + + [TestMethod] + public void SysIdJsonConverter_RoundTripsValidAndInvalidValues() + { + string validJson = JsonConvert.SerializeObject(new SysId(42)); + string invalidJson = JsonConvert.SerializeObject(new SysId()); + + SysId validId = JsonConvert.DeserializeObject(validJson); + SysId invalidId = JsonConvert.DeserializeObject(invalidJson); + + Assert.AreEqual("42", validJson); + Assert.AreEqual("0", invalidJson); + Assert.AreEqual(42, validId.ToInt()); + Assert.IsFalse(SysId.IsValid(invalidId)); + Assert.IsFalse(SysId.IsValid(JsonConvert.DeserializeObject("null"))); + } + + // --- Parsing --- + + [TestMethod] + public void ParseJsonMethods_ParseTokensObjectsAndArrays() + { + JToken token = NewtonsoftJsonUtil.ParseJsonToken("123"); + JObject jsonObject = NewtonsoftJsonUtil.ParseJsonObject("{\"name\":\"alpha\",\"nested\":{\"enabled\":true}}"); + JArray jsonArray = NewtonsoftJsonUtil.ParseJsonArray("[1,2,3]"); + + Assert.AreEqual(123, token.Value()); + Assert.AreEqual("alpha", jsonObject["name"].Value()); + Assert.IsTrue(jsonObject["nested"]["enabled"].Value()); + CollectionAssert.AreEqual(new[] { 1, 2, 3 }, jsonArray.ToObject()); + } + + [TestMethod] + public void ParseJsonMethods_ReportInvalidInputWithoutThrowing() + { + Assert.IsNull(NewtonsoftJsonUtil.ParseJsonToken("{", out Exception tokenException)); + Assert.IsNotNull(tokenException); + Assert.IsNull(NewtonsoftJsonUtil.ParseJsonObject("[]", out Exception objectException)); + Assert.IsNotNull(objectException); + Assert.IsNull(NewtonsoftJsonUtil.ParseJsonArray("{}", out Exception arrayException)); + Assert.IsNotNull(arrayException); + Assert.ThrowsException(() => NewtonsoftJsonUtil.ParseJsonToken("{")); + } + + [TestMethod] + public void DeserializeJsonObject_ProvidesDynamicPropertyAccess() + { + dynamic data = SacsJsonHelper.DeserializeJsonObject("{\"name\":\"alpha\",\"count\":3,\"items\":[\"a\",\"b\"]}"); + + Assert.AreEqual("alpha", (string)data.name); + Assert.AreEqual(3, (int)data.count); + Assert.AreEqual("b", (string)data.items[1]); + Assert.IsNull(SacsJsonHelper.DeserializeJsonObject("null")); + } + + // --- Deep Copy --- + + [TestMethod] + public void DeepCopy_CreatesIndependentNestedCollectionsAndReplacesConstructorValues() + { + JsonData source = new JsonData { Name = "source", Count = 2 }; + source.Values.Clear(); + source.Values.AddRange(new[] { "one", "two" }); + + JsonData copy = SerializableUtil.DeepCopy(source); + copy.Values.Add("copy only"); + + Assert.AreNotSame(source, copy); + Assert.AreNotSame(source.Values, copy.Values); + CollectionAssert.AreEqual(new[] { "one", "two" }, source.Values); + CollectionAssert.AreEqual(new[] { "one", "two", "copy only" }, copy.Values); + Assert.IsNull(SerializableUtil.DeepCopy(null)); + Assert.AreEqual(String.Empty, SerializableUtil.ToJson(null)); + } + + // --- The End --- + } +} diff --git a/SacsStandardBase/SacsStandardBase.csproj b/SacsStandardBase/SacsStandardBase.csproj index 174e3cd..98928e5 100644 --- a/SacsStandardBase/SacsStandardBase.csproj +++ b/SacsStandardBase/SacsStandardBase.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/SacsStandardDbas/SacsStandardDbas.csproj b/SacsStandardDbas/SacsStandardDbas.csproj index ee5ff22..ef8a741 100644 --- a/SacsStandardDbas/SacsStandardDbas.csproj +++ b/SacsStandardDbas/SacsStandardDbas.csproj @@ -11,7 +11,7 @@ - + diff --git a/docs/component-reviews.md b/docs/component-reviews.md index 1c501fb..813553b 100644 --- a/docs/component-reviews.md +++ b/docs/component-reviews.md @@ -7,11 +7,14 @@ table td { } -Last updated: 2026-09-12 +Last updated: 2026-09-14 | Component | Scope | Status | Date reviewed | Next steps | Notes | |---|---|---|---|---|---| -| `SaCommonVS.sln` | .NET 10 migration build and tests | Migration complete; passing with warnings | 2026-09-11 | Address the separately tracked dependency, security, and .NET 10 warnings. | All supported projects target .NET 10 only. The complete solution builds with zero errors, and 94 of 94 tests pass. BusinessMgr, Sandtrap, and Sandalwood rebuild successfully against the final Common targets. Xamarin remains isolated and unchanged. | +| `SaCommonVS.sln` | .NET 10 migration build and tests | Migration complete; passing with warnings | 2026-09-14 | Remove the redundant Registry reference, then continue the staged package review. | All supported projects target .NET 10 only. After the low-risk SqlClient, Newtonsoft.Json, and ImageSharp patch updates and their regression tests, the complete solution builds with zero errors and all 126 tests pass. BusinessMgr builds with 11 Control and 13 Portal tests passing. Xamarin remains isolated and unchanged. | +| `SacsStandardBase/Base/Utils/ImageUtils.cs` | ImageSharp wrapper | Covered for package upgrades | 2026-09-14 | Extend the suite when adding formats or changing resize policy. | Five in-memory tests cover PNG and JPEG metadata, invalid and truncated input, SVG recognition, format-preserving encoding, proportional landscape and portrait resizing, no-upscale behavior, and invalid constraints. | +| `SacsStandardBase` JSON utilities and `SysId` converter | Newtonsoft.Json integration | Covered for package upgrades | 2026-09-14 | Retain the suite for future Newtonsoft.Json updates. | Six tests cover objects, arrays, nulls, serializer settings, valid and invalid parsing paths, dynamic access, `SysId` conversion, independent deep copies, nested collections, and constructor-value replacement. | +| `SacsStandardDbas` provider and parameter creation | Microsoft.Data.SqlClient integration | Initial package-upgrade coverage complete | 2026-09-14 | Expand into configured database integration tests before the planned SqlClient major upgrade. | Four database-free tests cover SQL versus forced ODBC connection selection, connection strings, values and nulls, explicit integer and binary types, SQL `DateTime2`, ODBC `DateTime`, precision normalization, and sentinel dates. | | `SacsMobileXamarin` | Migration disposition | Isolated; retained for reference | 2026-09-10 | Keep outside the supported build unless a separately scoped resurrection or migration is approved. | Source is unchanged and available through `SaCommonVS.LegacyXamarin.sln`; see `docs/legacy-xamarin.md`. | | [`SacsMobileMaui`](../SacsMobileMaui/docs/component-reviews.md) | Project | .NET 10 migration complete; tracked in project docs | 2026-09-11 | Validate Apple targets on a Mac and continue API cleanup in the project tracker. | The shared project and both active mobile consumers target .NET 10. Windows runtime smoke tests pass, and Windows and Android Debug builds complete with zero errors. | | `SacsStandardBase/Base/Procs/SaProcess_Base.cs` | Class | In progress | 2026-09-09 | Complete the Process Base actions recorded in `topics.md`; review derived processes separately for cooperative polling. | Lifecycle state ownership and cancellation lock ordering were changed. Five focused tests pass. Remaining concerns include mixed sync/async execution, durable cancellation outcome, cancellation exception handling, disposal, and status synchronization. | diff --git a/docs/topics.md b/docs/topics.md index 5450b99..37c1327 100644 --- a/docs/topics.md +++ b/docs/topics.md @@ -7,7 +7,7 @@ table td { } -Last updated: 2026-09-12 +Last updated: 2026-09-14 ## Prioritised Topics to Be Actioned @@ -44,6 +44,7 @@ Last updated: 2026-09-12 | Topic | Date noted | Date started | Date completed | Outcome | |---|---|---|---|---| +| Apply low-risk Common package patches and regression coverage | 2026-09-14 | 2026-09-14 | 2026-09-14 | Updated Microsoft.Data.SqlClient from 5.2.2 to 5.2.3, Newtonsoft.Json from 13.0.3 to 13.0.4, and SixLabors.ImageSharp from 3.1.11 to 3.1.12. Added 15 in-memory tests covering image recognition, encoding and resizing; JSON serialization, parsing, dynamic access, `SysId` conversion and deep copying; and SQL/ODBC provider selection and command parameters. The Common solution builds with zero errors; all 126 Common tests, 11 BusinessMgr Control tests, and 13 BusinessMgr Portal tests pass. Existing warnings are unchanged. | | Make asynchronous Process Base execution awaitable and retain state after a stop timeout | 2026-09-08 | 2026-09-08 | 2026-09-08 | `RunAsync()` returns `Task`; `StopProcessAsync()` reports success and leaves active state owned by a worker that has not stopped. | | Remove cancellation callbacks from the Process Base lifecycle lock | 2026-09-08 | 2026-09-08 | 2026-09-08 | Cancellation and task-data disposal use separate synchronization; worker cleanup detaches state before disposal. | | Add Process Base lifecycle tests | 2026-09-08 | 2026-09-08 | 2026-09-08 | Five focused MSTest cases cover completion, duplicate starts, stop timeout retention, cooperative cancellation, and cancellation callback lock behavior. |