microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
docs/update-release-process

Branches

Tags

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

Clone

HTTPS

Download ZIP

core/test/IntegrationTests/ApiClientTests.cs

478lines · modecode

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