Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion test/IntegrationTests/ApiClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ private static MessageActivityInput CreateMessageActivity(string text) =>
private static MessageActivityInput CreateMessageActivity(string text, ChannelAccount recipient)
{
MessageActivityInput activity = CreateMessageActivity(text);
return activity;
return activity.WithRecipient(TeamsChannelAccount.FromChannelAccount(recipient)!, isTargeted: true);
}

#region Activities
Expand Down
36 changes: 32 additions & 4 deletions test/IntegrationTests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ This project runs integration tests against Teams Server using bot and agentic i
- Bot app registration (client ID + secret)
- Agentic app registration (client ID + secret) — optional
- A team with at least one channel
- A scheduled meeting
- At least 2 test users in the conversation
- At least 3 non-bot users in the test conversation (`GroupChat_ThreeMembers` needs three)
Comment thread
corinagum marked this conversation as resolved.
- A regular scheduled meeting (not a channel meeting) with the bot app installed *in the meeting*
- An OAuth connection on the Azure Bot resource named to match `TEST_CONNECTION_NAME`

## RunSettings

Expand Down Expand Up @@ -44,6 +45,7 @@ Place your `.runsettings` files in the `.runsettings/` directory (gitignored).
<TEST_USER_ID>29:...</TEST_USER_ID>
<TEST_TEAMID>19:...@thread.tacv2</TEST_TEAMID>
<TEST_CHANNELID>19:...@thread.tacv2</TEST_CHANNELID>
<!-- base64 of "0#<19:meeting_...@thread.v2>#0" -->
<TEST_MEETINGID>MCM...</TEST_MEETINGID>
<TEST_TENANTID>YOUR_TENANT_ID</TEST_TENANTID>

Expand All @@ -52,6 +54,8 @@ Place your `.runsettings` files in the `.runsettings/` directory (gitignored).
<TEST_AGENTIC_USERID></TEST_AGENTIC_USERID>

