microsoft/teams.net

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feature/extended-markdown-text-format

Branches

Tags

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

Clone

HTTPS

Download ZIP

core/test/IntegrationTests/CompatTeamsInfoTests.cs

385lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using Microsoft.Bot.Builder;
5using Microsoft.Bot.Connector;
6using Microsoft.Bot.Schema;
7using Microsoft.Bot.Schema.Teams;
8using Microsoft.Teams.Apps.Api.Clients;
9using Microsoft.Teams.Apps.BotBuilder;
10using Microsoft.Teams.Apps.Schema;
11using Xunit.Abstractions;
12using TeamsChannelData = Microsoft.Bot.Schema.Teams.TeamsChannelData;
13
14namespace IntegrationTests;
15
16/// <summary>
17/// Integration tests for <see cref="TeamsApiClient"/> static methods making real API calls.
18/// These tests verify that TeamsApiClient correctly bridges Bot Framework ITurnContext
19/// to the underlying ConversationClient and ApiClient, producing valid compat types.
20/// </summary>
21public class TeamsApiClientTests : IClassFixture<IntegrationTestFixture>
22{
23 private readonly IntegrationTestFixture _f;
24 private readonly ITestOutputHelper _output;
25
26 public TeamsApiClientTests(IntegrationTestFixture fixture, ITestOutputHelper output)
27 {
28 _f = fixture;
29 _f.OutputHelper = output;
30 _output = output;
31 }
32
33 private ChannelAccount CreateFromAccount()
34 {
35 ChannelAccount from = new() { Id = "bot" };
36 if (_f.AgenticIdentity is not null)
37 {
38 from.Properties.Add("agenticAppId", _f.AgenticIdentity.AgenticAppId);
39 from.Properties.Add("agenticUserId", _f.AgenticIdentity.AgenticUserId);
40 from.Properties.Add("agenticAppBlueprintId", _f.AgenticIdentity.AgenticAppBlueprintId);
41 }
42
43 return from;
44 }
45
46 /// <summary>
47 /// Creates an ITurnContext wired to real clients, simulating what TeamsBotFrameworkHttpAdapter does.
48 /// </summary>
49 private TurnContext CreateTurnContext(
50 string? conversationId = null,
51 string? teamId = null,
52 string? meetingId = null,
53 string? tenantId = null)
54 {
55 Activity activity = new()
56 {
57 Type = ActivityTypes.Message,
58 ServiceUrl = _f.ServiceUrl.ToString(),
59 ChannelId = "msteams",
60 Conversation = new Microsoft.Bot.Schema.ConversationAccount { Id = conversationId ?? _f.ConversationId },
61 From = CreateFromAccount(),
62 Recipient = new ChannelAccount { Id = "user" },
63 };
64
65 // Set TeamsChannelData if teamId or meetingId is provided
66 if (teamId != null || meetingId != null || tenantId != null)
67 {
68 TeamsChannelData channelData = new();
69 if (teamId != null)
70 {
71 channelData.Team = new TeamInfo { Id = teamId };
72 }
73
74 if (meetingId != null)
75 {
76 channelData.Meeting = new TeamsMeetingInfo { Id = meetingId };
77 }
78
79 if (tenantId != null)
80 {
81 channelData.Tenant = new TenantInfo { Id = tenantId };
82 }
83
84 activity.ChannelData = channelData;
85 }
86
87 // Create a stub adapter (BotAdapter is abstract, use SimpleAdapter)
88 SimpleAdapter adapter = new();
89 TurnContext turnContext = new(adapter, activity);
90
91 // Wire up CompatConnectorClient with real ConversationClient (same as TeamsBotFrameworkHttpAdapter does)
92 CompatConversations compatConversations = new(_f.ConversationClient)
93 {
94 ServiceUrl = _f.ServiceUrl.ToString(),
95 AgenticIdentity = _f.AgenticIdentity
96 };
97 CompatConnectorClient connectorClient = new(compatConversations);
98 turnContext.TurnState.Add<IConnectorClient>(connectorClient);
99
100 // Wire up scoped ApiClient (same as TeamsBotFrameworkHttpAdapter does)
101 ApiClient scopedApi = _f.ScopedApiClient;
102 turnContext.TurnState.Add(scopedApi);
103
104 return turnContext;
105 }
106
107 #region Member Methods (non-team scope)
108
109 [Fact(Timeout = 5000, Skip = "GET /members throttled on canary — cached fixture needed")]
110 public async Task GetMemberAsync_ReturnsTeamsChannelAccount()
111 {
112
113 // First get a valid MRI-format member ID
114 ApiClient api = _f.ScopedApiClient;
115 IList<TeamsConversationAccount?> members = await api.Conversations.Members.GetAsync(_f.ConversationId, _f.AgenticIdentity);
116 Assert.NotEmpty(members);
117 string memberId = members[0]?.Id!;
118
119 using TurnContext ctx = CreateTurnContext();
120 TeamsChannelAccount result = await TeamsApiClient.GetMemberAsync(ctx, memberId);
121
122 Assert.NotNull(result);
123 Assert.Equal(memberId, result.Id);
124 _output.WriteLine($"GetMember: {result.Id} — {result.Name}, Email: {result.Email}, UPN: {result.UserPrincipalName}");
125 }
126
127 [Fact(Timeout = 5000, Skip = "GET /members throttled on canary — cached fixture needed")]
128 public async Task GetMembersAsync_ReturnsTeamsChannelAccounts()
129 {
130
131 using TurnContext ctx = CreateTurnContext();
132 IEnumerable<TeamsChannelAccount> result = await TeamsApiClient.GetMembersAsync(ctx);
133
134 Assert.NotNull(result);
135 List<TeamsChannelAccount> members = [.. result];
136 Assert.NotEmpty(members);
137
138 foreach (TeamsChannelAccount m in members)
139 {
140 _output.WriteLine($"GetMembers: {m.Id} — {m.Name}");
141 }
142 }
143
144 [Fact(Timeout = 5000, Skip = "GET /members throttled on canary — cached fixture needed")]
145 public async Task GetPagedMembersAsync_ReturnsPaged()
146 {
147
148 using TurnContext ctx = CreateTurnContext();
149 TeamsPagedMembersResult result = await TeamsApiClient.GetPagedMembersAsync(ctx, pageSize: 2);
150
151 Assert.NotNull(result);
152 Assert.NotNull(result.Members);
153 Assert.NotEmpty(result.Members);
154
155 foreach (TeamsChannelAccount m in result.Members)
156 {
157 _output.WriteLine($"PagedMember: {m.Id} — {m.Name}");
158 }
159
160 _output.WriteLine($"ContinuationToken: {result.ContinuationToken ?? "(null)"}");
161 }
162
163 #endregion
164
165 #region Team-scoped Member Methods
166
167 [Fact(Timeout = 5000, Skip = "GET /members throttled on canary — cached fixture needed")]
168 public async Task GetTeamMemberAsync_ReturnsTeamsChannelAccount()
169 {
170
171 // Get a valid MRI-format member ID from the team
172 ApiClient api = _f.ScopedApiClient;
173 IList<TeamsConversationAccount?> members = await api.Conversations.Members.GetAsync(_f.TeamId, _f.AgenticIdentity);
174 Assert.NotEmpty(members);
175 string memberId = members[0]?.Id!;
176
177 using TurnContext ctx = CreateTurnContext(teamId: _f.TeamId);
178 TeamsChannelAccount result = await TeamsApiClient.GetTeamMemberAsync(ctx, memberId, _f.TeamId);
179
180 Assert.NotNull(result);
181 Assert.Equal(memberId, result.Id);
182 _output.WriteLine($"GetTeamMember: {result.Id} — {result.Name}, Email: {result.Email}");
183 }
184
185 [Fact(Timeout = 5000, Skip = "GET /members throttled on canary — cached fixture needed")]
186 public async Task GetMemberAsync_WithTeamScope_DelegatesToGetTeamMember()
187 {
188
189 // When activity has TeamInfo, GetMemberAsync should delegate to GetTeamMemberAsync
190 ApiClient api = _f.ScopedApiClient;
191 IList<TeamsConversationAccount?> members = await api.Conversations.Members.GetAsync(_f.TeamId, _f.AgenticIdentity);
192 Assert.NotEmpty(members);
193 string memberId = members[0]?.Id!;
194
195 using TurnContext ctx = CreateTurnContext(teamId: _f.TeamId);
196 TeamsChannelAccount result = await TeamsApiClient.GetMemberAsync(ctx, memberId);
197
198 Assert.NotNull(result);
199 Assert.Equal(memberId, result.Id);
200 _output.WriteLine($"GetMember (team scope): {result.Id} — {result.Name}");
201 }
202
203 [Fact(Timeout = 5000, Skip = "GET /members throttled on canary — cached fixture needed")]
204 public async Task GetTeamMembersAsync_ReturnsMembers()
205 {
206
207 using TurnContext ctx = CreateTurnContext(teamId: _f.TeamId);
208 IEnumerable<TeamsChannelAccount> result = await TeamsApiClient.GetTeamMembersAsync(ctx, _f.TeamId);
209
210 Assert.NotNull(result);
211 List<TeamsChannelAccount> members = [.. result];
212 Assert.NotEmpty(members);
213
214 foreach (TeamsChannelAccount m in members)
215 {
216 _output.WriteLine($"TeamMember: {m.Id} — {m.Name}");
217 }
218 }
219
220 [Fact(Timeout = 5000, Skip = "GET /members throttled on canary — cached fixture needed")]
221 public async Task GetPagedTeamMembersAsync_ReturnsPaged()
222 {
223
224 using TurnContext ctx = CreateTurnContext(teamId: _f.TeamId);
225 TeamsPagedMembersResult result = await TeamsApiClient.GetPagedTeamMembersAsync(ctx, _f.TeamId, pageSize: 2);
226
227 Assert.NotNull(result);
228 Assert.NotNull(result.Members);
229 Assert.NotEmpty(result.Members);
230
231 foreach (TeamsChannelAccount m in result.Members)
232 {
233 _output.WriteLine($"PagedTeamMember: {m.Id} — {m.Name}");
234 }
235
236 _output.WriteLine($"ContinuationToken: {result.ContinuationToken ?? "(null)"}");
237 }
238
239 #endregion
240
241 #region Team & Channel Methods
242
243 [Fact(Timeout = 5000)]
244 public async Task GetTeamDetailsAsync_ReturnsDetails()
245 {
246
247 using TurnContext ctx = CreateTurnContext(teamId: _f.TeamId);
248 TeamDetails result = await TeamsApiClient.GetTeamDetailsAsync(ctx, _f.TeamId);
249
250 Assert.NotNull(result);
251 Assert.NotNull(result.Id);
252 Assert.NotNull(result.Name);
253 _output.WriteLine($"TeamDetails: {result.Id} — {result.Name}, AadGroupId: {result.AadGroupId}");
254 }
255
256 [Fact(Timeout = 5000)]
257 public async Task GetTeamDetailsAsync_InfersTeamIdFromActivity()
258 {
259
260 // When teamId is null, it should be inferred from the activity's TeamsChannelData
261 using TurnContext ctx = CreateTurnContext(teamId: _f.TeamId);
262 TeamDetails result = await TeamsApiClient.GetTeamDetailsAsync(ctx);
263
264 Assert.NotNull(result);
265 Assert.NotNull(result.Id);
266 _output.WriteLine($"TeamDetails (inferred): {result.Id} — {result.Name}");
267 }
268
269 [Fact(Timeout = 5000)]
270 public async Task GetTeamChannelsAsync_ReturnsChannels()
271 {
272
273 using TurnContext ctx = CreateTurnContext(teamId: _f.TeamId);
274 ConversationList result = await TeamsApiClient.GetTeamChannelsAsync(ctx, _f.TeamId);
275
276 Assert.NotNull(result);
277 Assert.NotNull(result.Conversations);
278 Assert.NotEmpty(result.Conversations);
279
280 foreach (ChannelInfo ch in result.Conversations)
281 {
282 _output.WriteLine($"Channel: {ch.Id} — {ch.Name}");
283 }
284 }
285
286 [Fact(Timeout = 5000)]
287 public async Task GetTeamChannelsAsync_InfersTeamIdFromActivity()
288 {
289
290 using TurnContext ctx = CreateTurnContext(teamId: _f.TeamId);
291 ConversationList result = await TeamsApiClient.GetTeamChannelsAsync(ctx);
292
293 Assert.NotNull(result);
294 Assert.NotNull(result.Conversations);
295 Assert.NotEmpty(result.Conversations);
296 _output.WriteLine($"Channels (inferred): {result.Conversations.Count} channels found");
297 }
298
299 #endregion
300
301 #region Meeting Methods
302
303 [Fact(Timeout = 5000)]
304 public async Task GetMeetingParticipantAsync_ReturnsParticipant()
305 {
306
307 // The meetings participant API requires AAD object ID, not MRI/pairwise bot framework ID.
308 // Get the AAD object ID from a human member (bots don't have one).
309 ApiClient api = _f.ScopedApiClient;
310 IList<TeamsConversationAccount?> members = await api.Conversations.Members.GetAsync(_f.ConversationId, _f.AgenticIdentity);
311 Assert.NotEmpty(members);
312
313 string? aadObjectId = null;
314 foreach (TeamsConversationAccount? m in members)
315 {
316 TeamsConversationAccount tm = await api.Conversations.Members
317 .GetByIdAsync<TeamsConversationAccount>(_f.ConversationId, m?.Id!, _f.AgenticIdentity);
318 _output.WriteLine($"Member: {tm.Name} — AadObjectId: {tm.AadObjectId ?? "(null)"}, Properties: [{string.Join(", ", tm.Properties.Keys)}]");
319 if (tm.AadObjectId is not null)
320 {
321 aadObjectId = tm.AadObjectId;
322 break;
323 }
324 }
325
326 if (aadObjectId is null)
327 {
328 _output.WriteLine("SKIP: No members with AAD object ID found in test conversation");
329 return;
330 }
331
332 using TurnContext ctx = CreateTurnContext(meetingId: _f.MeetingId, tenantId: _f.TenantId);
333 TeamsMeetingParticipant result = await TeamsApiClient.GetMeetingParticipantAsync(
334 ctx, _f.MeetingId, aadObjectId, _f.TenantId);
335
336 Assert.NotNull(result);
337 _output.WriteLine($"Participant: {result.User?.Id} — Role: {result.Meeting?.Role}, InMeeting: {result.Meeting?.InMeeting}");
338 }
339
340 #endregion
341
342 #region Error Cases
343
344 [Fact(Timeout = 5000)]
345 public async Task GetTeamDetailsAsync_ThrowsWithoutTeamScope()
346 {
347 // No teamId in activity and no explicit teamId parameter
348 using TurnContext ctx = CreateTurnContext();
349 await Assert.ThrowsAsync<InvalidOperationException>(
350 () => TeamsApiClient.GetTeamDetailsAsync(ctx));
351 }
352
353 [Fact(Timeout = 5000)]
354 public async Task GetTeamChannelsAsync_ThrowsWithoutTeamScope()
355 {
356 using TurnContext ctx = CreateTurnContext();
357 await Assert.ThrowsAsync<InvalidOperationException>(
358 () => TeamsApiClient.GetTeamChannelsAsync(ctx));
359 }
360
361 [Fact(Timeout = 5000)]
362 public async Task GetMemberAsync_ThrowsWithNullUserId()
363 {
364 using TurnContext ctx = CreateTurnContext();
365 await Assert.ThrowsAsync<InvalidOperationException>(
366 () => TeamsApiClient.GetMemberAsync(ctx, null!));
367 }
368
369 #endregion
370
371 /// <summary>
372 /// Minimal BotAdapter stub for creating TurnContext in tests.
373 /// </summary>
374 private sealed class SimpleAdapter : BotAdapter
375 {
376 public override Task DeleteActivityAsync(ITurnContext turnContext, ConversationReference reference, CancellationToken cancellationToken)
377 => Task.CompletedTask;
378
379 public override Task<ResourceResponse[]> SendActivitiesAsync(ITurnContext turnContext, Activity[] activities, CancellationToken cancellationToken)
380 => Task.FromResult(Array.Empty<ResourceResponse>());
381
382 public override Task<ResourceResponse> UpdateActivityAsync(ITurnContext turnContext, Activity activity, CancellationToken cancellationToken)
383 => Task.FromResult(new ResourceResponse());
384 }
385}
386