// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Teams.Apps.Api.Clients;
using Microsoft.Teams.Apps.Handlers;
using Microsoft.Teams.Apps.Handlers.MessageExtension;
using Microsoft.Teams.Apps.Handlers.TaskModules;
using Microsoft.Teams.Apps.Schema;
using Microsoft.Teams.Apps.State;
using Microsoft.Teams.Core;
using Moq;
namespace Microsoft.Teams.Apps.UnitTests;
///
/// Verifies that all handler extension methods propagate State from the
/// original context to the typed context passed to the user's handler,
/// and that handlers work without state configured.
///
public class HandlerStatePropagationTests
{
// ===== Invoke handlers with state =====
[Fact]
public async Task OnAdaptiveCardAction_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnAdaptiveCardAction((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.AdaptiveCardAction);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnFileConsent_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnFileConsent((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.FileConsent);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnQuery_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnQuery((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.MessageExtensionQuery);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnSubmitAction_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnSubmitAction((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.MessageExtensionSubmitAction);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnQueryLink_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnQueryLink((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.MessageExtensionQueryLink);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnAnonQueryLink_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnAnonQueryLink((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.MessageExtensionAnonQueryLink);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnFetchTask_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnFetchTask((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.MessageExtensionFetchTask);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnSelectItem_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnSelectItem((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.MessageExtensionSelectItem);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnQuerySettingUrl_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnQuerySettingUrl((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.MessageExtensionQuerySettingUrl);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnMessageFetchTask_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnMessageFetchTask((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.MessageFetchTask);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnMessageSubmitAction_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnMessageSubmitAction((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.MessageSubmitAction);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnMessageSubmitFeedback_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnMessageSubmitFeedback((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
InvokeActivity activity = new()
{
Type = TeamsActivityTypes.Invoke,
Name = InvokeNames.MessageSubmitAction,
Value = new JsonObject
{
["actionName"] = "feedback",
["actionValue"] = new JsonObject()
}
};
Context context = new(app, activity);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnTaskFetch_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnTaskFetch((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.TaskFetch);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnTaskSubmit_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnTaskSubmit((ctx, _) =>
{
captured = ctx.State;
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.TaskSubmit);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchWithReturnAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
// ===== Event handlers with state =====
[Fact]
public async Task OnMeetingStart_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnMeetingStart((ctx, _) =>
{
captured = ctx.State;
return Task.CompletedTask;
});
Context context = CreateEventContext(app, EventNames.MeetingStart);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnMeetingEnd_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnMeetingEnd((ctx, _) =>
{
captured = ctx.State;
return Task.CompletedTask;
});
Context context = CreateEventContext(app, EventNames.MeetingEnd);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnMeetingJoin_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnMeetingJoin((ctx, _) =>
{
captured = ctx.State;
return Task.CompletedTask;
});
Context context = CreateEventContext(app, EventNames.MeetingParticipantJoin);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
[Fact]
public async Task OnMeetingLeave_PropagatesState()
{
TeamsBotApplication app = CreateApp();
TurnStateContainer? captured = null;
app.OnMeetingLeave((ctx, _) =>
{
captured = ctx.State;
return Task.CompletedTask;
});
Context context = CreateEventContext(app, EventNames.MeetingParticipantLeave);
TurnStateContainer state = CreateState();
context.State = state;
await app.Router.DispatchAsync(context);
Assert.NotNull(captured);
Assert.Same(state, captured);
}
// ===== Without state (HasState guard) =====
[Fact]
public async Task OnAdaptiveCardAction_WorksWithoutState()
{
TeamsBotApplication app = CreateApp();
bool handlerCalled = false;
app.OnAdaptiveCardAction((ctx, _) =>
{
handlerCalled = true;
Assert.False(ctx.HasState);
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.AdaptiveCardAction);
// No state set on context
await app.Router.DispatchWithReturnAsync(context);
Assert.True(handlerCalled);
}
[Fact]
public async Task OnMeetingStart_WorksWithoutState()
{
TeamsBotApplication app = CreateApp();
bool handlerCalled = false;
app.OnMeetingStart((ctx, _) =>
{
handlerCalled = true;
Assert.False(ctx.HasState);
return Task.CompletedTask;
});
Context context = CreateEventContext(app, EventNames.MeetingStart);
// No state set on context
await app.Router.DispatchAsync(context);
Assert.True(handlerCalled);
}
[Fact]
public async Task OnQuery_WorksWithoutState()
{
TeamsBotApplication app = CreateApp();
bool handlerCalled = false;
app.OnQuery((ctx, _) =>
{
handlerCalled = true;
Assert.False(ctx.HasState);
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.MessageExtensionQuery);
// No state set on context
await app.Router.DispatchWithReturnAsync(context);
Assert.True(handlerCalled);
}
[Fact]
public async Task OnTaskFetch_WorksWithoutState()
{
TeamsBotApplication app = CreateApp();
bool handlerCalled = false;
app.OnTaskFetch((ctx, _) =>
{
handlerCalled = true;
Assert.False(ctx.HasState);
return Task.FromResult(new InvokeResponse(200));
});
Context context = CreateInvokeContext(app, InvokeNames.TaskFetch);
// No state set on context
await app.Router.DispatchWithReturnAsync(context);
Assert.True(handlerCalled);
}
// ===== Helpers =====
private static TurnStateContainer CreateState()
{
TurnState convState = new();
convState.Set("test-key", "test-value");
return new TurnStateContainer(convState, new TurnState());
}
private static Context CreateInvokeContext(TeamsBotApplication app, string invokeName)
{
InvokeActivity activity = new() { Type = TeamsActivityTypes.Invoke, Name = invokeName };
return new Context(app, activity);
}
private static Context CreateEventContext(TeamsBotApplication app, string eventName)
{
EventActivity activity = new() { Type = TeamsActivityTypes.Event, Name = eventName };
return new Context(app, activity);
}
private static TeamsBotApplication CreateApp()
{
Mock mockUserTokenClient = new(
new HttpClient(),
new Mock().Object,
NullLogger.Instance);
Mock mockConversationClient = new(
new HttpClient(),
NullLogger.Instance);
ApiClient apiClient = new(
new HttpClient(),
mockConversationClient.Object,
mockUserTokenClient.Object);
return new TeamsBotApplication(
apiClient,
new HttpContextAccessor(),
NullLogger.Instance,
new TeamsBotApplicationOptions { AppId = "test-app-id" });
}
}