using System; using System.IO; using System.Threading; using System.Threading.Tasks; using GermanApp.Infrastructure.Configuration; using GermanApp.Infrastructure.Services; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace GermanApp.Tests.Unit.Infrastructure.Services; [TestClass] public class VoskServiceTests { private Mock> _mockConfigOptions; private Mock> _mockLogger; private VoskConfig _config; private VoskService _service; private string _tempModelPath; [TestInitialize] public void Setup() { _mockConfigOptions = new Mock>(); _mockLogger = new Mock>(); // Create a temporary model directory for testing _tempModelPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); Directory.CreateDirectory(_tempModelPath); _config = new VoskConfig { PythonPath = "python3", ModelPath = _tempModelPath, SampleRate = 16000, TimeoutSeconds = 30, BeamWidth = 20 }; _mockConfigOptions.Setup(c => c.Value).Returns(_config); _service = new VoskService(_mockConfigOptions.Object, _mockLogger.Object); } [TestCleanup] public void Cleanup() { // Clean up temp directory try { if (Directory.Exists(_tempModelPath)) { Directory.Delete(_tempModelPath, true); } } catch { } } [TestMethod] public void Constructor_WithValidConfig_CreatesService() { // Act & Assert Assert.IsNotNull(_service); } [TestMethod] public void Constructor_WithEmptyModelPath_ThrowsInvalidOperationException() { // Arrange var invalidConfig = new VoskConfig { PythonPath = "python3", ModelPath = "", SampleRate = 16000 }; _mockConfigOptions.Setup(c => c.Value).Returns(invalidConfig); // Act try { new VoskService(_mockConfigOptions.Object, _mockLogger.Object); Assert.Fail("Expected InvalidOperationException was not thrown"); } catch (InvalidOperationException) { // Expected } } [TestMethod] public void Constructor_WithNonExistentModelPath_ThrowsDirectoryNotFoundException() { // Arrange var invalidConfig = new VoskConfig { PythonPath = "python3", ModelPath = "/nonexistent/path/to/model", SampleRate = 16000 }; _mockConfigOptions.Setup(c => c.Value).Returns(invalidConfig); // Act try { new VoskService(_mockConfigOptions.Object, _mockLogger.Object); Assert.Fail("Expected DirectoryNotFoundException was not thrown"); } catch (DirectoryNotFoundException) { // Expected } } [TestMethod] public async Task RecognizeSpeechAsync_WithValidAudio_CallsPythonProcess() { // Arrange var audioBytes = new byte[1000]; for (int i = 0; i < audioBytes.Length; i++) { audioBytes[i] = (byte)(i % 256); } var cancellationToken = CancellationToken.None; // Note: We can't easily test the actual Python execution without Vosk installed // This test verifies the basic validation works // Act & Assert // This will throw if the model is not properly configured for actual execution // But we're testing the validation logic try { var result = await _service.RecognizeSpeechAsync(audioBytes, 16000, null, cancellationToken); // If we get here, the validation passed } catch (Exception ex) when (ex is TimeoutException || ex is InvalidOperationException) { // Expected when Python/Vosk is not installed // The important thing is that basic validation passed } } [TestMethod] public async Task RecognizeSpeechAsync_WithEmptyAudio_ThrowsArgumentException() { // Arrange var emptyAudio = new byte[0]; var cancellationToken = CancellationToken.None; // Act try { await _service.RecognizeSpeechAsync(emptyAudio, 16000, null, cancellationToken); Assert.Fail("Expected ArgumentException was not thrown"); } catch (ArgumentException) { // Expected } } [TestMethod] public async Task RecognizeSpeechAsync_WithNullAudio_ThrowsArgumentException() { // Arrange byte[] nullAudio = null!; var cancellationToken = CancellationToken.None; // Act try { await _service.RecognizeSpeechAsync(nullAudio, 16000, null, cancellationToken); Assert.Fail("Expected ArgumentException was not thrown"); } catch (ArgumentException) { // Expected } } [TestMethod] public async Task RecognizeSpeechAsync_WithDifferentSampleRate_LogsWarning() { // Arrange var audioBytes = new byte[1000]; for (int i = 0; i < audioBytes.Length; i++) { audioBytes[i] = (byte)(i % 256); } var wrongSampleRate = 44100; // Different from config's 16000 var cancellationToken = CancellationToken.None; // Note: We can't easily verify the log message without more complex setup // This test verifies it doesn't throw with wrong sample rate // Act try { var result = await _service.RecognizeSpeechAsync(audioBytes, wrongSampleRate, null, cancellationToken); // If we get here, validation passed (though Python execution may fail) } catch (Exception ex) when (ex is TimeoutException || ex is InvalidOperationException) { // Expected when Python/Vosk is not installed } } [TestMethod] public async Task RecognizeSpeechFromFileAsync_WithNonExistentFile_ThrowsFileNotFoundException() { // Arrange var nonExistentFile = "/nonexistent/file.wav"; var cancellationToken = CancellationToken.None; // Act try { await _service.RecognizeSpeechFromFileAsync(nonExistentFile, cancellationToken); Assert.Fail("Expected FileNotFoundException was not thrown"); } catch (FileNotFoundException) { // Expected } } [TestMethod] public async Task RecognizeSpeechFromStreamAsync_WithNullStream_ThrowsArgumentException() { // Arrange Stream nullStream = null!; var cancellationToken = CancellationToken.None; // Act try { await _service.RecognizeSpeechFromStreamAsync(nullStream, 16000, cancellationToken); Assert.Fail("Expected ArgumentException was not thrown"); } catch (ArgumentException) { // Expected } } [TestMethod] public async Task RecognizeSpeechFromStreamAsync_WithNonReadableStream_ThrowsArgumentException() { // Arrange var nonReadableStream = new MemoryStream(); nonReadableStream.Close(); // Make it non-readable var cancellationToken = CancellationToken.None; // Act try { await _service.RecognizeSpeechFromStreamAsync(nonReadableStream, 16000, cancellationToken); Assert.Fail("Expected ArgumentException was not thrown"); } catch (ArgumentException) { // Expected } } [TestMethod] public async Task TestModelAsync_WithValidModel_ReturnsTrue() { // Arrange var cancellationToken = CancellationToken.None; // Act // This will test if the model directory exists var result = await _service.TestModelAsync(cancellationToken); // Assert // Should return true since we created the temp directory // Note: Actual recognition test may fail if Vosk is not installed Assert.IsTrue(result); } [TestMethod] public async Task GetModelInfoAsync_ReturnsModelInfo() { // Act var result = await _service.GetModelInfoAsync(); // Assert Assert.IsNotNull(result); Assert.IsNotNull(result.ModelName); Assert.IsNotNull(result.ModelPath); Assert.AreEqual(_tempModelPath, result.ModelPath); } [TestMethod] public void VoskConfig_Validate_WithValidConfig_Passes() { // Arrange var config = new VoskConfig { PythonPath = "python3", ModelPath = "/path/to/model", SampleRate = 16000, TimeoutSeconds = 30, BeamWidth = 20 }; // Act & Assert // Should not throw config.Validate(); } [TestMethod] public void VoskConfig_Validate_WithEmptyPythonPath_Throws() { // Arrange var config = new VoskConfig { PythonPath = "", ModelPath = "/path/to/model", SampleRate = 16000 }; // Act try { config.Validate(); Assert.Fail("Expected ArgumentException was not thrown"); } catch (ArgumentException) { // Expected } } [TestMethod] public void VoskConfig_Validate_WithInvalidSampleRate_Throws() { // Arrange var config = new VoskConfig { PythonPath = "python3", ModelPath = "/path/to/model", SampleRate = 0 }; // Act try { config.Validate(); Assert.Fail("Expected ArgumentException was not thrown"); } catch (ArgumentException) { // Expected } } [TestMethod] public void VoskConfig_Validate_WithInvalidTimeout_Throws() { // Arrange var config = new VoskConfig { PythonPath = "python3", ModelPath = "/path/to/model", SampleRate = 16000, TimeoutSeconds = 0 }; // Act try { config.Validate(); Assert.Fail("Expected ArgumentException was not thrown"); } catch (ArgumentException) { // Expected } } [TestMethod] public void ParseVoskOutput_WithJsonText_ReturnsText() { // This is a private method, tested indirectly // We can't test it directly without reflection } }