|
12 | 12 | // ------------------------------------------------------------------------ |
13 | 13 |
|
14 | 14 | using System.Threading.Channels; |
| 15 | +using Dapr; |
15 | 16 | using Dapr.AppCallback.Autogen.Grpc.v1; |
16 | 17 | using Dapr.Messaging.PublishSubscribe; |
17 | 18 | using Grpc.Core; |
@@ -193,14 +194,194 @@ public async Task DisposeAsync_ShouldCompleteChannels() |
193 | 194 | } |
194 | 195 |
|
195 | 196 | [Fact] |
196 | | - public void HandleTaskCompletion_ShouldThrowException_WhenTaskHasException() |
| 197 | + public void HandleTaskCompletion_ShouldInvokeErrorHandler_WhenTaskHasException() |
197 | 198 | { |
| 199 | + const string pubSubName = "testPubSub"; |
| 200 | + const string topicName = "testTopic"; |
| 201 | + DaprException? receivedException = null; |
| 202 | + var options = |
| 203 | + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)) |
| 204 | + { |
| 205 | + ErrorHandler = ex => receivedException = ex |
| 206 | + }; |
| 207 | + |
| 208 | + var messageHandler = new TopicMessageHandler((message, token) => Task.FromResult(TopicResponseAction.Success)); |
| 209 | + var mockDaprClient = new Mock<P.Dapr.DaprClient>(); |
| 210 | + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, mockDaprClient.Object); |
| 211 | + |
| 212 | + var task = Task.FromException(new InvalidOperationException("Test exception")); |
| 213 | + |
| 214 | + receiver.HandleTaskCompletion(task, null); |
| 215 | + |
| 216 | + Assert.NotNull(receivedException); |
| 217 | + Assert.IsType<InvalidOperationException>(receivedException.InnerException); |
| 218 | + Assert.Equal("Test exception", receivedException.InnerException.Message); |
| 219 | + Assert.Contains("testTopic", receivedException.Message); |
| 220 | + Assert.Contains("testPubSub", receivedException.Message); |
| 221 | + } |
| 222 | + |
| 223 | + [Fact] |
| 224 | + public void HandleTaskCompletion_ShouldNotThrow_WhenNoErrorHandler() |
| 225 | + { |
| 226 | + const string pubSubName = "testPubSub"; |
| 227 | + const string topicName = "testTopic"; |
| 228 | + var options = |
| 229 | + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)); |
| 230 | + |
| 231 | + var messageHandler = new TopicMessageHandler((message, token) => Task.FromResult(TopicResponseAction.Success)); |
| 232 | + var mockDaprClient = new Mock<P.Dapr.DaprClient>(); |
| 233 | + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, mockDaprClient.Object); |
| 234 | + |
| 235 | + var task = Task.FromException(new InvalidOperationException("Test exception")); |
| 236 | + |
| 237 | + var exception = Record.Exception(() => receiver.HandleTaskCompletion(task, null)); |
| 238 | + |
| 239 | + Assert.Null(exception); |
| 240 | + } |
| 241 | + |
| 242 | + [Fact] |
| 243 | + public void HandleTaskCompletion_ShouldNotThrow_WhenErrorHandlerThrows() |
| 244 | + { |
| 245 | + const string pubSubName = "testPubSub"; |
| 246 | + const string topicName = "testTopic"; |
| 247 | + var options = |
| 248 | + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)) |
| 249 | + { |
| 250 | + ErrorHandler = _ => throw new InvalidOperationException("Handler failed") |
| 251 | + }; |
| 252 | + |
| 253 | + var messageHandler = new TopicMessageHandler((message, token) => Task.FromResult(TopicResponseAction.Success)); |
| 254 | + var mockDaprClient = new Mock<P.Dapr.DaprClient>(); |
| 255 | + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, mockDaprClient.Object); |
| 256 | + |
198 | 257 | var task = Task.FromException(new InvalidOperationException("Test exception")); |
199 | 258 |
|
200 | | - var exception = Assert.Throws<AggregateException>(() => |
201 | | - PublishSubscribeReceiver.HandleTaskCompletion(task, null)); |
202 | | - |
203 | | - Assert.IsType<InvalidOperationException>(exception.InnerException); |
204 | | - Assert.Equal("Test exception", exception.InnerException.Message); |
| 259 | + var exception = Record.Exception(() => receiver.HandleTaskCompletion(task, null)); |
| 260 | + |
| 261 | + Assert.Null(exception); |
| 262 | + } |
| 263 | + |
| 264 | + [Fact] |
| 265 | + public void HandleTaskCompletion_ShouldNotInvokeErrorHandler_WhenTaskSucceeded() |
| 266 | + { |
| 267 | + const string pubSubName = "testPubSub"; |
| 268 | + const string topicName = "testTopic"; |
| 269 | + var handlerInvoked = false; |
| 270 | + var options = |
| 271 | + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)) |
| 272 | + { |
| 273 | + ErrorHandler = _ => handlerInvoked = true |
| 274 | + }; |
| 275 | + |
| 276 | + var messageHandler = new TopicMessageHandler((message, token) => Task.FromResult(TopicResponseAction.Success)); |
| 277 | + var mockDaprClient = new Mock<P.Dapr.DaprClient>(); |
| 278 | + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, mockDaprClient.Object); |
| 279 | + |
| 280 | + receiver.HandleTaskCompletion(Task.CompletedTask, null); |
| 281 | + |
| 282 | + Assert.False(handlerInvoked); |
| 283 | + } |
| 284 | + |
| 285 | + [Fact] |
| 286 | + public async Task SubscribeAsync_ShouldThrowDaprException_WhenSidecarUnavailable() |
| 287 | + { |
| 288 | + const string pubSubName = "testPubSub"; |
| 289 | + const string topicName = "testTopic"; |
| 290 | + var options = |
| 291 | + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)); |
| 292 | + |
| 293 | + var messageHandler = new TopicMessageHandler((message, token) => Task.FromResult(TopicResponseAction.Success)); |
| 294 | + var mockDaprClient = new Mock<P.Dapr.DaprClient>(); |
| 295 | + |
| 296 | + // Setup the mock to throw RpcException (simulating unavailable sidecar) |
| 297 | + mockDaprClient.Setup(client => |
| 298 | + client.SubscribeTopicEventsAlpha1(null, null, It.IsAny<CancellationToken>())) |
| 299 | + .Throws(new RpcException(new Status(StatusCode.Unavailable, "Connect Failed"))); |
| 300 | + |
| 301 | + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, mockDaprClient.Object); |
| 302 | + |
| 303 | + var exception = await Assert.ThrowsAsync<DaprException>(() => receiver.SubscribeAsync()); |
| 304 | + |
| 305 | + Assert.Contains("testTopic", exception.Message); |
| 306 | + Assert.Contains("testPubSub", exception.Message); |
| 307 | + Assert.IsType<RpcException>(exception.InnerException); |
| 308 | + } |
| 309 | + |
| 310 | + [Fact] |
| 311 | + public async Task SubscribeAsync_ShouldAllowRetry_AfterSidecarFailure() |
| 312 | + { |
| 313 | + const string pubSubName = "testPubSub"; |
| 314 | + const string topicName = "testTopic"; |
| 315 | + var options = |
| 316 | + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)); |
| 317 | + |
| 318 | + var messageHandler = new TopicMessageHandler((message, token) => Task.FromResult(TopicResponseAction.Success)); |
| 319 | + var mockDaprClient = new Mock<P.Dapr.DaprClient>(); |
| 320 | + |
| 321 | + // First call throws RpcException |
| 322 | + mockDaprClient.Setup(client => |
| 323 | + client.SubscribeTopicEventsAlpha1(null, null, It.IsAny<CancellationToken>())) |
| 324 | + .Throws(new RpcException(new Status(StatusCode.Unavailable, "Connect Failed"))); |
| 325 | + |
| 326 | + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, mockDaprClient.Object); |
| 327 | + |
| 328 | + await Assert.ThrowsAsync<DaprException>(() => receiver.SubscribeAsync()); |
| 329 | + |
| 330 | + // Now setup the mock to succeed on retry |
| 331 | + var mockRequestStream = new Mock<IClientStreamWriter<P.SubscribeTopicEventsRequestAlpha1>>(); |
| 332 | + var mockResponseStream = new Mock<IAsyncStreamReader<P.SubscribeTopicEventsResponseAlpha1>>(); |
| 333 | + var mockCall = |
| 334 | + new AsyncDuplexStreamingCall<P.SubscribeTopicEventsRequestAlpha1, P.SubscribeTopicEventsResponseAlpha1>( |
| 335 | + mockRequestStream.Object, mockResponseStream.Object, Task.FromResult(new Metadata()), |
| 336 | + () => new Status(), () => new Metadata(), () => { }); |
| 337 | + |
| 338 | + mockDaprClient.Setup(client => |
| 339 | + client.SubscribeTopicEventsAlpha1(null, null, It.IsAny<CancellationToken>())) |
| 340 | + .Returns(mockCall); |
| 341 | + |
| 342 | + // Second call should succeed (hasInitialized was reset) |
| 343 | + var retryException = await Record.ExceptionAsync(() => receiver.SubscribeAsync()); |
| 344 | + Assert.Null(retryException); |
| 345 | + |
| 346 | + // Verify the client was called twice |
| 347 | + mockDaprClient.Verify(client => |
| 348 | + client.SubscribeTopicEventsAlpha1(null, null, It.IsAny<CancellationToken>()), Times.Exactly(2)); |
| 349 | + } |
| 350 | + |
| 351 | + [Fact] |
| 352 | + public async Task SubscribeAsync_ShouldResetHasInitialized_WhenNonRpcExceptionThrown() |
| 353 | + { |
| 354 | + const string pubSubName = "testPubSub"; |
| 355 | + const string topicName = "testTopic"; |
| 356 | + var options = |
| 357 | + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)); |
| 358 | + |
| 359 | + var messageHandler = new TopicMessageHandler((message, token) => Task.FromResult(TopicResponseAction.Success)); |
| 360 | + var mockDaprClient = new Mock<P.Dapr.DaprClient>(); |
| 361 | + |
| 362 | + // First call throws a non-RPC exception |
| 363 | + mockDaprClient.Setup(client => |
| 364 | + client.SubscribeTopicEventsAlpha1(null, null, It.IsAny<CancellationToken>())) |
| 365 | + .Throws(new ObjectDisposedException("client")); |
| 366 | + |
| 367 | + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, mockDaprClient.Object); |
| 368 | + |
| 369 | + await Assert.ThrowsAsync<ObjectDisposedException>(() => receiver.SubscribeAsync()); |
| 370 | + |
| 371 | + // Now setup the mock to succeed on retry |
| 372 | + var mockRequestStream = new Mock<IClientStreamWriter<P.SubscribeTopicEventsRequestAlpha1>>(); |
| 373 | + var mockResponseStream = new Mock<IAsyncStreamReader<P.SubscribeTopicEventsResponseAlpha1>>(); |
| 374 | + var mockCall = |
| 375 | + new AsyncDuplexStreamingCall<P.SubscribeTopicEventsRequestAlpha1, P.SubscribeTopicEventsResponseAlpha1>( |
| 376 | + mockRequestStream.Object, mockResponseStream.Object, Task.FromResult(new Metadata()), |
| 377 | + () => new Status(), () => new Metadata(), () => { }); |
| 378 | + |
| 379 | + mockDaprClient.Setup(client => |
| 380 | + client.SubscribeTopicEventsAlpha1(null, null, It.IsAny<CancellationToken>())) |
| 381 | + .Returns(mockCall); |
| 382 | + |
| 383 | + // Second call should succeed (hasInitialized was reset) |
| 384 | + var retryException = await Record.ExceptionAsync(() => receiver.SubscribeAsync()); |
| 385 | + Assert.Null(retryException); |
205 | 386 | } |
206 | 387 | } |
0 commit comments