using GitConverter.Lib.Logging; using GitConverter.TestsApp.Helpers; namespace GitConverter.TestsApp.Logging { /// /// Integration tests for file-based logging using the Log.SetFile / Log.Enable APIs. /// /// /// - These tests write to temporary files and therefore are slightly slower and non-CPU-bound. /// - They disable global logging in Dispose to avoid interfering with other tests. /// - Keep these tests isolated (consider placing in a separate collection to avoid parallel execution with other tests that modify global logging). /// public class FileLoggingTests : IDisposable { private readonly string _tempDir; public FileLoggingTests() { _tempDir = Path.Combine(Path.GetTempPath(), "GitConverter.Tests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); } public void Dispose() { try { // restore logging to no-op to avoid interfering with other tests Log.Disable(); if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true); } catch { /* best-effort cleanup */ } } // Small helper to allow log4net to flush writes to disk on CI machines private static void WaitForFileWrite(string path, int attempts = 10, int delayMs = 100) { for (int i = 0; i < attempts; i++) { if (File.Exists(path)) return; Thread.Sleep(delayMs); } } /// /// Verifies Log.SetFile + Log.Enable writes logged messages to the configured file. /// /// /// - Arrange: set file path under a temp directory and enable logging. /// - Act: write messages using the static Log API. /// - Assert: the log file exists and contains the messages. /// - Cleanup: disable logging in the test teardown. /// [Fact] public void Log_SetFile_WritesMessagesToFile() { var logFile = Path.Combine(_tempDir, "factory.log"); // configure file logger with default log level (All) Log.SetFile(logFile); Log.Enable(); // safe to call; SetFile already configured but call to be explicit // write some messages Log.Info("Integration test - info message"); Log.Debug("Integration test - debug message"); Log.Warn("Integration test - warn message"); Log.Error("Integration test - error message", null); // give log4net a moment to flush the file WaitForFileWrite(logFile); // assert file created and contains our messages Assert.True(File.Exists(logFile), "Log file was not created."); // Use test helper that opens the file with FileShare.ReadWrite and retries if locked. var content = FileTestHelpers.ReadAllTextAllowLocked(logFile); Assert.Contains("Integration test - info message", content); Assert.Contains("Integration test - debug message", content); Assert.Contains("Integration test - warn message", content); Assert.Contains("Integration test - error message", content); // cleanup Log.Disable(); } /// /// Verifies that Log.SetFile with "Info" level filters out DEBUG messages. /// [Fact] public void Log_SetFile_WithInfoLevel_FiltersDebugMessages() { var logFile = Path.Combine(_tempDir, "info-level.log"); // configure file logger with Info level Log.SetFile(logFile, "Info"); Log.Enable(); // write messages at different levels Log.Debug("This DEBUG message should be filtered"); Log.Info("This INFO message should appear"); Log.Warn("This WARN message should appear"); Log.Error("This ERROR message should appear"); WaitForFileWrite(logFile); Assert.True(File.Exists(logFile), "Log file was not created."); var content = FileTestHelpers.ReadAllTextAllowLocked(logFile); // DEBUG should be filtered out Assert.DoesNotContain("This DEBUG message should be filtered", content); // INFO, WARN, ERROR should be present Assert.Contains("This INFO message should appear", content); Assert.Contains("This WARN message should appear", content); Assert.Contains("This ERROR message should appear", content); Log.Disable(); } /// /// Verifies that Log.SetFile with "Warn" level filters out DEBUG and INFO messages. /// [Fact] public void Log_SetFile_WithWarnLevel_FiltersDebugAndInfo() { var logFile = Path.Combine(_tempDir, "warn-level.log"); // configure file logger with Warn level Log.SetFile(logFile, "Warn"); Log.Enable(); Log.Debug("DEBUG should be filtered"); Log.Info("INFO should be filtered"); Log.Warn("WARN should appear"); Log.Error("ERROR should appear"); WaitForFileWrite(logFile); Assert.True(File.Exists(logFile), "Log file was not created."); var content = FileTestHelpers.ReadAllTextAllowLocked(logFile); // DEBUG and INFO should be filtered Assert.DoesNotContain("DEBUG should be filtered", content); Assert.DoesNotContain("INFO should be filtered", content); // WARN and ERROR should be present Assert.Contains("WARN should appear", content); Assert.Contains("ERROR should appear", content); Log.Disable(); } /// /// Verifies that Log.SetFile with "Error" level filters out DEBUG, INFO, and WARN messages. /// [Fact] public void Log_SetFile_WithErrorLevel_FiltersDebugInfoWarn() { var logFile = Path.Combine(_tempDir, "error-level.log"); // configure file logger with Error level Log.SetFile(logFile, "Error"); Log.Enable(); Log.Debug("DEBUG should be filtered"); Log.Info("INFO should be filtered"); Log.Warn("WARN should be filtered"); Log.Error("ERROR should appear"); WaitForFileWrite(logFile); Assert.True(File.Exists(logFile), "Log file was not created."); var content = FileTestHelpers.ReadAllTextAllowLocked(logFile); // DEBUG, INFO, WARN should be filtered Assert.DoesNotContain("DEBUG should be filtered", content); Assert.DoesNotContain("INFO should be filtered", content); Assert.DoesNotContain("WARN should be filtered", content); // ERROR should be present Assert.Contains("ERROR should appear", content); Log.Disable(); } /// /// Verifies that Log.SetFile with "Off" level produces no log output. /// [Fact] public void Log_SetFile_WithOffLevel_ProducesNoOutput() { var logFile = Path.Combine(_tempDir, "off-level.log"); // configure file logger with Off level Log.SetFile(logFile, "Off"); Log.Enable(); Log.Debug("DEBUG should not appear"); Log.Info("INFO should not appear"); Log.Warn("WARN should not appear"); Log.Error("ERROR should not appear"); WaitForFileWrite(logFile); // File may be created but should be empty or contain only headers if (File.Exists(logFile)) { var content = FileTestHelpers.ReadAllTextAllowLocked(logFile); // No log messages should be present Assert.DoesNotContain("DEBUG should not appear", content); Assert.DoesNotContain("INFO should not appear", content); Assert.DoesNotContain("WARN should not appear", content); Assert.DoesNotContain("ERROR should not appear", content); } Log.Disable(); } /// /// Verifies that Log.SetFile with "Debug" level logs all messages (equivalent to "All"). /// [Fact] public void Log_SetFile_WithDebugLevel_LogsAllMessages() { var logFile = Path.Combine(_tempDir, "debug-level.log"); // configure file logger with Debug level Log.SetFile(logFile, "Debug"); Log.Enable(); Log.Debug("DEBUG message"); Log.Info("INFO message"); Log.Warn("WARN message"); Log.Error("ERROR message"); WaitForFileWrite(logFile); Assert.True(File.Exists(logFile), "Log file was not created."); var content = FileTestHelpers.ReadAllTextAllowLocked(logFile); // All messages should be present Assert.Contains("DEBUG message", content); Assert.Contains("INFO message", content); Assert.Contains("WARN message", content); Assert.Contains("ERROR message", content); Log.Disable(); } /// /// Verifies that Log.SetFile with invalid log level defaults to "All". /// [Fact] public void Log_SetFile_WithInvalidLevel_DefaultsToAll() { var logFile = Path.Combine(_tempDir, "invalid-level.log"); // configure file logger with invalid level Log.SetFile(logFile, "InvalidLevel"); Log.Enable(); Log.Debug("DEBUG should appear with fallback"); Log.Info("INFO should appear with fallback"); WaitForFileWrite(logFile); Assert.True(File.Exists(logFile), "Log file was not created."); var content = FileTestHelpers.ReadAllTextAllowLocked(logFile); // Should default to ALL, so DEBUG and INFO should be present Assert.Contains("DEBUG should appear with fallback", content); Assert.Contains("INFO should appear with fallback", content); Log.Disable(); } /// /// Verifies that Log.SetFile with case-insensitive log level works correctly. /// [Fact] public void Log_SetFile_WithCaseInsensitiveLevel_Works() { var logFile = Path.Combine(_tempDir, "case-insensitive.log"); // configure file logger with mixed-case level Log.SetFile(logFile, "iNfO"); Log.Enable(); Log.Debug("DEBUG should be filtered"); Log.Info("INFO should appear"); Log.Warn("WARN should appear"); WaitForFileWrite(logFile); Assert.True(File.Exists(logFile), "Log file was not created."); var content = FileTestHelpers.ReadAllTextAllowLocked(logFile); Assert.DoesNotContain("DEBUG should be filtered", content); Assert.Contains("INFO should appear", content); Assert.Contains("WARN should appear", content); Log.Disable(); } /// /// Verifies that Log.SetFile creates UTF-8 encoded file supporting international characters. /// [Fact] public void Log_SetFile_SupportsUtf8Encoding() { var logFile = Path.Combine(_tempDir, "utf8-test.log"); Log.SetFile(logFile); Log.Enable(); // Log messages with international characters and emoji Log.Info("Test with Hebrew: שלום"); Log.Info("Test with Arabic: مرحبا"); Log.Info("Test with Chinese: 你好"); Log.Info("Test with emoji: ✅ 🚀 ❌"); WaitForFileWrite(logFile); Assert.True(File.Exists(logFile), "Log file was not created."); // Read file with UTF-8 encoding var content = File.ReadAllText(logFile, System.Text.Encoding.UTF8); Assert.Contains("שלום", content); Assert.Contains("مرحبا", content); Assert.Contains("你好", content); Assert.Contains("✅", content); Assert.Contains("🚀", content); Assert.Contains("❌", content); Log.Disable(); } /// /// Verifies that Log.SetFile with exception logging writes stack traces. /// [Fact] public void Log_SetFile_LogsExceptionsWithStackTrace() { var logFile = Path.Combine(_tempDir, "exception-test.log"); Log.SetFile(logFile, "Error"); Log.Enable(); try { throw new InvalidOperationException("Test exception for logging"); } catch (Exception ex) { Log.Error("An error occurred during test", ex); } WaitForFileWrite(logFile); Assert.True(File.Exists(logFile), "Log file was not created."); var content = FileTestHelpers.ReadAllTextAllowLocked(logFile); Assert.Contains("An error occurred during test", content); Assert.Contains("System.InvalidOperationException", content); Assert.Contains("Test exception for logging", content); // Stack trace should be present Assert.Contains("at ", content); // Stack trace frames start with "at " Log.Disable(); } /// /// Verifies that multiple calls to Log.SetFile reset configuration properly. /// [Fact] public void Log_SetFile_CalledMultipleTimes_ResetsConfiguration() { var logFile1 = Path.Combine(_tempDir, "first.log"); var logFile2 = Path.Combine(_tempDir, "second.log"); // First configuration Log.SetFile(logFile1, "Info"); Log.Enable(); Log.Info("Message to first log"); WaitForFileWrite(logFile1); Assert.True(File.Exists(logFile1)); // Second configuration (should reset) Log.SetFile(logFile2, "Warn"); Log.Enable(); Log.Info("INFO should be filtered in second log"); Log.Warn("Message to second log"); WaitForFileWrite(logFile2); Assert.True(File.Exists(logFile2)); var content1 = FileTestHelpers.ReadAllTextAllowLocked(logFile1); var content2 = FileTestHelpers.ReadAllTextAllowLocked(logFile2); // First log should contain INFO Assert.Contains("Message to first log", content1); // Second log should filter INFO but contain WARN Assert.DoesNotContain("INFO should be filtered in second log", content2); Assert.Contains("Message to second log", content2); Log.Disable(); } } }