2222using Moq ;
2323
2424namespace Microsoft . AspNetCore . Components . Server . Tests . Circuits ;
25+
2526public 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}
0 commit comments