Skip to content

Commit 6017636

Browse files
committed
Unit tests
1 parent e85f90a commit 6017636

File tree

6 files changed

+399
-10
lines changed

6 files changed

+399
-10
lines changed

src/Components/Server/src/Circuits/CircuitPersistenceManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ await circuitPersistenceProvider.PersistCircuitAsync(
4444
}
4545
}
4646

47-
private async Task SaveStateToClient(CircuitHost circuit, PersistedCircuitState state, CancellationToken cancellation = default)
47+
internal async Task SaveStateToClient(CircuitHost circuit, PersistedCircuitState state, CancellationToken cancellation = default)
4848
{
4949
var (rootComponents, applicationState) = await ToProtectedStateAsync(state);
5050
if (!await circuit.SendPersistedStateToClient(rootComponents, applicationState, cancellation))

src/Components/Server/src/Circuits/CircuitRegistry.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,27 +314,28 @@ private async Task PauseAndDisposeCircuitHost(CircuitHost circuitHost, bool save
314314
await circuitHost.DisposeAsync();
315315
}
316316

317-
internal Task PauseCircuitAsync(
317+
internal async Task PauseCircuitAsync(
318318
CircuitHost circuitHost,
319319
string connectionId)
320320
{
321321
try
322322
{
323323
Log.CircuitPauseStarted(_logger, circuitHost.CircuitId, connectionId);
324324

325+
Task pauseTask;
325326
lock (CircuitRegistryLock)
326327
{
327-
return PauseCore(circuitHost, connectionId);
328+
pauseTask = PauseCore(circuitHost, connectionId);
328329
}
330+
await pauseTask;
329331
}
330332
catch (Exception)
331333
{
332334
Log.CircuitPauseFailed(_logger, circuitHost.CircuitId, connectionId);
333-
return Task.CompletedTask;
334335
}
335336
}
336337

337-
internal Task PauseCore(CircuitHost circuitHost, string connectionId)
338+
internal virtual Task PauseCore(CircuitHost circuitHost, string connectionId)
338339
{
339340
var circuitId = circuitHost.CircuitId;
340341
if (!ConnectedCircuits.TryGetValue(circuitId, out circuitHost))

src/Components/Server/test/Circuits/CircuitHostTest.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,115 @@ await circuitHost.HandleInboundActivityAsync(() =>
412412
Assert.True(wasHandlerFuncInvoked);
413413
}
414414

415+
[Fact]
416+
public async Task SendPersistedStateToClient_WithSuccessfulInvocation_ReturnsTrue()
417+
{
418+
// Arrange
419+
var mockClientProxy = new Mock<ISingleClientProxy>();
420+
mockClientProxy
421+
.Setup(c => c.InvokeCoreAsync<bool>(
422+
"JS.SavePersistedState",
423+
It.IsAny<object[]>(),
424+
It.IsAny<CancellationToken>()))
425+
.ReturnsAsync(true);
426+
427+
var client = new CircuitClientProxy(mockClientProxy.Object, "connection-id");
428+
var circuitHost = TestCircuitHost.Create(clientProxy: client);
429+
430+
var rootComponents = "mock-root-components";
431+
var applicationState = "mock-application-state";
432+
var cancellationToken = new CancellationToken();
433+
434+
// Act
435+
var result = await circuitHost.SendPersistedStateToClient(rootComponents, applicationState, cancellationToken);
436+
437+
// Assert
438+
Assert.True(result);
439+
mockClientProxy.Verify(
440+
c => c.InvokeCoreAsync<bool>(
441+
"JS.SavePersistedState",
442+
It.Is<object[]>(args => args[0].Equals(circuitHost.CircuitId.Secret) &&
443+
args[1].Equals(rootComponents) &&
444+
args[2].Equals(applicationState)),
445+
cancellationToken),
446+
Times.Once);
447+
}
448+
449+
[Fact]
450+
public async Task SendPersistedStateToClient_WithFailedInvocation_ReturnsFalse()
451+
{
452+
// Arrange
453+
var mockClientProxy = new Mock<ISingleClientProxy>();
454+
mockClientProxy
455+
.Setup(c => c.InvokeCoreAsync<bool>(
456+
"JS.SavePersistedState",
457+
It.IsAny<object[]>(),
458+
It.IsAny<CancellationToken>()))
459+
.ReturnsAsync(false);
460+
461+
var client = new CircuitClientProxy(mockClientProxy.Object, "connection-id");
462+
var circuitHost = TestCircuitHost.Create(clientProxy: client);
463+
464+
var rootComponents = "mock-root-components";
465+
var applicationState = "mock-application-state";
466+
var cancellationToken = new CancellationToken();
467+
468+
// Act
469+
var result = await circuitHost.SendPersistedStateToClient(rootComponents, applicationState, cancellationToken);
470+
471+
// Assert
472+
Assert.False(result);
473+
}
474+
475+
[Fact]
476+
public async Task SendPersistedStateToClient_WithException_LogsAndReturnsFalse()
477+
{
478+
// Arrange
479+
var expectedException = new InvalidOperationException("Test exception");
480+
var mockClientProxy = new Mock<ISingleClientProxy>();
481+
mockClientProxy
482+
.Setup(c => c.InvokeCoreAsync<bool>(
483+
"JS.SavePersistedState",
484+
It.IsAny<object[]>(),
485+
It.IsAny<CancellationToken>()))
486+
.ThrowsAsync(expectedException);
487+
488+
var client = new CircuitClientProxy(mockClientProxy.Object, "connection-id");
489+
var circuitHost = TestCircuitHost.Create(clientProxy: client);
490+
491+
var rootComponents = "mock-root-components";
492+
var applicationState = "mock-application-state";
493+
var cancellationToken = new CancellationToken();
494+
495+
// Act
496+
var result = await circuitHost.SendPersistedStateToClient(rootComponents, applicationState, cancellationToken);
497+
498+
// Assert
499+
Assert.False(result);
500+
mockClientProxy.Verify(
501+
c => c.InvokeCoreAsync<bool>(
502+
"JS.SavePersistedState",
503+
It.IsAny<object[]>(),
504+
It.IsAny<CancellationToken>()),
505+
Times.Once);
506+
}
507+
508+
[Fact]
509+
public async Task SendPersistedStateToClient_WithDisconnectedClient_ThrowsInvalidOperationException()
510+
{
511+
// Arrange
512+
var client = new CircuitClientProxy(); // Creates a disconnected client
513+
var circuitHost = TestCircuitHost.Create(clientProxy: client);
514+
515+
var rootComponents = "mock-root-components";
516+
var applicationState = "mock-application-state";
517+
var cancellationToken = new CancellationToken();
518+
519+
// Act & Assert
520+
await Assert.ThrowsAsync<InvalidOperationException>(() =>
521+
circuitHost.SendPersistedStateToClient(rootComponents, applicationState, cancellationToken));
522+
}
523+
415524
[Fact]
416525
public async Task UpdateRootComponents_CanAddNewRootComponent()
417526
{

src/Components/Server/test/Circuits/CircuitPersistenceManagerTest.cs

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using Moq;
2323

2424
namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits;
25+
2526
public class CircuitPersistenceManagerTest
2627
{
2728
// Pause circuit registers with PersistentComponentStatemanager to persist root components.
@@ -146,6 +147,140 @@ public async Task PauseCircuitAsync_CanPersistMultipleComponents_WithTheirParame
146147
state.RootComponents);
147148
}
148149

150+
[Fact]
151+
public async Task SaveStateToClient_PersistsStateToServer_WhenSendingToClientFails()
152+
{
153+
// Arrange
154+
var dataProtectionProvider = new EphemeralDataProtectionProvider();
155+
var deserializer = CreateDeserializer(dataProtectionProvider);
156+
var options = Options.Create(new CircuitOptions());
157+
var components = new[]
158+
{
159+
(RootComponentType: typeof(RootComponent), Parameters: new Dictionary<string, object>
160+
{
161+
["Count"] = 42
162+
})
163+
};
164+
165+
var mockClientProxy = new Mock<ISingleClientProxy>();
166+
mockClientProxy.Setup(c => c.InvokeCoreAsync<bool>(
167+
It.IsAny<string>(),
168+
It.IsAny<object[]>(),
169+
It.IsAny<CancellationToken>()))
170+
.ReturnsAsync(false); // Simulate client failure
171+
172+
var client = new CircuitClientProxy(mockClientProxy.Object, Guid.NewGuid().ToString());
173+
var circuitHost = await CreateCircuitHostAsync(
174+
options,
175+
dataProtectionProvider,
176+
deserializer,
177+
components,
178+
client);
179+
180+
var circuitPersistenceProvider = new TestCircuitPersistenceProvider();
181+
var circuitPersistenceManager = new CircuitPersistenceManager(
182+
options,
183+
new ServerComponentSerializer(dataProtectionProvider),
184+
circuitPersistenceProvider,
185+
dataProtectionProvider);
186+
187+
var store = new CircuitPersistenceManagerStore();
188+
189+
// Create a minimal persisted state for testing
190+
var persistedState = new PersistedCircuitState
191+
{
192+
ApplicationState = new Dictionary<string, byte[]> { ["test"] = [1, 2, 3] },
193+
RootComponents = [1, 2, 3, 4]
194+
};
195+
196+
// Act
197+
await circuitPersistenceManager.SaveStateToClient(
198+
circuitHost,
199+
persistedState,
200+
default);
201+
202+
// Assert
203+
Assert.NotNull(circuitPersistenceProvider.State);
204+
Assert.Same(persistedState, circuitPersistenceProvider.State);
205+
206+
// Verify that InvokeAsync was called to attempt client-side storage
207+
mockClientProxy.Verify(c => c.InvokeCoreAsync<bool>(
208+
"JS.SavePersistedState",
209+
It.IsAny<object[]>(),
210+
It.IsAny<CancellationToken>()),
211+
Times.Once);
212+
}
213+
214+
[Fact]
215+
public async Task SaveStateToClient_CatchesException_WhenPersistingToServerFails()
216+
{
217+
// Arrange
218+
var dataProtectionProvider = new EphemeralDataProtectionProvider();
219+
var deserializer = CreateDeserializer(dataProtectionProvider);
220+
var options = Options.Create(new CircuitOptions());
221+
var components = new[]
222+
{
223+
(RootComponentType: typeof(RootComponent), Parameters: new Dictionary<string, object>
224+
{
225+
["Count"] = 42
226+
})
227+
};
228+
229+
var mockClientProxy = new Mock<ISingleClientProxy>();
230+
mockClientProxy.Setup(c => c.InvokeCoreAsync<bool>(
231+
It.IsAny<string>(),
232+
It.IsAny<object[]>(),
233+
It.IsAny<CancellationToken>()))
234+
.ReturnsAsync(false); // Simulate client failure
235+
236+
var client = new CircuitClientProxy(mockClientProxy.Object, Guid.NewGuid().ToString());
237+
var circuitHost = await CreateCircuitHostAsync(
238+
options,
239+
dataProtectionProvider,
240+
deserializer,
241+
components,
242+
client);
243+
244+
// Create a circuit persistence provider that throws an exception when PersistCircuitAsync is called
245+
var circuitPersistenceProvider = new Mock<ICircuitPersistenceProvider>();
246+
circuitPersistenceProvider
247+
.Setup(p => p.PersistCircuitAsync(It.IsAny<CircuitId>(), It.IsAny<PersistedCircuitState>(), It.IsAny<CancellationToken>()))
248+
.ThrowsAsync(new InvalidOperationException("Failed to persist circuit"));
249+
250+
var circuitPersistenceManager = new CircuitPersistenceManager(
251+
options,
252+
new ServerComponentSerializer(dataProtectionProvider),
253+
circuitPersistenceProvider.Object,
254+
dataProtectionProvider);
255+
256+
var persistedState = new PersistedCircuitState
257+
{
258+
ApplicationState = new Dictionary<string, byte[]> { ["test"] = [1, 2, 3] },
259+
RootComponents = [1, 2, 3, 4]
260+
};
261+
262+
// Act - This should not throw even though both client and server persistence fail
263+
await circuitPersistenceManager.SaveStateToClient(
264+
circuitHost,
265+
persistedState,
266+
default);
267+
268+
// Assert
269+
// Verify that InvokeAsync was called to attempt client-side storage
270+
mockClientProxy.Verify(c => c.InvokeCoreAsync<bool>(
271+
"JS.SavePersistedState",
272+
It.IsAny<object[]>(),
273+
It.IsAny<CancellationToken>()),
274+
Times.Once);
275+
276+
// Verify that PersistCircuitAsync was called when client-side storage failed
277+
circuitPersistenceProvider.Verify(p => p.PersistCircuitAsync(
278+
It.IsAny<CircuitId>(),
279+
It.IsAny<PersistedCircuitState>(),
280+
It.IsAny<CancellationToken>()),
281+
Times.Once);
282+
}
283+
149284
[Fact]
150285
public void ToRootComponentOperationBatch_WorksFor_EmptyBatch()
151286
{
@@ -262,7 +397,8 @@ private async Task<CircuitHost> CreateCircuitHostAsync(
262397
IOptions<CircuitOptions> options,
263398
EphemeralDataProtectionProvider dataProtectionProvider,
264399
ServerComponentDeserializer deserializer,
265-
(Type RootComponentType, Dictionary<string, object> Parameters)[] components = null)
400+
(Type RootComponentType, Dictionary<string, object> Parameters)[] components = null,
401+
CircuitClientProxy client = null)
266402
{
267403
components ??= [];
268404
var circuitId = new CircuitIdFactory(dataProtectionProvider).CreateCircuitId();
@@ -286,7 +422,7 @@ private async Task<CircuitHost> CreateCircuitHostAsync(
286422

287423
var scope = serviceProvider.CreateAsyncScope();
288424

289-
var client = new CircuitClientProxy(Mock.Of<ISingleClientProxy>(), Guid.NewGuid().ToString());
425+
client ??= new CircuitClientProxy(Mock.Of<ISingleClientProxy>(), Guid.NewGuid().ToString());
290426

291427
var renderer = new RemoteRenderer(
292428
scope.ServiceProvider,
@@ -508,4 +644,23 @@ public Task SetParametersAsync(ParameterView parameters)
508644
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
509645
PropertyNameCaseInsensitive = true,
510646
};
647+
648+
private class CircuitPersistenceManagerStore : IPersistentComponentStateStore
649+
{
650+
internal PersistedCircuitState PersistedCircuitState { get; private set; }
651+
652+
Task<IDictionary<string, byte[]>> IPersistentComponentStateStore.GetPersistedStateAsync() =>
653+
throw new NotImplementedException();
654+
655+
Task IPersistentComponentStateStore.PersistStateAsync(IReadOnlyDictionary<string, byte[]> state)
656+
{
657+
PersistedCircuitState = new PersistedCircuitState
658+
{
659+
ApplicationState = new Dictionary<string, byte[]>(state),
660+
RootComponents = null
661+
};
662+
663+
return Task.CompletedTask;
664+
}
665+
}
511666
}

src/Components/Server/test/Circuits/CircuitRegistryPauseTest.cs

Whitespace-only changes.

0 commit comments

Comments
 (0)