1+ using Eddi ;
2+ using EddiConfigService . Configurations ;
3+ using Microsoft . VisualStudio . TestTools . UnitTesting ;
4+ using System ;
5+ using System . Globalization ;
6+ using System . IO ;
7+ using System . Net . Http ;
8+ using System . Reflection ;
9+ using System . Threading ;
10+ using Utilities ;
11+
12+ namespace Tests
13+ {
14+ [ TestClass , TestCategory ( "UnitTests" ) ]
15+ public class AppTests : TestBase
16+ {
17+ [ TestInitialize ]
18+ public void Init ( )
19+ {
20+ // Make test environment safe (disable telemetry, unit testing flags)
21+ MakeSafe ( ) ;
22+
23+ // Ensure log directory exists so Logging won't fail when tests inspect files
24+ Directory . CreateDirectory ( Constants . DATA_DIR ) ;
25+ }
26+
27+ [ TestCleanup ]
28+ public void Cleanup ( )
29+ {
30+ // Try to clean up any mutex left behind by tests
31+ try
32+ {
33+ App . eddiMutex ? . ReleaseMutex ( ) ;
34+ }
35+ catch
36+ {
37+ // ignore if current thread doesn't own it or already released
38+ }
39+ finally
40+ {
41+ App . eddiMutex ? . Dispose ( ) ;
42+ App . eddiMutex = null ;
43+ }
44+ }
45+
46+ [ TestMethod ]
47+ public void AlreadyRunning_ReturnsTrueWhenExternalMutexHeld ( )
48+ {
49+ // Ensure starting state - dispose any existing App mutex
50+ App . eddiMutex ? . Dispose ( ) ;
51+ App . eddiMutex = null ;
52+
53+ Mutex external = null ;
54+ try
55+ {
56+ // Create an external (simulated other process) mutex and take ownership
57+ external = new Mutex ( true , Constants . EDDI_SYSTEM_MUTEX_NAME , out bool externalOwner ) ;
58+ Assert . IsTrue ( externalOwner , "Test setup failed to obtain external mutex ownership" ) ;
59+
60+ // Now call AlreadyRunning which will create its own mutex; it should detect an existing owner
61+ var already = App . AlreadyRunning ( ) ;
62+ Assert . IsTrue ( already , "AlreadyRunning should return true when an external mutex exists" ) ;
63+ }
64+ finally
65+ {
66+ // Release & dispose external mutex
67+ try
68+ { external ? . ReleaseMutex ( ) ; }
69+ catch
70+ {
71+ // ignored
72+ }
73+
74+ external ? . Dispose ( ) ;
75+
76+ // Dispose the mutex created by AlreadyRunning (may not be owned by this thread)
77+ App . eddiMutex ? . Dispose ( ) ;
78+ App . eddiMutex = null ;
79+ }
80+ }
81+
82+ [ TestMethod ]
83+ public void ApplyAnyOverrideCulture_AppliesValidCulture ( )
84+ {
85+ var config = new EDDIConfiguration
86+ {
87+ OverrideCulture = "fr-FR"
88+ } ;
89+
90+ // Apply override
91+ App . ApplyAnyOverrideCulture ( config ) ;
92+
93+ // Default thread cultures and current thread culture should reflect override
94+ Assert . IsNotNull ( CultureInfo . DefaultThreadCurrentCulture ) ;
95+ Assert . AreEqual ( "fr-FR" , CultureInfo . DefaultThreadCurrentCulture . Name , "DefaultThreadCurrentCulture should be set to 'fr-FR'" ) ;
96+ Assert . AreEqual ( "fr-FR" , Thread . CurrentThread . CurrentCulture . Name , "Current thread culture should be set to 'fr-FR'" ) ;
97+ }
98+
99+ [ TestMethod ]
100+ public void ApplyAnyOverrideCulture_InvalidCultureDoesNotThrow_AndSetsDefaultToNull ( )
101+ {
102+ // Start with a known default so we can observe the fallback behavior
103+ CultureInfo backupDefault = CultureInfo . DefaultThreadCurrentCulture ;
104+
105+ var config = new EDDIConfiguration
106+ {
107+ OverrideCulture = "this-is-not-a-culture"
108+ } ;
109+
110+ // Should not throw
111+ App . ApplyAnyOverrideCulture ( config ) ;
112+
113+ // Per implementation, invalid culture triggers ApplyCulture(null) -> DefaultThreadCurrentCulture becomes null
114+ Assert . IsNull ( CultureInfo . DefaultThreadCurrentCulture , "DefaultThreadCurrentCulture should have been set to null when invalid override culture is provided" ) ;
115+
116+ // Restore original default to avoid affecting other tests
117+ CultureInfo . DefaultThreadCurrentCulture = backupDefault ;
118+ CultureInfo . DefaultThreadCurrentUICulture = backupDefault ;
119+ }
120+
121+ [ TestMethod ]
122+ public void CrashLogger_SuppressesRollbarInternalHttpExceptions ( )
123+ {
124+ // Build an HttpRequestException with a stack trace containing "Rollbar" by throwing from a helper method
125+ HttpRequestException httpEx = null ;
126+ try
127+ {
128+ ThrowHttpRequestExceptionWithRollbarInStack ( ) ;
129+ }
130+ catch ( HttpRequestException hre )
131+ {
132+ httpEx = hre ;
133+ }
134+ Assert . IsNotNull ( httpEx ) ;
135+
136+ // Wrap in AggregateException as the CrashLogger expects
137+ var agg = new AggregateException ( httpEx ) ;
138+
139+ // Invoke private static CrashLogger via reflection and ensure it doesn't throw
140+ var crashLogger = typeof ( App ) . GetMethod ( "CrashLogger" , BindingFlags . NonPublic | BindingFlags . Static ) ;
141+ Assert . IsNotNull ( crashLogger , "CrashLogger method not found via reflection" ) ;
142+
143+ // Should not throw
144+ crashLogger . Invoke ( null , new object [ ] { agg } ) ;
145+ }
146+
147+ [ TestMethod ]
148+ public void CrashLogger_LogsUnhandledException ( )
149+ {
150+ // Ensure we have a fresh log file to inspect
151+ var logFile = Path . Combine ( Constants . DATA_DIR , "eddi.log" ) ;
152+ if ( File . Exists ( logFile ) )
153+ {
154+ File . Delete ( logFile ) ;
155+ }
156+
157+ var crashLogger = typeof ( App ) . GetMethod ( "CrashLogger" , BindingFlags . NonPublic | BindingFlags . Static ) ;
158+ Assert . IsNotNull ( crashLogger ) ;
159+
160+ var ex = new Exception ( "TestCrashLogger" ) ;
161+
162+ // Invoke CrashLogger - it will call Logging.Error which writes asynchronously
163+ crashLogger . Invoke ( null , new object [ ] { ex } ) ;
164+
165+ // Wait for the background logging task to complete and write to file (polling with timeout)
166+ var sw = System . Diagnostics . Stopwatch . StartNew ( ) ;
167+ bool found = false ;
168+ while ( sw . Elapsed < TimeSpan . FromSeconds ( 5 ) )
169+ {
170+ if ( File . Exists ( logFile ) )
171+ {
172+ var contents = File . ReadAllText ( logFile ) ;
173+ if ( contents . Contains ( "Unhandled exception: TestCrashLogger." ) )
174+ {
175+ found = true ;
176+ break ;
177+ }
178+ }
179+ Thread . Sleep ( 100 ) ;
180+ }
181+
182+ Assert . IsTrue ( found , "CrashLogger did not write the expected message to the log file within the timeout" ) ;
183+ }
184+
185+ // Helper used to create an HttpRequestException whose stack trace contains the substring "Rollbar"
186+ private void ThrowHttpRequestExceptionWithRollbarInStack ( )
187+ {
188+ // Method name intentionally contains "Rollbar" so that the resulting stack trace includes that token.
189+ RollbarMarker ( ) ;
190+ }
191+
192+ private void RollbarMarker ( )
193+ {
194+ // Throw an HttpRequestException so its stack trace contains "RollbarMarker" (and thus "Rollbar")
195+ throw new HttpRequestException ( "simulated http exception" ) ;
196+ }
197+ }
198+ }
0 commit comments