diff --git a/test/IntegrationTests/ApiClientTests.cs b/test/IntegrationTests/ApiClientTests.cs
index 95d56efc..bc1fcfe8 100644
--- a/test/IntegrationTests/ApiClientTests.cs
+++ b/test/IntegrationTests/ApiClientTests.cs
@@ -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
diff --git a/test/IntegrationTests/README.md b/test/IntegrationTests/README.md
index 965938ae..76d88526 100644
--- a/test/IntegrationTests/README.md
+++ b/test/IntegrationTests/README.md
@@ -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)
+ - 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
@@ -44,6 +45,7 @@ Place your `.runsettings` files in the `.runsettings/` directory (gitignored).
29:...
19:...@thread.tacv2
19:...@thread.tacv2
+
MCM...
YOUR_TENANT_ID
@@ -52,6 +54,8 @@ Place your `.runsettings` files in the `.runsettings/` directory (gitignored).
+
29:...
aadv2
@@ -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 |
@@ -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
+
+| 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).
@@ -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).
diff --git a/test/Microsoft.Teams.Core.UnitTests/ConversationClientTests.cs b/test/Microsoft.Teams.Core.UnitTests/ConversationClientTests.cs
index 3ba9d1b0..8e1948db 100644
--- a/test/Microsoft.Teams.Core.UnitTests/ConversationClientTests.cs
+++ b/test/Microsoft.Teams.Core.UnitTests/ConversationClientTests.cs
@@ -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;
@@ -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 mockHttpMessageHandler = new();
+ mockHttpMessageHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .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.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()
{