microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
next/core-compat-sso-middleware

Branches

Tags

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

Clone

HTTPS

Download ZIP

core/test/IntegrationTests/ApiClientTests.cs

474lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Text;
5using System.Text.Json;
6using Microsoft.Teams.Bot.Apps.Api.Clients;
7using Microsoft.Teams.Bot.Apps.Handlers.MessageExtension;
8using Microsoft.Teams.Bot.Apps.Schema;
9using Microsoft.Teams.Bot.Core;
10using Microsoft.Teams.Bot.Core.Schema;
11using Xunit.Abstractions;
12
13namespace IntegrationTests;
14
15/// <summary>
16/// Integration tests for <see cref="ApiClient"/> sub-clients making real API calls.
17/// These tests verify that the ApiClient facade correctly delegates to core ConversationClient
18/// and that Teams/Meeting-specific BotHttpClient calls work end-to-end.
19/// </summary>
20public class ApiClientTests : IClassFixture<IntegrationTestFixture>
21{
22 private readonly IntegrationTestFixture _f;
23 private readonly ITestOutputHelper _output;
24 private readonly ApiClient _api;
25
26 public ApiClientTests(IntegrationTestFixture fixture, ITestOutputHelper output)
27 {
28 _f = fixture;
29 _f.OutputHelper = output;
30 _output = output;
31 _api = _f.ScopedApiClient;
32 }
33
34 #region Activities
35
36 [Fact]
37 public async Task Activities_CreateAsync()
38 {
39 CoreActivity activity = new()
40 {
41 Type = ActivityType.Message,
42 Properties = { { "text", $"[ApiClient.Activities.Create] at `{DateTime.UtcNow:s}`" } }
43 };
44
45 SendActivityResponse? res = await _api.Conversations.Activities.CreateAsync(_f.ConversationId, activity);
46
47 Assert.NotNull(res);
48 Assert.NotNull(res.Id);
49 _output.WriteLine($"Created activity: {res.Id}");
50 }
51
52 [Fact]
53 public async Task Activities_UpdateAsync()
54 {
55 CoreActivity original = new()
56 {
57 Type = ActivityType.Message,
58 Properties = { { "text", $"[ApiClient.Activities.Update] Original at `{DateTime.UtcNow:s}`" } }
59 };
60
61 SendActivityResponse? sent = await _api.Conversations.Activities.CreateAsync(_f.ConversationId, original);
62 Assert.NotNull(sent?.Id);
63
64 CoreActivity updated = new()
65 {
66 Type = ActivityType.Message,
67 Properties = { { "text", $"[ApiClient.Activities.Update] Updated at `{DateTime.UtcNow:s}`" } }
68 };
69
70 UpdateActivityResponse? res = await _api.Conversations.Activities.UpdateAsync(
71 _f.ConversationId, sent.Id, updated);
72
73 Assert.NotNull(res?.Id);
74 _output.WriteLine($"Updated activity: {res.Id}");
75 }
76
77 [Fact]
78 public async Task Activities_ReplyAsync()
79 {
80 CoreActivity original = new()
81 {
82 Type = ActivityType.Message,
83 Properties = { { "text", $"[ApiClient.Activities.Reply] Parent at `{DateTime.UtcNow:s}`" } }
84 };
85
86 SendActivityResponse? sent = await _api.Conversations.Activities.CreateAsync(_f.ConversationId, original);
87 Assert.NotNull(sent?.Id);
88
89 CoreActivity reply = new()
90 {
91 Type = ActivityType.Message,
92 Properties = { { "text", $"[ApiClient.Activities.Reply] Reply at `{DateTime.UtcNow:s}`" } }
93 };
94
95 SendActivityResponse? res = await _api.Conversations.Activities.ReplyAsync(
96 _f.ConversationId, sent.Id, reply);
97
98 Assert.NotNull(res);
99 _output.WriteLine($"Reply activity: {res?.Id}");
100 }
101
102 [Fact]
103 public async Task Activities_DeleteAsync()
104 {
105 CoreActivity activity = new()
106 {
107 Type = ActivityType.Message,
108 Properties = { { "text", $"[ApiClient.Activities.Delete] at `{DateTime.UtcNow:s}`" } }
109 };
110
111 SendActivityResponse? sent = await _api.Conversations.Activities.CreateAsync(_f.ConversationId, activity);
112 Assert.NotNull(sent?.Id);
113
114 await Task.Delay(2000);
115
116 await _api.Conversations.Activities.DeleteAsync(_f.ConversationId, sent.Id);
117 _output.WriteLine($"Deleted activity: {sent.Id}");
118 }
119
120 #endregion
121
122 #region Targeted Activities
123
124 [Fact(Skip = "Targeted activities are not supported in team channel conversations")]
125 public async Task Activities_CreateTargetedAsync()
126 {
127 // Targeted activities require a valid Recipient — get a real member ID
128 IList<ConversationAccount> members = await _api.Conversations.Members.GetAsync(_f.ConversationId);
129 Assert.NotEmpty(members);
130
131 CoreActivity activity = new()
132 {
133 Type = ActivityType.Message,
134 Recipient = new ConversationAccount { Id = members[0].Id },
135 Properties = { { "text", $"[ApiClient.Activities.CreateTargeted] at `{DateTime.UtcNow:s}`" } }
136 };
137
138 SendActivityResponse? res = await _api.Conversations.Activities.CreateTargetedAsync(_f.ConversationId, activity);
139
140 Assert.NotNull(res);
141 Assert.NotNull(res.Id);
142 _output.WriteLine($"Created targeted activity: {res.Id}");
143 }
144
145 [Fact(Skip = "Targeted activities are not supported in team channel conversations")]
146 public async Task Activities_UpdateTargetedAsync()
147 {
148 IList<ConversationAccount> members = await _api.Conversations.Members.GetAsync(_f.ConversationId);
149 Assert.NotEmpty(members);
150
151 CoreActivity original = new()
152 {
153 Type = ActivityType.Message,
154 Recipient = new ConversationAccount { Id = members[0].Id },
155 Properties = { { "text", $"[ApiClient.Activities.UpdateTargeted] Original at `{DateTime.UtcNow:s}`" } }
156 };
157
158 SendActivityResponse? sent = await _api.Conversations.Activities.CreateTargetedAsync(_f.ConversationId, original);
159 Assert.NotNull(sent?.Id);
160
161 CoreActivity updated = new()
162 {
163 Type = ActivityType.Message,
164 Properties = { { "text", $"[ApiClient.Activities.UpdateTargeted] Updated at `{DateTime.UtcNow:s}`" } }
165 };
166
167 UpdateActivityResponse? res = await _api.Conversations.Activities.UpdateTargetedAsync(
168 _f.ConversationId, sent.Id, updated);
169
170 Assert.NotNull(res?.Id);
171 _output.WriteLine($"Updated targeted activity: {res.Id}");
172 }
173
174 [Fact(Skip = "Targeted activities are not supported in team channel conversations")]
175 public async Task Activities_DeleteTargetedAsync()
176 {
177 IList<ConversationAccount> members = await _api.Conversations.Members.GetAsync(_f.ConversationId);
178 Assert.NotEmpty(members);
179
180 CoreActivity activity = new()
181 {
182 Type = ActivityType.Message,
183 Recipient = new ConversationAccount { Id = members[0].Id },
184 Properties = { { "text", $"[ApiClient.Activities.DeleteTargeted] at `{DateTime.UtcNow:s}`" } }
185 };
186
187 SendActivityResponse? sent = await _api.Conversations.Activities.CreateTargetedAsync(_f.ConversationId, activity);
188 Assert.NotNull(sent?.Id);
189
190 await Task.Delay(2000);
191
192 await _api.Conversations.Activities.DeleteTargetedAsync(_f.ConversationId, sent.Id);
193 _output.WriteLine($"Deleted targeted activity: {sent.Id}");
194 }
195
196 #endregion
197
198 #region Members
199
200 [Fact]
201 public async Task Members_GetAsync()
202 {
203 IList<ConversationAccount> members = await _api.Conversations.Members.GetAsync(_f.ConversationId);
204
205 Assert.NotNull(members);
206 Assert.NotEmpty(members);
207
208 foreach (ConversationAccount m in members)
209 {
210 _output.WriteLine($"Member: {m.Id} — {m.Name}");
211 }
212 }
213
214 [Fact]
215 public async Task Members_GetByIdAsync()
216 {
217 // Get MRI-format member ID from the members list first
218 IList<ConversationAccount> members = await _api.Conversations.Members.GetAsync(_f.ConversationId);
219 Assert.NotEmpty(members);
220 string memberId = members[0].Id!;
221
222 ConversationAccount member = await _api.Conversations.Members.GetByIdAsync(
223 _f.ConversationId, memberId);
224
225 Assert.NotNull(member);
226 Assert.Equal(memberId, member.Id);
227 _output.WriteLine($"Member: {member.Id} — {member.Name}");
228 }
229
230 [Fact]
231 public async Task Members_GetByIdAsync_AsTeamsConversationAccount()
232 {
233 // Get MRI-format member ID from the members list first
234 IList<ConversationAccount> members = await _api.Conversations.Members.GetAsync(_f.ConversationId);
235 Assert.NotEmpty(members);
236 string memberId = members[0].Id!;
237
238 TeamsConversationAccount member = await _api.Conversations.Members.GetByIdAsync<TeamsConversationAccount>(
239 _f.ConversationId, memberId);
240
241 Assert.NotNull(member);
242 Assert.Equal(memberId, member.Id);
243 _output.WriteLine($"Member: {member.Id} — {member.Name}, Email: {member.Email}, UPN: {member.UserPrincipalName}");
244 }
245
246 #endregion
247
248 #region Reactions
249
250 [Fact(Skip = "Reactions endpoint does not exist in Teams Bot Framework API (experimental/assumed route)")]
251 public async Task Reactions_AddAndDelete()
252 {
253 CoreActivity activity = new()
254 {
255 Type = ActivityType.Message,
256 Properties = { { "text", $"[ApiClient.Reactions] Test at `{DateTime.UtcNow:s}`" } }
257 };
258
259 SendActivityResponse? sent = await _api.Conversations.Activities.CreateAsync(_f.ConversationId, activity);
260 Assert.NotNull(sent?.Id);
261
262 await _api.Conversations.Reactions.AddAsync(_f.ConversationId, sent.Id, "like");
263 _output.WriteLine("Added 'like' reaction");
264
265 await Task.Delay(1000);
266
267 await _api.Conversations.Reactions.DeleteAsync(_f.ConversationId, sent.Id, "like");
268 _output.WriteLine("Removed 'like' reaction");
269 }
270
271 #endregion
272
273 #region Teams
274
275 [Fact]
276 public async Task Teams_GetByIdAsync()
277 {
278 Team? team = await _api.Teams.GetByIdAsync(_f.TeamId);
279
280 Assert.NotNull(team);
281 _output.WriteLine($"Team: {team.Id} — {team.Name}, Members: {team.MemberCount}, Channels: {team.ChannelCount}");
282 }
283
284 [Fact]
285 public async Task Teams_GetConversationsAsync()
286 {
287 List<TeamsChannel>? channels = await _api.Teams.GetConversationsAsync(_f.TeamId);
288
289 Assert.NotNull(channels);
290 Assert.NotEmpty(channels);
291
292 foreach (TeamsChannel ch in channels)
293 {
294 _output.WriteLine($"Channel: {ch.Id} — {ch.Name}");
295 }
296 }
297
298 #endregion
299
300 #region Meetings
301
302 [Fact]
303 public async Task Meetings_GetByIdAsync()
304 {
305 Meeting? meeting = await _api.Meetings.GetByIdAsync(_f.MeetingId);
306
307 Assert.NotNull(meeting);
308 _output.WriteLine($"Meeting: {meeting.Id}");
309 if (meeting.Details is not null)
310 {
311 _output.WriteLine($" Title: {meeting.Details.Title}, Type: {meeting.Details.Type}");
312 }
313 }
314
315 [Fact]
316 public async Task Meetings_GetParticipantAsync()
317 {
318 // The meetings participant API requires AAD object ID, not MRI/pairwise bot framework ID.
319 // Get the AAD object ID from a human member (bots don't have one).
320 IList<ConversationAccount> members = await _api.Conversations.Members.GetAsync(_f.ConversationId);
321 Assert.NotEmpty(members);
322
323 string? aadObjectId = null;
324 foreach (ConversationAccount m in members)
325 {
326 TeamsConversationAccount tm = await _api.Conversations.Members
327 .GetByIdAsync<TeamsConversationAccount>(_f.ConversationId, m.Id!);
328 _output.WriteLine($"Member: {tm.Name} — AadObjectId: {tm.AadObjectId ?? "(null)"}, Properties: [{string.Join(", ", tm.Properties.Keys)}]");
329 if (tm.AadObjectId is not null)
330 {
331 aadObjectId = tm.AadObjectId;
332 break;
333 }
334 }
335
336 if (aadObjectId is null)
337 {
338 _output.WriteLine("SKIP: No members with AAD object ID found in test conversation");
339 return;
340 }
341
342 MeetingParticipant? participant = await _api.Meetings.GetParticipantAsync(
343 _f.MeetingId, aadObjectId, _f.TenantId);
344
345 Assert.NotNull(participant);
346 _output.WriteLine($"Participant: {participant.User?.Id} — Role: {participant.Meeting?.Role}, InMeeting: {participant.Meeting?.InMeeting}");
347 }
348
349 #endregion
350
351 #region Bots — SignIn
352
353 [Fact]
354 public async Task Bots_SignIn_GetUrlAsync()
355 {
356 string connectionName = Environment.GetEnvironmentVariable("TEST_CONNECTION_NAME")
357 ?? throw new InvalidOperationException("TEST_CONNECTION_NAME not set");
358
359 var tokenExchangeState = new
360 {
361 ConnectionName = connectionName,
362 Conversation = new
363 {
364 User = new ConversationAccount { Id = _f.UserId },
365 }
366 };
367 string tokenExchangeStateJson = JsonSerializer.Serialize(tokenExchangeState);
368 string state = Convert.ToBase64String(Encoding.UTF8.GetBytes(tokenExchangeStateJson));
369
370 string? url = await _api.Bots.SignIn.GetUrlAsync(state);
371
372 Assert.NotNull(url);
373 Assert.StartsWith("https://", url);
374 _output.WriteLine($"SignIn URL: {url}");
375 }
376
377 [Fact]
378 public async Task Bots_SignIn_GetResourceAsync()
379 {
380 string connectionName = Environment.GetEnvironmentVariable("TEST_CONNECTION_NAME")
381 ?? throw new InvalidOperationException("TEST_CONNECTION_NAME not set");
382
383 var tokenExchangeState = new
384 {
385 ConnectionName = connectionName,
386 Conversation = new
387 {
388 User = new ConversationAccount { Id = _f.UserId },
389 }
390 };
391 string tokenExchangeStateJson = JsonSerializer.Serialize(tokenExchangeState);
392 string state = Convert.ToBase64String(Encoding.UTF8.GetBytes(tokenExchangeStateJson));
393
394
395 var resource = await _api.Bots.SignIn.GetResourceAsync(state);
396
397 Assert.NotNull(resource);
398 _output.WriteLine($"SignIn Resource: {resource.SignInLink}");
399 }
400
401 #endregion
402
403 #region Users — Token
404
405 [Fact]
406 public async Task Users_Token_GetStatusAsync()
407 {
408 // Get a valid member ID from the conversation
409 IList<ConversationAccount> members = await _api.Conversations.Members.GetAsync(_f.ConversationId);
410 Assert.NotEmpty(members);
411 string userId = members[0].Id!;
412
413 IList<GetTokenStatusResult>? statuses = await _api.Users.Token.GetStatusAsync(userId, "msteams");
414
415 // May return null or empty if user has no token connections — that's OK
416 _output.WriteLine($"Token statuses: {statuses?.Count ?? 0} connections");
417 if (statuses is not null)
418 {
419 foreach (var s in statuses)
420 {
421 _output.WriteLine($" Connection: {s.ConnectionName}, HasToken: {s.HasToken}");
422 }
423 }
424 }
425
426 [Fact]
427 public async Task Users_Token_GetAsync()
428 {
429 string connectionName = Environment.GetEnvironmentVariable("TEST_CONNECTION_NAME")
430 ?? throw new InvalidOperationException("TEST_CONNECTION_NAME not set");
431
432 IList<ConversationAccount> members = await _api.Conversations.Members.GetAsync(_f.ConversationId);
433 Assert.NotEmpty(members);
434
435 var result = await _api.Users.Token.GetAsync(members[0].Id!, connectionName, "msteams");
436 _output.WriteLine($"Token: {(result is not null ? "acquired" : "not available")}");
437 }
438
439 [Fact]
440 public async Task Users_Token_SignOutAsync()
441 {
442 string connectionName = Environment.GetEnvironmentVariable("TEST_CONNECTION_NAME")
443 ?? throw new InvalidOperationException("TEST_CONNECTION_NAME not set");
444
445 IList<ConversationAccount> members = await _api.Conversations.Members.GetAsync(_f.ConversationId);
446 Assert.NotEmpty(members);
447
448 await _api.Users.Token.SignOutAsync(members[0].Id!, connectionName, "msteams");
449 _output.WriteLine("SignOut completed");
450 }
451
452 #endregion
453
454 #region ForServiceUrl
455
456 [Fact]
457 public async Task ForServiceUrl_CreatesScopedClient()
458 {
459 ApiClient scoped = _f.ApiClient.ForServiceUrl(_f.ServiceUrl);
460
461 Assert.NotNull(scoped.Conversations);
462 Assert.NotNull(scoped.Teams);
463 Assert.NotNull(scoped.Meetings);
464 Assert.Equal(_f.ServiceUrl, scoped.ServiceUrl);
465
466 // Verify the scoped client can make a real call
467 IList<ConversationAccount> members = await scoped.Conversations.Members.GetAsync(_f.ConversationId);
468 Assert.NotNull(members);
469 Assert.NotEmpty(members);
470 _output.WriteLine($"ForServiceUrl scoped client retrieved {members.Count} members");
471 }
472
473 #endregion
474}
475