microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v2.0.8

Branches

Tags

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

Clone

HTTPS

Download ZIP

core/test/IntegrationTests/ApiClientTests.cs

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