<!-- Optional -->
<!-- TEST_USER_ID_2 is read by the fixture but consumed by no test today; safe to omit.
Multi-member tests read the live conversation roster instead. -->
<TEST_USER_ID_2>29:...</TEST_USER_ID_2>
<TEST_CONNECTION_NAME>aadv2</TEST_CONNECTION_NAME>
</EnvironmentVariables>
Expand Down Expand Up @@ -80,6 +84,14 @@ dotnet test IntegrationTests/IntegrationTests.csproj \
--logger "trx;LogFileName=botid-prod.trx"
```

### Throttling

The tenant enforces a call quota that a single full run already approaches. Running the full suite twice in quick succession produces a large batch of `TooManyRequests` / `"API calls quota exceeded"` failures that look exactly like real breakage. One observed back-to-back run reported 36 failures, 35 of which were quota errors rather than genuine problems.

- Leave roughly 10 minutes between full runs.
- When triaging any unexpected failure, grep the output for `TooManyRequests` before investigating anything else.
- While iterating on one area, use `--filter` to run just that category instead of the full suite.

### Trait Categories

| Category | Tests | Description |
Expand All @@ -90,12 +102,27 @@ dotnet test IntegrationTests/IntegrationTests.csproj \
| `Reactions` | 1 | Add and delete reactions |
| `Teams` | 6 | Get team details, channels |
| `Meetings` | 3 | Get participant, meeting details |
| `Bots` | 2 | Sign-in URL and resource |
| `Users` | 3 | Token get, status, sign-out |
| `Users` | 5 | Sign-in URL and resource, token get, status, sign-out |
| `Client` | 1 | ForServiceUrl scoped client |
| `Diagnostic` | 13 | Conversation creation matrix |
| `ErrorHandling` | 3 | Error cases (compat layer) |

> These account for 68 of the suite's 72 tests; the remaining 4 carry no `Category` trait. A filter naming a category that does not exist prints `No test matches the given testcase filter` and **exits 0**, so a typo looks exactly like a clean pass.

## Troubleshooting
Comment thread
corinagum marked this conversation as resolved.

| Symptom | Cause | Fix |
|---------|-------|-----|
| Many unrelated tests fail with `TooManyRequests` / `"API calls quota exceeded"` | Tenant quota exhausted by consecutive runs | Wait ~10 minutes and re-run. These are not real failures. |
| Every test fails during fixture initialization with an `AADSTS` error | Tenant expired or credentials wrong | Re-provision the tenant and regenerate the runsettings |
| Fixture throws `... environment variable not set` | A required variable is missing; `TEST_MEETINGID` and `TEST_TENANTID` are required even for unrelated categories | Populate the runsettings fully |
| `GroupChat_*` fail on `Assert.NotNull` before any API call | Conversation has fewer than 3 non-bot members | Add users to the channel |
| Meetings tests return `404 ConversationNotFound` | `TEST_MEETINGID` is wrong, stale, or a placeholder | Re-encode from the current meeting thread ID |
| Meetings tests return `403 BotNotInConversationRoster` | App is not installed in the meeting | Install it in the meeting, then message the bot in the meeting chat |
| `Meetings_GetByIdAsync` returns `403 NotEnoughPermissions` | Manifest lacks RSC `OnlineMeeting.ReadBasic.Chat` | Add the RSC permission, bump the manifest version, reinstall in the meeting |
| `Users_GetSignInResourceAsync` returns `400 Could not find Connection Setting` | OAuth connection missing on the Azure Bot resource | Create a connection named to match `TEST_CONNECTION_NAME` |
| A filtered run reports "No test matches" and exits 0 | Category name does not exist | Check the [Trait Categories](#trait-categories) table |

## Architecture

- **`IntegrationTestFixture`** — Shared xUnit fixture that configures DI, acquires auth tokens, and caches conversation members (to avoid 429 throttling).
Expand All @@ -104,6 +131,7 @@ dotnet test IntegrationTests/IntegrationTests.csproj \

## Known Limitations

- **Expected skips**: 5 tests are skipped by design via `Skip.If` guards (paged members and reactions on canary). Skips are not failures. A correctly provisioned tenant should report 72 total with 5 skipped and 0 failed.
- **Agentic identity**: Targeted activities, paged members, and reactions return 500/404 with agentic identity. These are service-side limitations pending investigation.
- **Group chat creation**: Bot-only identity cannot create group chats with `IsGroup=true` + multiple members via the conversations API.
- **User token tests**: `SignIn` and `Users` token tests are skipped when agentic identity is configured (not supported).
Expand Down
39 changes: 39 additions & 0 deletions test/Microsoft.Teams.Core.UnitTests/ConversationClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System.Net;
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Teams.Core.Schema;
using Moq;
Expand Down Expand Up @@ -185,6 +186,44 @@ public async Task UpdateActivityAsync_WithIsTargeted_AppendsQueryString()
Assert.Equal(HttpMethod.Put, capturedRequest.Method);
}

[Fact]
public async Task UpdateActivityAsync_WithIsTargeted_DoesNotSendRecipient()
{
string? capturedBody = null;
Mock<HttpMessageHandler> mockHttpMessageHandler = new();
mockHttpMessageHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.Returns(async (HttpRequestMessage req, CancellationToken ct) =>
{
capturedBody = req.Content is null ? null : await req.Content.ReadAsStringAsync(ct);
return new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("{\"id\":\"activity123\"}")
};
});

HttpClient httpClient = new(mockHttpMessageHandler.Object);
ConversationClient conversationClient = new(httpClient, NullLogger<ConversationClient>.Instance);

await conversationClient.UpdateActivityAsync(
"conv123",
"activity123",
CoreActivityInput.CreateBuilder().WithType(ActivityType.Message).Build(),
new Uri("https://test.service.url/"),
isTargeted: true);

Assert.NotNull(capturedBody);
using JsonDocument document = JsonDocument.Parse(capturedBody);
Assert.False(
document.RootElement.TryGetProperty("recipient", out _),
"Targeted update payloads must omit 'recipient'; the service rejects it on update.");
}

[Fact]
public async Task DeleteActivityAsync_WithIsTargeted_AppendsQueryString()
{
Expand Down
Loading