// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Linq; using MartinCostello.Logging.XUnit; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Api.Clients; using Microsoft.Teams.Apps.Schema; using Microsoft.Teams.Core; using Microsoft.Teams.Core.Schema; using Xunit.Abstractions; namespace IntegrationTests; /// /// Shared fixture that configures DI, acquires tokens, and exposes clients for integration tests. /// Reused across test classes via IClassFixture to avoid repeated token acquisition. /// public class IntegrationTestFixture : IAsyncLifetime, IDisposable, ITestOutputHelperAccessor { public ServiceProvider ServiceProvider { get; } public ConversationClient ConversationClient { get; } public ApiClient ApiClient { get; } public Uri ServiceUrl { get; } public string ConversationId { get; } public string UserId { get; } public string TeamId { get; } public string ChannelId { get; } public string MeetingId { get; } public string TenantId { get; } public string BotAppId { get; } public string? UserId2 { get; } public AgenticIdentity? AgenticIdentity { get; } /// /// True when running against the canary service endpoint. /// public bool IsCanary => ServiceUrl.Host.Contains("canary", StringComparison.OrdinalIgnoreCase); /// /// Cached conversation members — fetched once during InitializeAsync to avoid /// repeated /members calls that trigger throttling (429). /// public IList? CachedMembers { get; private set; } /// /// First member MRI from cache (convenience for tests needing a valid member ID). /// public string? MemberMri1 => CachedMembers?.FirstOrDefault()?.Id; /// /// Second member MRI from cache (for group chat tests requiring 2+ members). /// public string? MemberMri2 => CachedMembers?.Skip(1).FirstOrDefault()?.Id; /// /// Third member MRI from cache. /// public string? MemberMri3 => CachedMembers?.Skip(2).FirstOrDefault()?.Id; /// /// Set by each test class constructor to route ILogger output to xUnit's test output. /// public ITestOutputHelper? OutputHelper { get; set; } public IntegrationTestFixture() { IConfiguration configuration = new ConfigurationBuilder() .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) .AddEnvironmentVariables() .Build(); ServiceCollection services = new(); services.AddLogging(builder => { builder.AddXUnit(this); builder.AddFilter("System.Net", LogLevel.Warning); builder.AddFilter("Microsoft.Identity", LogLevel.Error); builder.AddFilter("Microsoft.Teams", LogLevel.Information); }); services.AddSingleton(configuration); services.AddTeamsBotApplication(); ServiceProvider = services.BuildServiceProvider(); ConversationClient = ServiceProvider.GetRequiredService(); ApiClient = ServiceProvider.GetRequiredService(); ServiceUrl = new Uri(Env("TEST_SERVICEURL", "https://smba.trafficmanager.net/teams/")); ConversationId = Env("TEST_CONVERSATIONID"); UserId = Env("TEST_USER_ID"); TeamId = Env("TEST_TEAMID"); ChannelId = Env("TEST_CHANNELID"); MeetingId = Env("TEST_MEETINGID"); TenantId = Env("TEST_TENANTID"); BotAppId = Env("AzureAd__ClientId"); UserId2 = Environment.GetEnvironmentVariable("TEST_USER_ID_2"); string? agenticAppId = Environment.GetEnvironmentVariable("TEST_AGENTIC_APPID"); string? agenticUserId = Environment.GetEnvironmentVariable("TEST_AGENTIC_USERID"); if (!string.IsNullOrEmpty(agenticAppId) && !string.IsNullOrEmpty(agenticUserId)) { string appBlueprintId = Env("AzureAd__ClientId"); ChannelAccount recipient = new() { AgenticAppBlueprintId = appBlueprintId, AgenticAppId = agenticAppId, AgenticUserId = agenticUserId }; AgenticIdentity = AgenticIdentity.FromAccount(recipient); } } /// /// Fetches and caches conversation members once for the entire test run. /// Filters out the bot itself and null entries. Fails fast if no usable members are found. /// public async Task InitializeAsync() { ApiClient scoped = ScopedApiClient; IList raw = await scoped.Conversations.Members.GetAsync(ConversationId, AgenticIdentity); string botMri = $"28:{BotAppId}"; CachedMembers = raw .Where(m => m?.Id is not null && !m.Id.Equals(botMri, StringComparison.OrdinalIgnoreCase)) .ToList(); if (CachedMembers.Count == 0) { throw new InvalidOperationException( "No usable members found in test conversation (all null or bot-only). " + "Ensure the conversation has at least 2 non-bot members."); } } public Task DisposeAsync() => Task.CompletedTask; public ApiClient ScopedApiClient => ApiClient.ForServiceUrl(ServiceUrl); public void Dispose() { ServiceProvider.Dispose(); GC.SuppressFinalize(this); } private static string Env(string name, string? fallback = null) => Environment.GetEnvironmentVariable(name) ?? fallback ?? throw new InvalidOperationException($"{name} environment variable not set"); internal static ChannelAccount GetChannelAccountWithAgenticProperties() { string agenticUserId = Env("TEST_AGENTIC_USERID"); string agenticAppId = Env("TEST_AGENTIC_APPID"); string agenticAppBlueprintId = Env("AzureAd__ClientId"); if (string.IsNullOrEmpty(agenticUserId)) { return new ChannelAccount(); } ChannelAccount account = new() { Id = agenticUserId, Name = "Agentic User", AgenticAppBlueprintId = agenticAppBlueprintId, AgenticAppId = agenticAppId, AgenticUserId = agenticUserId }; return account; } internal static AgenticIdentity GetAgenticIdentity() { string agenticUserId = Env("TEST_AGENTIC_USERID"); string agenticAppId = Env("TEST_AGENTIC_APPID"); string agenticAppBlueprintId = Env("AzureAd__ClientId"); if (string.IsNullOrEmpty(agenticUserId)) { return null!; } AgenticIdentity identity = new() { AgenticUserId = agenticUserId, AgenticAppId = agenticAppId, AgenticAppBlueprintId = agenticAppBlueprintId }; return identity; } }