using System; using NUnit.Framework; using Moq; using NcProgramManager.Cnc; using NcProgramManager.Cnc.Models; using NcProgramManager.Cnc.Mazak; namespace NcProgramManager.Tests.Unit { [TestFixture] public class MazakMachineTests { private MazakConnectionConfig _config; private Mock _ftpMock; private Mock _validatorMock; private MazakMachine _machine; [SetUp] public void SetUp() { _config = new MazakConnectionConfig { Name = "TestMazak", IpAddress = "192.168.1.4", Port = 21, ProgramDirectory = "/PRG/" }; _ftpMock = new Mock(MockBehavior.Strict); // Always-ok validator: Mazak addresses files verbatim (passthrough), // so validation must not interfere with path/verbatim assertions. _validatorMock = new Mock(); _validatorMock.Setup(v => v.Validate(It.IsAny(), It.IsAny())) .Returns(ValidationResult.Ok()); _machine = new MazakMachine(_config, _ftpMock.Object, _validatorMock.Object); } [TearDown] public void TearDown() { _machine?.Dispose(); } // ConnectAsync [Test] public void ConnectAsync_PingTrue_StateConnectedAndResultOk() { _ftpMock.Setup(f => f.Ping()).Returns(true); var result = _machine.ConnectAsync().GetAwaiter().GetResult(); Assert.That(result.Success, Is.True); Assert.That(result.Value, Is.True); Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected)); } [Test] public void ConnectAsync_PingFalse_StateFaultedAndResultFail() { _ftpMock.Setup(f => f.Ping()).Returns(false); var result = _machine.ConnectAsync().GetAwaiter().GetResult(); Assert.That(result.Success, Is.False); Assert.That(_machine.State, Is.EqualTo(ConnectionState.Faulted)); } // ReadProgramAsync - verbatim filename, NO extension appended [Test] public void ReadProgramAsync_FullPath_CallsDownloadFileWithExactPath() { _ftpMock.Setup(f => f.Ping()).Returns(true); _ftpMock.Setup(f => f.DownloadFile("/PRG/1234")).Returns("EIA_DATA"); _machine.ConnectAsync().GetAwaiter().GetResult(); var result = _machine.ReadProgramAsync("/PRG/1234").GetAwaiter().GetResult(); Assert.That(result.Success, Is.True); Assert.That(result.Value, Is.EqualTo("EIA_DATA")); _ftpMock.Verify(f => f.DownloadFile("/PRG/1234"), Times.Once); } [Test] public void ReadProgramAsync_BareIdentifier_ResolvesWithDirectoryNoExtension() { _ftpMock.Setup(f => f.Ping()).Returns(true); // Bare "1234" -> ProgramDirectory + "1234" verbatim, NO .eia / extension. _ftpMock.Setup(f => f.DownloadFile("/PRG/1234")).Returns("DATA"); _machine.ConnectAsync().GetAwaiter().GetResult(); _machine.ReadProgramAsync("1234").GetAwaiter().GetResult(); _ftpMock.Verify(f => f.DownloadFile("/PRG/1234"), Times.Once); // Must NOT append any extension (e.g. .eia / .EIA). _ftpMock.Verify(f => f.DownloadFile(It.Is(s => s.Contains("."))), Times.Never); } // WriteProgramAsync [Test] public void WriteProgramAsync_BareIdentifier_ResolvesWithDirectory_SymmetricWithRead() { // ISS-020 symmetry regression: write("1234") must upload to the SAME // resolved path that read("1234") downloads from ("/PRG/1234"), so the // roundtrip targets one location. Path comes from the identifier verbatim, // NOT from a directory combined with program.Name. const string readPath = "/PRG/1234"; // path used by ReadProgramAsync("1234") _ftpMock.Setup(f => f.Ping()).Returns(true); _ftpMock.Setup(f => f.UploadFile(readPath, "CONTENT")); _machine.ConnectAsync().GetAwaiter().GetResult(); // program.Name deliberately differs from the identifier to prove the path // is built from the identifier ("1234"), not from program.Name. var prog = new CncProgram { Name = "O0001", Content = "CONTENT" }; var result = _machine.WriteProgramAsync("1234", prog).GetAwaiter().GetResult(); Assert.That(result.Success, Is.True); // write-path == read-path for identical input -> symmetry guard. _ftpMock.Verify(f => f.UploadFile(readPath, "CONTENT"), Times.Once); } [Test] public void WriteProgramAsync_FullPath_CallsUploadFileWithExactPath() { _ftpMock.Setup(f => f.Ping()).Returns(true); _ftpMock.Setup(f => f.UploadFile("/PRG/1234", "CONTENT")); _machine.ConnectAsync().GetAwaiter().GetResult(); var prog = new CncProgram { Name = "1234", Content = "CONTENT" }; var result = _machine.WriteProgramAsync("/PRG/1234", prog).GetAwaiter().GetResult(); Assert.That(result.Success, Is.True); _ftpMock.Verify(f => f.UploadFile("/PRG/1234", "CONTENT"), Times.Once); } [Test] public void WriteProgramAsync_LetterName_VerbatimPassthroughNoRename() { // Passthrough proof: a letter name "MYPART.NC" is uploaded to // ProgramDirectory + "MYPART.NC" UNCHANGED - no rename, no O#### enforcement, // no extension mangling. Validator is always-ok (Mazak passthrough). _ftpMock.Setup(f => f.Ping()).Returns(true); _ftpMock.Setup(f => f.UploadFile("/PRG/MYPART.NC", "CONTENT")); _machine.ConnectAsync().GetAwaiter().GetResult(); var prog = new CncProgram { Name = "MYPART.NC", Content = "CONTENT" }; var result = _machine.WriteProgramAsync("MYPART.NC", prog).GetAwaiter().GetResult(); Assert.That(result.Success, Is.True); _ftpMock.Verify(f => f.UploadFile("/PRG/MYPART.NC", "CONTENT"), Times.Once); } [Test] public void WriteProgramAsync_EmptyContent_FailsWithoutCallingUploadFile() { _ftpMock.Setup(f => f.Ping()).Returns(true); _machine.ConnectAsync().GetAwaiter().GetResult(); var prog = new CncProgram { Name = "1234", Content = "" }; var result = _machine.WriteProgramAsync("/PRG/1234", prog).GetAwaiter().GetResult(); Assert.That(result.Success, Is.False); _ftpMock.Verify(f => f.UploadFile(It.IsAny(), It.IsAny()), Times.Never); } [Test] public void WriteProgramAsync_NullProgram_FailsWithoutCallingUploadFile() { _ftpMock.Setup(f => f.Ping()).Returns(true); _machine.ConnectAsync().GetAwaiter().GetResult(); var result = _machine.WriteProgramAsync("/PRG/1234", null).GetAwaiter().GetResult(); Assert.That(result.Success, Is.False); _ftpMock.Verify(f => f.UploadFile(It.IsAny(), It.IsAny()), Times.Never); } // DeleteProgramAsync [Test] public void DeleteProgramAsync_BareIdentifier_ResolvesWithDirectory_SymmetricWithReadWrite() { // ISS-020 symmetry regression: delete("1234") must target the SAME // resolved path read/write use for "1234" ("/PRG/1234"). Path comes from // ResolvePath(identifier), NOT the raw identifier verbatim. _ftpMock.Setup(f => f.Ping()).Returns(true); _ftpMock.Setup(f => f.DeleteFile("/PRG/1234")); _machine.ConnectAsync().GetAwaiter().GetResult(); var result = _machine.DeleteProgramAsync("1234").GetAwaiter().GetResult(); Assert.That(result.Success, Is.True); _ftpMock.Verify(f => f.DeleteFile("/PRG/1234"), Times.Once); } // ReadActiveProgramAsync - not supported via FTP [Test] public void ReadActiveProgramAsync_ReturnsFailNotSupported() { var result = _machine.ReadActiveProgramAsync().GetAwaiter().GetResult(); Assert.That(result.Success, Is.False); } // SelectMainProgramAsync - not supported via FTP [Test] public void SelectMainProgramAsync_ReturnsFailNotSupported() { var result = _machine.SelectMainProgramAsync("/PRG/1234").GetAwaiter().GetResult(); Assert.That(result.Success, Is.False); } } }