microsoft/teams.net

Public

mirrored fromhttps://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
6594a29aa91c928c547a8821d305758bc8d340ed

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginStreamTests.cs

208lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT License.
3
4using Microsoft.Teams.Api.Activities;
5using Microsoft.Teams.Api.Entities;
6
7namespace Microsoft.Teams.Plugins.AspNetCore.Tests;
8
9public class AspNetCorePluginStreamTests
10{
11 [Fact]
12 public async Task Stream_EmitMessage_FlushesAfter500ms()
13 {
14 var sendCallCount = 0;
15 var sendTimes = new List<DateTime>();
16 var stream = new AspNetCorePlugin.Stream
17 {
18 Send = activity =>
19 {
20 sendCallCount++;
21 sendTimes.Add(DateTime.Now);
22 activity.Id = $"test-id-{sendCallCount}";
23 return Task.FromResult(activity);
24 }
25 };
26
27 var startTime = DateTime.Now;
28
29 stream.Emit("Test message");
30 await Task.Delay(600); // Wait longer than 500ms timeout
31
32 Assert.True(sendCallCount > 0, "Should have sent at least one message");
33 Assert.True(sendTimes.Any(t => t >= startTime.AddMilliseconds(450)),
34 "Should have waited approximately 500ms before sending");
35 }
36
37 [Fact]
38 public async Task Stream_MultipleEmits_RestartsTimer()
39 {
40 var sendCallCount = 0;
41 var stream = new AspNetCorePlugin.Stream
42 {
43 Send = activity =>
44 {
45 sendCallCount++;
46 activity.Id = $"test-id-{sendCallCount}";
47 return Task.FromResult(activity);
48 }
49 };
50
51 stream.Emit("First message");
52 await Task.Delay(300); // Wait less than 500ms
53
54 stream.Emit("Second message"); // This should reset the timer
55 await Task.Delay(300); // Still less than 500ms from second emit
56
57 Assert.Equal(0, sendCallCount); // Should not have sent yet
58
59 await Task.Delay(300); // Now over 500ms from second emit
60
61 Assert.True(sendCallCount > 0, "Should have sent messages after timer expired");
62 }
63
64 [Fact]
65 public async Task Stream_SendTimeout_HandledGracefully()
66 {
67 var callCount = 0;
68 var stream = new AspNetCorePlugin.Stream
69 {
70 Send = activity =>
71 {
72 callCount++;
73 if (callCount == 1) // Fail first attempt
74 {
75 throw new TimeoutException("Operation timed out");
76 }
77
78 // Succeed on second attempt
79 activity.Id = $"success-after-timeout-{callCount}";
80 return Task.FromResult(activity);
81 }
82 };
83
84 stream.Emit("Test message with timeout");
85 await Task.Delay(600); // Wait for flush and retries
86
87 var result = await stream.Close();
88
89 Assert.True(callCount > 1, "Should have retried after timeout");
90 Assert.NotNull(result);
91 Assert.Contains("Test message with timeout", result.Text);
92 }
93
94 [Fact]
95 public async Task Stream_UpdateStatus_SendsTypingActivity()
96 {
97 var sentActivities = new List<IActivity>();
98 var stream = new AspNetCorePlugin.Stream
99 {
100 Send = activity =>
101 {
102 sentActivities.Add(activity);
103 return Task.FromResult(activity);
104 }
105 };
106
107 stream.Update("Thinking...");
108 await Task.Delay(1000); // Wait for the flush task to complete
109
110 Assert.True(stream.Count > 0, "Should have processed the update");
111 Assert.Equal(2, stream.Sequence); // Should increment sequence after sending
112
113 Assert.True(sentActivities.Count > 0, "Should have sent at least one activity");
114 var sentActivity = sentActivities.First();
115 Assert.IsType<TypingActivity>(sentActivity);
116 Assert.Equal("Thinking...", ((TypingActivity)sentActivity).Text);
117 Assert.Equal(StreamType.Informative, ((TypingActivity)sentActivity).ChannelData?.StreamType);
118 }
119
120 [Fact]
121 public async Task Stream_Close_FinalMessageHasStreamTypeFinal_AfterInformativeUpdate()
122 {
123 var sendCallCount = 0;
124 var stream = new AspNetCorePlugin.Stream
125 {
126 Send = activity =>
127 {
128 sendCallCount++;
129 activity.Id = $"id-{sendCallCount}";
130 return Task.FromResult(activity);
131 }
132 };
133
134 // Update + Emit both queue activities; Close() waits for the flush to drain the
135 // queue and complete, so no fixed sleeps are needed.
136 stream.Update("Thinking...");
137 stream.Emit("Done");
138
139 var result = await stream.Close();
140
141 Assert.NotNull(result);
142 // Final message must have StreamType.Final, not the accumulated Informative
143 // from the prior typing update.
144 Assert.Equal(StreamType.Final, result.ChannelData?.StreamType);
145
146 // The streaminfo entity on the final message should also be Final.
147 var streamInfo = result.Entities?.OfType<StreamInfoEntity>().Single();
148 Assert.NotNull(streamInfo);
149 Assert.Equal(StreamType.Final, streamInfo.StreamType);
150 }
151
152 [Fact]
153 public async Task Stream_Close_WaitsForInFlightFlushToComplete()
154 {
155 var sendCallCount = 0;
156 var firstSendCompleted = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
157 var secondSendStarted = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
158 var secondSendRelease = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
159 var stream = new AspNetCorePlugin.Stream
160 {
161 Send = async activity =>
162 {
163 sendCallCount++;
164 var thisCall = sendCallCount;
165 if (thisCall == 2)
166 {
167 secondSendStarted.TrySetResult(true);
168 await secondSendRelease.Task;
169 }
170 activity.Id = $"id-{thisCall}";
171 if (thisCall == 1) firstSendCompleted.TrySetResult(true);
172 return activity;
173 }
174 };
175
176 // First flush: emit and wait for the send to actually complete (deterministic
177 // signal from the Send delegate). _id is assigned by the SendActivity helper after
178 // the await — yielding once lets that post-await code run before we proceed.
179 stream.Emit("chunk 1");
180 await firstSendCompleted.Task.WaitAsync(TimeSpan.FromSeconds(2));
181 await Task.Yield();
182 Assert.Equal(1, sendCallCount);
183
184 // Second flush: Send blocks → queue drained, _id set, _lock held.
185 stream.Emit("chunk 2");
186 await secondSendStarted.Task.WaitAsync(TimeSpan.FromSeconds(2));
187
188 var closeTask = stream.Close();
189
190 // With the race-fix, Close() must not progress past its wait loop while the
191 // flush is mid-await. Pre-fix, closeTask would race ahead and call Send for the
192 // final activity (sendCallCount → 3) before we release the second flush.
193 // We yield several times rather than sleep — Close() polls every 50ms and we
194 // want to give it ample chance to make progress if the bug is present.
195 for (var i = 0; i < 10; i++) await Task.Yield();
196 await Task.Delay(100);
197 Assert.False(closeTask.IsCompleted);
198 Assert.Equal(2, sendCallCount);
199
200 // Releasing the second flush lets the lock drop, and Close() then sends the final.
201 secondSendRelease.SetResult(true);
202
203 var result = await closeTask.WaitAsync(TimeSpan.FromSeconds(2));
204
205 Assert.NotNull(result);
206 Assert.Equal(3, sendCallCount);
207 }
208}