Skip to content

Commit b760d71

Browse files
committed
Add tests for OTA functionality
1 parent 233e143 commit b760d71

4 files changed

Lines changed: 1301 additions & 0 deletions

File tree

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
// Copyright (c) 2025 Nikolaos Protopapas
2+
// Licensed under the MIT License
3+
4+
using LocalizationManager.JsonLocalization.Ota;
5+
using Xunit;
6+
7+
namespace LocalizationManager.Tests.UnitTests.JsonLocalization;
8+
9+
public class CircuitBreakerTests
10+
{
11+
#region State Transition Tests
12+
13+
[Fact]
14+
public void InitialState_IsClosed()
15+
{
16+
// Arrange & Act
17+
var circuitBreaker = new CircuitBreaker(threshold: 3, timeout: TimeSpan.FromSeconds(30));
18+
19+
// Assert
20+
Assert.Equal(CircuitState.Closed, circuitBreaker.State);
21+
}
22+
23+
[Fact]
24+
public void RecordSuccess_InClosedState_StaysClosed()
25+
{
26+
// Arrange
27+
var circuitBreaker = new CircuitBreaker(threshold: 3);
28+
29+
// Act
30+
circuitBreaker.RecordSuccess();
31+
32+
// Assert
33+
Assert.Equal(CircuitState.Closed, circuitBreaker.State);
34+
}
35+
36+
[Fact]
37+
public void RecordFailure_BelowThreshold_StaysClosed()
38+
{
39+
// Arrange
40+
var circuitBreaker = new CircuitBreaker(threshold: 3);
41+
42+
// Act
43+
circuitBreaker.RecordFailure();
44+
circuitBreaker.RecordFailure();
45+
46+
// Assert (2 failures, threshold is 3)
47+
Assert.Equal(CircuitState.Closed, circuitBreaker.State);
48+
}
49+
50+
[Fact]
51+
public void RecordFailure_ReachesThreshold_OpensCircuit()
52+
{
53+
// Arrange
54+
var circuitBreaker = new CircuitBreaker(threshold: 3);
55+
56+
// Act
57+
circuitBreaker.RecordFailure();
58+
circuitBreaker.RecordFailure();
59+
circuitBreaker.RecordFailure();
60+
61+
// Assert
62+
Assert.Equal(CircuitState.Open, circuitBreaker.State);
63+
}
64+
65+
[Fact]
66+
public void RecordSuccess_ResetsFailureCount()
67+
{
68+
// Arrange
69+
var circuitBreaker = new CircuitBreaker(threshold: 3);
70+
71+
// Act
72+
circuitBreaker.RecordFailure();
73+
circuitBreaker.RecordFailure();
74+
circuitBreaker.RecordSuccess(); // Reset
75+
circuitBreaker.RecordFailure();
76+
circuitBreaker.RecordFailure();
77+
78+
// Assert (only 2 failures after reset, should stay closed)
79+
Assert.Equal(CircuitState.Closed, circuitBreaker.State);
80+
}
81+
82+
[Fact]
83+
public void RecordSuccess_AfterTimeout_ClosesCircuit()
84+
{
85+
// Arrange
86+
var circuitBreaker = new CircuitBreaker(threshold: 1, timeout: TimeSpan.FromMilliseconds(1));
87+
88+
// Act - Open the circuit
89+
circuitBreaker.RecordFailure();
90+
Assert.Equal(CircuitState.Open, circuitBreaker.State);
91+
92+
// Wait for recovery time
93+
Thread.Sleep(10);
94+
95+
// Record success (state transitions to HalfOpen, then to Closed)
96+
circuitBreaker.RecordSuccess();
97+
98+
// Assert
99+
Assert.Equal(CircuitState.Closed, circuitBreaker.State);
100+
}
101+
102+
[Fact]
103+
public void RecordFailure_InHalfOpenState_ReopensCircuit()
104+
{
105+
// Arrange
106+
var circuitBreaker = new CircuitBreaker(threshold: 1, timeout: TimeSpan.FromMilliseconds(1));
107+
108+
// Act - Open the circuit
109+
circuitBreaker.RecordFailure();
110+
Assert.Equal(CircuitState.Open, circuitBreaker.State);
111+
112+
// Wait for recovery time (transitions to HalfOpen)
113+
Thread.Sleep(10);
114+
Assert.Equal(CircuitState.HalfOpen, circuitBreaker.State);
115+
116+
// Record another failure (should re-open)
117+
circuitBreaker.RecordFailure();
118+
119+
// Assert
120+
Assert.Equal(CircuitState.Open, circuitBreaker.State);
121+
}
122+
123+
#endregion
124+
125+
#region IsAllowed Tests
126+
127+
[Fact]
128+
public void IsAllowed_InClosedState_ReturnsTrue()
129+
{
130+
// Arrange
131+
var circuitBreaker = new CircuitBreaker(threshold: 3);
132+
133+
// Act & Assert
134+
Assert.True(circuitBreaker.IsAllowed);
135+
}
136+
137+
[Fact]
138+
public void IsAllowed_InOpenState_ReturnsFalse()
139+
{
140+
// Arrange
141+
var circuitBreaker = new CircuitBreaker(threshold: 1, timeout: TimeSpan.FromHours(1));
142+
143+
// Act
144+
circuitBreaker.RecordFailure();
145+
146+
// Assert
147+
Assert.Equal(CircuitState.Open, circuitBreaker.State);
148+
Assert.False(circuitBreaker.IsAllowed);
149+
}
150+
151+
[Fact]
152+
public void IsAllowed_AfterTimeoutExpires_TransitionsToHalfOpen()
153+
{
154+
// Arrange
155+
var circuitBreaker = new CircuitBreaker(threshold: 1, timeout: TimeSpan.FromMilliseconds(1));
156+
157+
// Act
158+
circuitBreaker.RecordFailure();
159+
Assert.Equal(CircuitState.Open, circuitBreaker.State);
160+
161+
// Wait for recovery
162+
Thread.Sleep(10);
163+
164+
// Assert
165+
Assert.Equal(CircuitState.HalfOpen, circuitBreaker.State);
166+
Assert.True(circuitBreaker.IsAllowed);
167+
}
168+
169+
#endregion
170+
171+
#region ExecuteAsync Tests
172+
173+
[Fact]
174+
public async Task ExecuteAsync_SuccessfulAction_ReturnsResult()
175+
{
176+
// Arrange
177+
var circuitBreaker = new CircuitBreaker(threshold: 3);
178+
179+
// Act
180+
var result = await circuitBreaker.ExecuteAsync(async () =>
181+
{
182+
await Task.Delay(1);
183+
return "success";
184+
});
185+
186+
// Assert
187+
Assert.Equal("success", result);
188+
Assert.Equal(CircuitState.Closed, circuitBreaker.State);
189+
}
190+
191+
[Fact]
192+
public async Task ExecuteAsync_FailingAction_ReturnsFallback()
193+
{
194+
// Arrange
195+
var circuitBreaker = new CircuitBreaker(threshold: 3);
196+
197+
// Act
198+
var result = await circuitBreaker.ExecuteAsync<string>(async () =>
199+
{
200+
await Task.Delay(1);
201+
throw new InvalidOperationException("Test failure");
202+
}, fallback: "fallback-value");
203+
204+
// Assert
205+
Assert.Equal("fallback-value", result);
206+
// Verify failure was recorded (still closed, only 1 failure)
207+
Assert.Equal(CircuitState.Closed, circuitBreaker.State);
208+
}
209+
210+
[Fact]
211+
public async Task ExecuteAsync_CircuitOpen_ReturnsFallback()
212+
{
213+
// Arrange
214+
var circuitBreaker = new CircuitBreaker(threshold: 1, timeout: TimeSpan.FromHours(1));
215+
circuitBreaker.RecordFailure(); // Open the circuit
216+
217+
// Act
218+
var result = await circuitBreaker.ExecuteAsync(async () =>
219+
{
220+
await Task.Delay(1);
221+
return "should not reach";
222+
}, fallback: "circuit-open-fallback");
223+
224+
// Assert
225+
Assert.Equal("circuit-open-fallback", result);
226+
}
227+
228+
[Fact]
229+
public async Task ExecuteAsync_MultipleFailures_OpensCircuit()
230+
{
231+
// Arrange
232+
var circuitBreaker = new CircuitBreaker(threshold: 2, timeout: TimeSpan.FromHours(1));
233+
var callCount = 0;
234+
235+
// Act
236+
for (int i = 0; i < 3; i++)
237+
{
238+
await circuitBreaker.ExecuteAsync<string>(async () =>
239+
{
240+
callCount++;
241+
await Task.Delay(1);
242+
throw new Exception("Fail");
243+
}, fallback: null);
244+
}
245+
246+
// Assert - Circuit should be open after 2 failures
247+
Assert.Equal(CircuitState.Open, circuitBreaker.State);
248+
Assert.Equal(2, callCount); // Only 2 calls made, third blocked by open circuit
249+
}
250+
251+
#endregion
252+
253+
#region Thread Safety Tests
254+
255+
[Fact]
256+
public async Task ConcurrentFailures_ThreadSafe()
257+
{
258+
// Arrange
259+
var circuitBreaker = new CircuitBreaker(threshold: 10, timeout: TimeSpan.FromSeconds(30));
260+
var tasks = new List<Task>();
261+
262+
// Act - Record 20 failures concurrently
263+
for (int i = 0; i < 20; i++)
264+
{
265+
tasks.Add(Task.Run(() => circuitBreaker.RecordFailure()));
266+
}
267+
await Task.WhenAll(tasks);
268+
269+
// Assert - Circuit should be open (threshold exceeded)
270+
Assert.Equal(CircuitState.Open, circuitBreaker.State);
271+
}
272+
273+
#endregion
274+
275+
#region Default Values Tests
276+
277+
[Fact]
278+
public void DefaultThreshold_IsFive()
279+
{
280+
// Arrange & Act
281+
var circuitBreaker = new CircuitBreaker();
282+
283+
// Open the circuit by recording 5 failures
284+
for (int i = 0; i < 4; i++)
285+
{
286+
circuitBreaker.RecordFailure();
287+
}
288+
Assert.Equal(CircuitState.Closed, circuitBreaker.State); // Still closed at 4
289+
290+
circuitBreaker.RecordFailure(); // 5th failure
291+
Assert.Equal(CircuitState.Open, circuitBreaker.State); // Now open
292+
}
293+
294+
#endregion
295+
}

0 commit comments

Comments
 (0)