using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using OrpaonVision.Core.LayerRecognition; using OrpaonVision.Core.PartRecognition; using OrpaonVision.Core.QuantityValidation; using OrpaonVision.Core.Results; using OrpaonVision.SiteApp.Runtime.Options; using OrpaonVision.SiteApp.Runtime.Services; namespace OrpaonVision.SiteApp.Tests.Runtime.Services; /// /// 数量校验服务单元测试。 /// [TestClass] public class QuantityValidationServiceTests { private QuantityValidationService _service = null!; private Mock> _loggerMock = null!; private Mock> _optionsMock = null!; private RuntimeOptions _options = null!; [TestInitialize] public void TestInitialize() { _loggerMock = new Mock>(); _options = new RuntimeOptions(); _optionsMock = new Mock>(); _optionsMock.Setup(x => x.Value).Returns(_options); _service = new QuantityValidationService(_loggerMock.Object, _optionsMock.Object); } [TestMethod] public async Task ValidateExactQuantityAsync_ExactMatch_ReturnsSuccess() { // Arrange var parts = CreateTestParts(3); const int expectedCount = 3; // Act var result = await _service.ValidateExactQuantityAsync(parts, expectedCount); // Assert Assert.IsTrue(result.Succeeded); Assert.IsTrue(result.Data.IsValid); Assert.AreEqual(expectedCount, result.Data.ActualCount); Assert.AreEqual(expectedCount, result.Data.ExpectedCount); Assert.AreEqual(0, result.Data.Deviation); } [TestMethod] public async Task ValidateExactQuantityAsync_NotExactMatch_ReturnsFailure() { // Arrange var parts = CreateTestParts(2); const int expectedCount = 3; // Act var result = await _service.ValidateExactQuantityAsync(parts, expectedCount); // Assert Assert.IsTrue(result.Succeeded); Assert.IsFalse(result.Data.IsValid); Assert.AreEqual(2, result.Data.ActualCount); Assert.AreEqual(expectedCount, result.Data.ExpectedCount); Assert.AreEqual(-1, result.Data.Deviation); } [TestMethod] public async Task ValidateExactQuantityAsync_WithPartType_FiltersCorrectly() { // Arrange var parts = CreateTestPartsWithTypes(); const int expectedCount = 2; const PartType partType = PartType.Required; // Act var result = await _service.ValidateExactQuantityAsync(parts, expectedCount, partType); // Assert Assert.IsTrue(result.Succeeded); Assert.IsTrue(result.Data.IsValid); Assert.AreEqual(partType, result.Data.PartType); Assert.AreEqual(expectedCount, result.Data.ActualCount); } [TestMethod] public async Task ValidateMinimumQuantityAsync_AboveMinimum_ReturnsSuccess() { // Arrange var parts = CreateTestParts(5); const int minCount = 3; // Act var result = await _service.ValidateMinimumQuantityAsync(parts, minCount); // Assert Assert.IsTrue(result.Succeeded); Assert.IsTrue(result.Data.IsValid); Assert.AreEqual(5, result.Data.ActualCount); Assert.AreEqual(minCount, result.Data.MinimumCount); Assert.AreEqual(2, result.Data.ExcessCount); } [TestMethod] public async Task ValidateMinimumQuantityAsync_BelowMinimum_ReturnsFailure() { // Arrange var parts = CreateTestParts(2); const int minCount = 3; // Act var result = await _service.ValidateMinimumQuantityAsync(parts, minCount); // Assert Assert.IsTrue(result.Succeeded); Assert.IsFalse(result.Data.IsValid); Assert.AreEqual(2, result.Data.ActualCount); Assert.AreEqual(minCount, result.Data.MinimumCount); Assert.AreEqual(0, result.Data.ExcessCount); } [TestMethod] public async Task ValidateRangeQuantityAsync_WithinRange_ReturnsSuccess() { // Arrange var parts = CreateTestParts(4); const int minCount = 3; const int maxCount = 5; // Act var result = await _service.ValidateRangeQuantityAsync(parts, minCount, maxCount); // Assert Assert.IsTrue(result.Succeeded); Assert.IsTrue(result.Data.IsValid); Assert.AreEqual(4, result.Data.ActualCount); Assert.AreEqual(minCount, result.Data.MinimumCount); Assert.AreEqual(maxCount, result.Data.MaximumCount); Assert.IsFalse(result.Data.IsBelowMinimum); Assert.IsFalse(result.Data.IsAboveMaximum); } [TestMethod] public async Task ValidateRangeQuantityAsync_BelowMinimum_ReturnsFailure() { // Arrange var parts = CreateTestParts(2); const int minCount = 3; const int maxCount = 5; // Act var result = await _service.ValidateRangeQuantityAsync(parts, minCount, maxCount); // Assert Assert.IsTrue(result.Succeeded); Assert.IsFalse(result.Data.IsValid); Assert.AreEqual(2, result.Data.ActualCount); Assert.AreEqual(minCount, result.Data.MinimumCount); Assert.AreEqual(maxCount, result.Data.MaximumCount); Assert.IsTrue(result.Data.IsBelowMinimum); Assert.IsFalse(result.Data.IsAboveMaximum); } [TestMethod] public async Task ValidateRangeQuantityAsync_AboveMaximum_ReturnsFailure() { // Arrange var parts = CreateTestParts(6); const int minCount = 3; const int maxCount = 5; // Act var result = await _service.ValidateRangeQuantityAsync(parts, minCount, maxCount); // Assert Assert.IsTrue(result.Succeeded); Assert.IsFalse(result.Data.IsValid); Assert.AreEqual(6, result.Data.ActualCount); Assert.AreEqual(minCount, result.Data.MinimumCount); Assert.AreEqual(maxCount, result.Data.MaximumCount); Assert.IsFalse(result.Data.IsBelowMinimum); Assert.IsTrue(result.Data.IsAboveMaximum); } [TestMethod] public async Task ValidateUniqueQuantityAsync_ExactUniqueCount_ReturnsSuccess() { // Arrange var parts = CreateTestPartsWithUniqueProperties("serial_number", 3); const int expectedUniqueCount = 3; const string uniqueProperty = "serial_number"; // Act var result = await _service.ValidateUniqueQuantityAsync(parts, uniqueProperty, expectedUniqueCount); // Assert Assert.IsTrue(result.Succeeded); Assert.IsTrue(result.Data.IsValid); Assert.AreEqual(expectedUniqueCount, result.Data.ActualUniqueCount); Assert.AreEqual(expectedUniqueCount, result.Data.ExpectedUniqueCount); Assert.AreEqual(uniqueProperty, result.Data.UniqueProperty); Assert.AreEqual(0, result.Data.DuplicateValues.Count); } [TestMethod] public async Task ValidateUniqueQuantityAsync_WithDuplicates_ReturnsFailure() { // Arrange var parts = CreateTestPartsWithDuplicates("serial_number", 3); const int expectedUniqueCount = 3; const string uniqueProperty = "serial_number"; // Act var result = await _service.ValidateUniqueQuantityAsync(parts, uniqueProperty, expectedUniqueCount); // Assert Assert.IsTrue(result.Succeeded); Assert.IsFalse(result.Data.IsValid); Assert.AreEqual(2, result.Data.ActualUniqueCount); // 3 parts, but 2 unique values Assert.AreEqual(expectedUniqueCount, result.Data.ExpectedUniqueCount); Assert.AreEqual(uniqueProperty, result.Data.UniqueProperty); Assert.AreEqual(1, result.Data.DuplicateValues.Count); } [TestMethod] public async Task ValidateUniqueQuantityAsync_PropertyNotExists_ReturnsZeroUnique() { // Arrange var parts = CreateTestParts(3); const int expectedUniqueCount = 3; const string uniqueProperty = "nonexistent_property"; // Act var result = await _service.ValidateUniqueQuantityAsync(parts, uniqueProperty, expectedUniqueCount); // Assert Assert.IsTrue(result.Succeeded); Assert.IsFalse(result.Data.IsValid); Assert.AreEqual(0, result.Data.ActualUniqueCount); Assert.AreEqual(expectedUniqueCount, result.Data.ExpectedUniqueCount); Assert.AreEqual(uniqueProperty, result.Data.UniqueProperty); } [TestMethod] public async Task BatchValidateQuantityAsync_AllRulesPass_ReturnsSuccess() { // Arrange var parts = CreateTestParts(3); var rules = new List { CreateExactQuantityRule("Rule1", 3), CreateMinimumQuantityRule("Rule2", 2), CreateRangeQuantityRule("Rule3", 2, 4) }; // Act var result = await _service.BatchValidateQuantityAsync(parts, rules); // Assert Assert.IsTrue(result.Succeeded); Assert.AreEqual(3, result.Data.TotalRules); Assert.AreEqual(3, result.Data.PassedRules); Assert.AreEqual(0, result.Data.FailedRules); Assert.AreEqual(100.0, result.Data.OverallPassRate); } [TestMethod] public async Task BatchValidateQuantityAsync_SomeRulesFail_ReturnsPartialSuccess() { // Arrange var parts = CreateTestParts(3); var rules = new List { CreateExactQuantityRule("Rule1", 3), // Pass CreateExactQuantityRule("Rule2", 5), // Fail CreateMinimumQuantityRule("Rule3", 2) // Pass }; // Act var result = await _service.BatchValidateQuantityAsync(parts, rules); // Assert Assert.IsTrue(result.Succeeded); Assert.AreEqual(3, result.Data.TotalRules); Assert.AreEqual(2, result.Data.PassedRules); Assert.AreEqual(1, result.Data.FailedRules); Assert.AreEqual(66.7, Math.Round(result.Data.OverallPassRate, 1)); } [TestMethod] public async Task CreateValidationRuleAsync_ValidRule_ReturnsSuccess() { // Arrange var rule = CreateExactQuantityRule("TestRule", 5); // Act var result = await _service.CreateValidationRuleAsync(rule); // Assert Assert.IsTrue(result.Succeeded); Assert.IsNotNull(result.Data.RuleId); Assert.AreEqual("TestRule", result.Data.RuleName); Assert.AreEqual(QuantityValidationType.Exact, result.Data.ValidationType); Assert.IsTrue(result.Data.IsEnabled); } [TestMethod] public async Task UpdateValidationRuleAsync_ExistingRule_ReturnsSuccess() { // Arrange var rule = CreateExactQuantityRule("TestRule", 5); var createResult = await _service.CreateValidationRuleAsync(rule); var updatedRule = createResult.Data; updatedRule.ExpectedCount = 10; updatedRule.RuleDescription = "Updated description"; // Act var result = await _service.UpdateValidationRuleAsync(updatedRule); // Assert Assert.IsTrue(result.Succeeded); } [TestMethod] public async Task UpdateValidationRuleAsync_NonExistingRule_ReturnsFailure() { // Arrange var rule = CreateExactQuantityRule("TestRule", 5); rule.RuleId = Guid.NewGuid(); // Act var result = await _service.UpdateValidationRuleAsync(rule); // Assert Assert.IsFalse(result.Succeeded); Assert.AreEqual("RULE_NOT_FOUND", result.Code); } [TestMethod] public async Task DeleteValidationRuleAsync_ExistingRule_ReturnsSuccess() { // Arrange var rule = CreateExactQuantityRule("TestRule", 5); var createResult = await _service.CreateValidationRuleAsync(rule); // Act var result = await _service.DeleteValidationRuleAsync(createResult.Data.RuleId); // Assert Assert.IsTrue(result.Succeeded); } [TestMethod] public async Task DeleteValidationRuleAsync_NonExistingRule_ReturnsFailure() { // Arrange var nonExistingRuleId = Guid.NewGuid(); // Act var result = await _service.DeleteValidationRuleAsync(nonExistingRuleId); // Assert Assert.IsFalse(result.Succeeded); Assert.AreEqual("RULE_NOT_FOUND", result.Code); } [TestMethod] public async Task GetValidationRulesAsync_NoFilters_ReturnsAllRules() { // Arrange await _service.CreateValidationRuleAsync(CreateExactQuantityRule("Rule1", 5)); await _service.CreateValidationRuleAsync(CreateMinimumQuantityRule("Rule2", 3)); await _service.CreateValidationRuleAsync(CreateRangeQuantityRule("Rule3", 2, 4)); // Act var result = await _service.GetValidationRulesAsync(); // Assert Assert.IsTrue(result.Succeeded); Assert.IsTrue(result.Data.Count >= 3); // At least the 3 rules we created plus default rules } [TestMethod] public async Task GetValidationRulesAsync_WithFilters_ReturnsFilteredRules() { // Arrange await _service.CreateValidationRuleAsync(CreateExactQuantityRule("Rule1", 5, "ProductA", 1)); await _service.CreateValidationRuleAsync(CreateMinimumQuantityRule("Rule2", 3, "ProductB", 2)); await _service.CreateValidationRuleAsync(CreateRangeQuantityRule("Rule3", 2, 4, "ProductA", 2)); // Act var result = await _service.GetValidationRulesAsync("ProductA", 2); // Assert Assert.IsTrue(result.Succeeded); Assert.AreEqual(1, result.Data.Count); // Only Rule3 matches both filters Assert.AreEqual("Rule3", result.Data.First().RuleName); } [TestMethod] public async Task GetValidationStatisticsAsync_WithHistory_ReturnsStatistics() { // Arrange var parts = CreateTestParts(3); var startTime = DateTime.UtcNow.AddHours(-1); var endTime = DateTime.UtcNow; // Perform some validations to generate history await _service.ValidateExactQuantityAsync(parts, 3); await _service.ValidateMinimumQuantityAsync(parts, 2); await _service.ValidateRangeQuantityAsync(parts, 2, 4); // Act var result = await _service.GetValidationStatisticsAsync(startTime, endTime); // Assert Assert.IsTrue(result.Succeeded); Assert.IsTrue(result.Data.TotalValidations >= 3); Assert.IsTrue(result.Data.ByValidationType.Count > 0); Assert.IsTrue(result.Data.OverallSuccessRate >= 0); } #region Helper Methods private static List CreateTestParts(int count) { var parts = new List(); for (int i = 0; i < count; i++) { parts.Add(new MappedPart { PartId = Guid.NewGuid(), PartCode = $"PART-{i:D3}", PartName = $"Test Part {i}", PartType = PartType.Required, OriginalClass = "component_a", MappingConfidence = 0.9, DetectionConfidence = 0.85, OverallConfidence = 0.875, BoundingBox = new BoundingBox { X = i * 10, Y = i * 10, Width = 50, Height = 50, Confidence = 0.85, Class = "component_a" }, PartAttributes = new Dictionary(), IsValid = true, MappingTimeUtc = DateTime.UtcNow }); } return parts; } private static List CreateTestPartsWithTypes() { return new List { new MappedPart { PartId = Guid.NewGuid(), PartCode = "PART-001", PartName = "Required Part 1", PartType = PartType.Required, OriginalClass = "component_a", MappingConfidence = 0.9, DetectionConfidence = 0.85, OverallConfidence = 0.875, BoundingBox = new BoundingBox { X = 0, Y = 0, Width = 50, Height = 50, Confidence = 0.85, Class = "component_a" }, PartAttributes = new Dictionary(), IsValid = true, MappingTimeUtc = DateTime.UtcNow }, new MappedPart { PartId = Guid.NewGuid(), PartCode = "PART-002", PartName = "Required Part 2", PartType = PartType.Required, OriginalClass = "component_a", MappingConfidence = 0.9, DetectionConfidence = 0.85, OverallConfidence = 0.875, BoundingBox = new BoundingBox { X = 60, Y = 0, Width = 50, Height = 50, Confidence = 0.85, Class = "component_a" }, PartAttributes = new Dictionary(), IsValid = true, MappingTimeUtc = DateTime.UtcNow }, new MappedPart { PartId = Guid.NewGuid(), PartCode = "PART-003", PartName = "Optional Part 1", PartType = PartType.Optional, OriginalClass = "component_b", MappingConfidence = 0.8, DetectionConfidence = 0.75, OverallConfidence = 0.775, BoundingBox = new BoundingBox { X = 120, Y = 0, Width = 50, Height = 50, Confidence = 0.75, Class = "component_b" }, PartAttributes = new Dictionary(), IsValid = true, MappingTimeUtc = DateTime.UtcNow } }; } private static List CreateTestPartsWithUniqueProperties(string propertyName, int count) { var parts = new List(); for (int i = 0; i < count; i++) { parts.Add(new MappedPart { PartId = Guid.NewGuid(), PartCode = $"PART-{i:D3}", PartName = $"Test Part {i}", PartType = PartType.Required, OriginalClass = "component_a", MappingConfidence = 0.9, DetectionConfidence = 0.85, OverallConfidence = 0.875, BoundingBox = new BoundingBox { X = i * 10, Y = i * 10, Width = 50, Height = 50, Confidence = 0.85, Class = "component_a" }, PartAttributes = new Dictionary { [propertyName] = $"UNIQUE-{i:D3}" }, IsValid = true, MappingTimeUtc = DateTime.UtcNow }); } return parts; } private static List CreateTestPartsWithDuplicates(string propertyName, int count) { var parts = new List(); for (int i = 0; i < count; i++) { parts.Add(new MappedPart { PartId = Guid.NewGuid(), PartCode = $"PART-{i:D3}", PartName = $"Test Part {i}", PartType = PartType.Required, OriginalClass = "component_a", MappingConfidence = 0.9, DetectionConfidence = 0.85, OverallConfidence = 0.875, BoundingBox = new BoundingBox { X = i * 10, Y = i * 10, Width = 50, Height = 50, Confidence = 0.85, Class = "component_a" }, PartAttributes = new Dictionary { [propertyName] = i < 2 ? "DUPLICATE-001" : $"UNIQUE-{i:D3}" // First two have same value }, IsValid = true, MappingTimeUtc = DateTime.UtcNow }); } return parts; } private static QuantityValidationRule CreateExactQuantityRule(string name, int expectedCount, string productType = "default", int layerNumber = 0) { return new QuantityValidationRule { RuleId = Guid.NewGuid(), RuleName = name, RuleDescription = $"Exact quantity rule for {name}", ProductTypeCode = productType, LayerNumber = layerNumber, ValidationType = QuantityValidationType.Exact, ExpectedCount = expectedCount, MinimumCount = expectedCount, MaximumCount = expectedCount, IsEnabled = true, Priority = 1, CreatedAtUtc = DateTime.UtcNow, UpdatedAtUtc = DateTime.UtcNow, Version = "1.0" }; } private static QuantityValidationRule CreateMinimumQuantityRule(string name, int minCount, string productType = "default", int layerNumber = 0) { return new QuantityValidationRule { RuleId = Guid.NewGuid(), RuleName = name, RuleDescription = $"Minimum quantity rule for {name}", ProductTypeCode = productType, LayerNumber = layerNumber, ValidationType = QuantityValidationType.Minimum, MinimumCount = minCount, ExpectedCount = minCount, IsEnabled = true, Priority = 2, CreatedAtUtc = DateTime.UtcNow, UpdatedAtUtc = DateTime.UtcNow, Version = "1.0" }; } private static QuantityValidationRule CreateRangeQuantityRule(string name, int minCount, int maxCount, string productType = "default", int layerNumber = 0) { return new QuantityValidationRule { RuleId = Guid.NewGuid(), RuleName = name, RuleDescription = $"Range quantity rule for {name}", ProductTypeCode = productType, LayerNumber = layerNumber, ValidationType = QuantityValidationType.Range, MinimumCount = minCount, MaximumCount = maxCount, ExpectedCount = (minCount + maxCount) / 2, IsEnabled = true, Priority = 3, CreatedAtUtc = DateTime.UtcNow, UpdatedAtUtc = DateTime.UtcNow, Version = "1.0" }; } #endregion }