using System; using System.IO; using System.Threading.Tasks; using Moq; using Serilog; using Xunit; using XplorePlane.Models; using XplorePlane.Services; using XplorePlane.Services.AppState; using XplorePlane.Services.Recipe; namespace XplorePlane.Tests.Services { /// /// RecipeService 单元测试。 /// 验证配方创建、步骤记录、文件加载错误处理和名称校验。 /// public class RecipeServiceTests { private readonly Mock _mockAppState; private readonly Mock _mockPipeline; private readonly Mock _mockLogger; private readonly RecipeService _service; public RecipeServiceTests() { _mockAppState = new Mock(); _mockPipeline = new Mock(); _mockLogger = new Mock(); // Setup default state returns _mockAppState.Setup(s => s.MotionState).Returns(MotionState.Default); _mockAppState.Setup(s => s.RaySourceState).Returns(RaySourceState.Default); _mockAppState.Setup(s => s.DetectorState).Returns(DetectorState.Default); _service = new RecipeService( _mockAppState.Object, _mockPipeline.Object, _mockLogger.Object); } // ── CreateRecipe 验证 ── [Fact] public void CreateRecipe_ValidName_ReturnsEmptyRecipe() { var recipe = _service.CreateRecipe("TestRecipe"); Assert.Equal("TestRecipe", recipe.Name); Assert.Empty(recipe.Steps); Assert.NotEqual(Guid.Empty, recipe.Id); Assert.True(recipe.CreatedAt <= DateTime.UtcNow); Assert.True(recipe.UpdatedAt <= DateTime.UtcNow); } [Fact] public void CreateRecipe_EmptyName_ThrowsArgumentException() { Assert.Throws(() => _service.CreateRecipe(string.Empty)); } [Fact] public void CreateRecipe_NullName_ThrowsArgumentException() { Assert.Throws(() => _service.CreateRecipe(null!)); } [Fact] public void CreateRecipe_WhitespaceName_ThrowsArgumentException() { Assert.Throws(() => _service.CreateRecipe(" ")); } // ── RecordCurrentStep 验证 ── [Fact] public void RecordCurrentStep_CapturesCurrentState() { var motionState = new MotionState(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); var raySourceState = new RaySourceState(true, 120.0, 80.0); var detectorState = new DetectorState(true, true, 30.0, "2048x2048"); _mockAppState.Setup(s => s.MotionState).Returns(motionState); _mockAppState.Setup(s => s.RaySourceState).Returns(raySourceState); _mockAppState.Setup(s => s.DetectorState).Returns(detectorState); var recipe = _service.CreateRecipe("TestRecipe"); var pipeline = new PipelineModel { Name = "TestPipeline" }; var step = _service.RecordCurrentStep(recipe, pipeline); Assert.Equal(0, step.StepIndex); Assert.Same(motionState, step.MotionState); Assert.Same(raySourceState, step.RaySourceState); Assert.Same(detectorState, step.DetectorState); Assert.Same(pipeline, step.Pipeline); } // ── LoadAsync 错误处理 ── [Fact] public async Task LoadAsync_FileNotExists_ThrowsFileNotFoundException() { var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".json"); await Assert.ThrowsAsync( () => _service.LoadAsync(nonExistentPath)); } [Fact] public async Task LoadAsync_InvalidJson_ThrowsInvalidDataException() { var tempFile = Path.GetTempFileName(); try { await File.WriteAllTextAsync(tempFile, "{ this is not valid json !!! }"); await Assert.ThrowsAsync( () => _service.LoadAsync(tempFile)); } finally { if (File.Exists(tempFile)) File.Delete(tempFile); } } } }