You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

303 lines
12KB

  1. using Campaign_Tracker.Server.LegacyData;
  2. using Campaign_Tracker.Server.LegacyData.Models;
  3. namespace Campaign_Tracker.Server.Tests;
  4. public sealed class LegacyDataAccessTests
  5. {
  6. // ── AC #1 — data returned via join keys (JCode, ID, KitID) ──────────────
  7. [Fact]
  8. public async Task GetJurisdictionAsync_ByJCode_ReturnsMatchingRecord_AC1()
  9. {
  10. var jurisdictions = new LegacyJurisdiction[]
  11. {
  12. new("FAIR01", "Fairview Borough", "100 Main St", "Fairview, PA 16415", null, null),
  13. new("LAKE02", "Lake Township", "200 Lake Rd", "Lake City, PA 16423", null, null),
  14. };
  15. var sut = new InMemoryLegacyDataAccess(jurisdictions: jurisdictions);
  16. var result = await sut.GetJurisdictionAsync("FAIR01");
  17. Assert.NotNull(result);
  18. Assert.Equal("FAIR01", result.JCode);
  19. Assert.Equal("Fairview Borough", result.Name);
  20. }
  21. [Fact]
  22. public async Task GetJurisdictionAsync_UnknownJCode_ReturnsNull_AC1()
  23. {
  24. var sut = new InMemoryLegacyDataAccess(jurisdictions: []);
  25. var result = await sut.GetJurisdictionAsync("UNKNOWN");
  26. Assert.Null(result);
  27. }
  28. [Fact]
  29. public async Task GetJurisdictionAsync_IsCaseInsensitive_AC1()
  30. {
  31. var sut = new InMemoryLegacyDataAccess(
  32. jurisdictions: [new("FAIR01", "Fairview", null, null, null, null)]);
  33. var result = await sut.GetJurisdictionAsync("fair01");
  34. Assert.NotNull(result);
  35. }
  36. [Fact]
  37. public async Task GetContactByIdAsync_ById_ReturnsMatchingRecord_AC1()
  38. {
  39. var contacts = new LegacyContact[]
  40. {
  41. new(1, "FAIR01", "Jane Doe", "Director", "j@test.gov",
  42. "555-0101", null, "100 Main St", null, null, null, null, null, "Fairview", "01"),
  43. };
  44. var sut = new InMemoryLegacyDataAccess(contacts: contacts);
  45. var result = await sut.GetContactByIdAsync(1);
  46. Assert.NotNull(result);
  47. Assert.Equal(1, result.Id);
  48. Assert.Equal("Jane Doe", result.ContactName);
  49. }
  50. [Fact]
  51. public async Task GetContactsByJurisdictionAsync_ByJurisCode_ReturnsAllMatches_AC1()
  52. {
  53. var contacts = new LegacyContact[]
  54. {
  55. new(1, "FAIR01", "Jane Doe", "Director", null, null, null, null, null, null, null, null, null, null, null),
  56. new(2, "FAIR01", "John Smith", "Clerk", null, null, null, null, null, null, null, null, null, null, null),
  57. new(3, "LAKE02", "Alice Jones","Director", null, null, null, null, null, null, null, null, null, null, null),
  58. };
  59. var sut = new InMemoryLegacyDataAccess(contacts: contacts);
  60. var result = await sut.GetContactsByJurisdictionAsync("FAIR01");
  61. Assert.Equal(2, result.Count);
  62. Assert.All(result, c => Assert.Equal("FAIR01", c.JurisCode));
  63. }
  64. [Fact]
  65. public async Task GetKitByIdAsync_ById_ReturnsMatchingRecord_AC1()
  66. {
  67. var kits = new LegacyKit[]
  68. {
  69. new(101, "FAIR01", "JOB-001", "Inkjet", "Active", null,
  70. Cass: true, InkJetJob: true,
  71. CreatedOn: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
  72. ExportedToSnailWorks: null, LabelsPrinted: null, OfficeCopiesAmount: null,
  73. InboundStid: null, OutboundStid: null),
  74. };
  75. var sut = new InMemoryLegacyDataAccess(kits: kits);
  76. var result = await sut.GetKitByIdAsync(101);
  77. Assert.NotNull(result);
  78. Assert.Equal(101, result.Id);
  79. Assert.Equal("FAIR01", result.JCode);
  80. }
  81. [Fact]
  82. public async Task GetKitsByJurisdictionAsync_ByJCode_ReturnsAllMatches_AC1()
  83. {
  84. var kits = new LegacyKit[]
  85. {
  86. new(101, "FAIR01", "JOB-001", "Inkjet", "Active", null, true, true, null, null, null, null, null, null),
  87. new(102, "FAIR01", "JOB-002", "OfficeCopy", "Pending", null, false, false, null, null, null, null, null, null),
  88. new(103, "LAKE02", "JOB-003", "Inkjet", "Active", null, true, true, null, null, null, null, null, null),
  89. };
  90. var sut = new InMemoryLegacyDataAccess(kits: kits);
  91. var result = await sut.GetKitsByJurisdictionAsync("FAIR01");
  92. Assert.Equal(2, result.Count);
  93. Assert.All(result, k => Assert.Equal("FAIR01", k.JCode));
  94. }
  95. [Fact]
  96. public async Task GetKitLabelsByKitAsync_ByKitId_ReturnsAllMatches_AC1()
  97. {
  98. var labels = new LegacyKitLabel[]
  99. {
  100. new(201, KitId: 101, "IMB1", "DIGITS1", "SN1", "OUTIMB1", "OUTDIGITS1", "SN2", 1),
  101. new(202, KitId: 101, "IMB2", "DIGITS2", "SN3", "OUTIMB2", "OUTDIGITS2", "SN4", 2),
  102. new(203, KitId: 102, "IMB3", "DIGITS3", "SN5", "OUTIMB3", "OUTDIGITS3", "SN6", 1),
  103. };
  104. var sut = new InMemoryLegacyDataAccess(kitLabels: labels);
  105. var result = await sut.GetKitLabelsByKitAsync(101);
  106. Assert.Equal(2, result.Count);
  107. Assert.All(result, l => Assert.Equal(101, l.KitId));
  108. }
  109. // ── AC #2 — only SELECT operations; write keywords blocked ───────────────
  110. [Fact]
  111. public void ILegacyDataAccess_HasNoWriteMethods_AC2()
  112. {
  113. var methods = typeof(ILegacyDataAccess).GetMethods()
  114. .Select(m => m.Name)
  115. .ToArray();
  116. var writePatterns = new[] { "Insert", "Update", "Delete", "Remove", "Modify", "Write", "Save", "Create", "Upsert" };
  117. foreach (var pattern in writePatterns)
  118. {
  119. Assert.DoesNotContain(methods,
  120. name => name.StartsWith(pattern, StringComparison.OrdinalIgnoreCase));
  121. }
  122. }
  123. [Theory]
  124. [InlineData("INSERT INTO Jurisdiction (JCode) VALUES ('X')")]
  125. [InlineData("UPDATE Jurisdiction SET Name = 'X' WHERE JCode = 'Y'")]
  126. [InlineData("DELETE FROM Jurisdiction WHERE JCode = 'X'")]
  127. [InlineData("DROP TABLE Jurisdiction")]
  128. [InlineData("ALTER TABLE Jurisdiction ADD COLUMN Foo TEXT")]
  129. [InlineData("EXEC sp_executesql N'DELETE FROM Kit'")]
  130. [InlineData("MERGE INTO Kit")]
  131. [InlineData("TRUNCATE TABLE Kit")]
  132. [InlineData("SELECT LastUpdated FROM Kit; UPDATE Kit SET Status = 'X'")]
  133. public void ReadOnlyCommandGuard_BlocksWriteStatements_AC2(string sql)
  134. {
  135. Assert.Throws<LegacyWriteAttemptException>(() => ReadOnlyCommandGuard.Validate(sql));
  136. }
  137. [Theory]
  138. [InlineData("SELECT * FROM Jurisdiction WHERE JCode = 'FAIR01'")]
  139. [InlineData("SELECT ID, Name FROM Jurisdiction")]
  140. [InlineData("SELECT k.ID, j.Name FROM Kit k INNER JOIN Jurisdiction j ON k.Jcode = j.JCode")]
  141. [InlineData("SELECT COUNT(*) FROM Contacts WHERE JURISCODE = 'FAIR01'")]
  142. [InlineData("SELECT [Update] FROM Contacts")]
  143. [InlineData("SELECT * FROM Contacts WHERE Notes = 'delete request'")]
  144. [InlineData("SELECT * FROM Contacts -- DELETE marker in comment")]
  145. public void ReadOnlyCommandGuard_AllowsSelectStatements_AC2(string sql)
  146. {
  147. var exception = Record.Exception(() => ReadOnlyCommandGuard.Validate(sql));
  148. Assert.Null(exception);
  149. }
  150. [Fact]
  151. public void ReadOnlyCommandGuard_BlocksEmptySql_AC2()
  152. {
  153. Assert.Throws<LegacyWriteAttemptException>(() => ReadOnlyCommandGuard.Validate(""));
  154. Assert.Throws<LegacyWriteAttemptException>(() => ReadOnlyCommandGuard.Validate(" "));
  155. }
  156. [Fact]
  157. public void OleDbLegacyDataAccess_IsAvailableForConfiguredLegacyDatabase_AC1()
  158. {
  159. Assert.True(typeof(ILegacyDataAccess).IsAssignableFrom(typeof(OleDbLegacyDataAccess)));
  160. }
  161. // ── AC #3 — results are strongly-typed domain records ───────────────────
  162. [Fact]
  163. public async Task GetAllJurisdictionsAsync_ReturnsStronglyTypedRecords_AC3()
  164. {
  165. var sut = new InMemoryLegacyDataAccess();
  166. var results = await sut.GetAllJurisdictionsAsync();
  167. // Strongly-typed: compile-time member access verifies type correctness.
  168. Assert.All(results, j =>
  169. {
  170. _ = j.JCode; // string
  171. _ = j.Name; // string?
  172. _ = j.MailingAddress;// string?
  173. _ = j.CityStateZip; // string?
  174. });
  175. Assert.IsAssignableFrom<IReadOnlyList<LegacyJurisdiction>>(results);
  176. }
  177. [Fact]
  178. public async Task GetKitByIdAsync_ReturnsStronglyTypedRecord_AC3()
  179. {
  180. var sut = new InMemoryLegacyDataAccess();
  181. var result = await sut.GetKitByIdAsync(101);
  182. Assert.NotNull(result);
  183. Assert.IsType<LegacyKit>(result);
  184. // Boolean fields mapped correctly from Access bit columns.
  185. Assert.True(result.Cass);
  186. Assert.True(result.InkJetJob);
  187. // DateTime? field mapped correctly from Access Date/Time column.
  188. Assert.IsType<DateTime>(result.CreatedOn);
  189. }
  190. [Fact]
  191. public async Task GetKitLabelsByKitAsync_ReturnsStronglyTypedRecords_AC3()
  192. {
  193. var sut = new InMemoryLegacyDataAccess();
  194. var results = await sut.GetKitLabelsByKitAsync(101);
  195. Assert.All(results, l =>
  196. {
  197. Assert.IsType<LegacyKitLabel>(l);
  198. _ = l.InBoundImb; // string?
  199. _ = l.OutboundImb; // string?
  200. _ = l.SetNumber; // double?
  201. });
  202. }
  203. // ── AC #4 — anti-corruption layer is the sole access point ───────────────
  204. [Fact]
  205. public void ILegacyDataAccess_IsTheOnlyPublicContract_LayerBoundaryEnforced_AC4()
  206. {
  207. // Verify that ILegacyDataAccess exists in the LegacyData namespace
  208. // and that InMemoryLegacyDataAccess implements it correctly.
  209. // The architectural constraint (no direct table access outside the layer)
  210. // is enforced by the interface — any external access must go through ILegacyDataAccess.
  211. var interfaceType = typeof(ILegacyDataAccess);
  212. var implType = typeof(InMemoryLegacyDataAccess);
  213. Assert.True(interfaceType.IsInterface);
  214. Assert.True(interfaceType.IsAssignableFrom(implType));
  215. Assert.Equal("Campaign_Tracker.Server.LegacyData", interfaceType.Namespace);
  216. }
  217. [Fact]
  218. public void LegacyDomainModels_AreReadOnlySealedRecords_AC4()
  219. {
  220. var modelTypes = new[]
  221. {
  222. typeof(LegacyJurisdiction),
  223. typeof(LegacyContact),
  224. typeof(LegacyKit),
  225. typeof(LegacyKitLabel),
  226. };
  227. foreach (var type in modelTypes)
  228. {
  229. Assert.True(type.IsSealed, $"{type.Name} must be sealed to prevent extension.");
  230. // Positional records expose init-only setters (construction-time only, not mutation).
  231. // AC #4 requires no post-construction mutation — verify no plain public setters exist.
  232. var mutableSetters = type.GetProperties()
  233. .Where(p => p.SetMethod is { IsPublic: true } setter
  234. && !setter.ReturnParameter.GetRequiredCustomModifiers()
  235. .Any(m => m == typeof(System.Runtime.CompilerServices.IsExternalInit)))
  236. .ToArray();
  237. Assert.Empty(mutableSetters);
  238. }
  239. }
  240. [Fact]
  241. public void LegacyJoinKeys_AreRequiredInDomainRecords_AC1()
  242. {
  243. Assert.False(IsNullableReference(typeof(LegacyContact).GetProperty(nameof(LegacyContact.JurisCode))!));
  244. Assert.False(IsNullableReference(typeof(LegacyKit).GetProperty(nameof(LegacyKit.JCode))!));
  245. Assert.Equal(typeof(int), typeof(LegacyKitLabel).GetProperty(nameof(LegacyKitLabel.KitId))!.PropertyType);
  246. }
  247. private static bool IsNullableReference(System.Reflection.PropertyInfo property) =>
  248. new System.Reflection.NullabilityInfoContext()
  249. .Create(property)
  250. .ReadState == System.Reflection.NullabilityState.Nullable;
  251. }

Powered by TurnKey Linux.