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

Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs

296lines · modecode

1using System.Net;
2using System.Text;
3using System.Text.Json;
4
5using Microsoft.AspNetCore.Http;
6using Microsoft.Teams.Api;
7using Microsoft.Teams.Api.Activities;
8using Microsoft.Teams.Api.Auth;
9using Microsoft.Teams.Apps;
10using Microsoft.Teams.Apps.Events;
11using Microsoft.Teams.Common.Logging;
12
13using Moq;
14
15namespace Microsoft.Teams.Plugins.AspNetCore.Tests;
16
17public class AspNetCorePluginTests
18{
19 private static AspNetCorePlugin CreatePlugin(Mock<ILogger>? loggerMock = null, EventFunction? events = null)
20 {
21 var plugin = new AspNetCorePlugin();
22 if (loggerMock is not null)
23 {
24 plugin.Logger = loggerMock.Object;
25 }
26 else
27 {
28 plugin.Logger = new ConsoleLogger("Test", LogLevel.Debug);
29 }
30 plugin.Client = new Mock<Microsoft.Teams.Common.Http.IHttpClient>().Object;
31 if (events is not null)
32 {
33 plugin.Events += events;
34 }
35 return plugin;
36 }
37
38 private static DefaultHttpContext CreateHttpContext(IActivity activity, string bearer = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ3MDI1MTUyMDB9.signature")
39 {
40 var ctx = new DefaultHttpContext();
41 ctx.TraceIdentifier = Guid.NewGuid().ToString();
42 ctx.Request.Headers.Append("Authorization", $"Bearer {bearer}");
43 var json = JsonSerializer.Serialize(activity, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull });
44 var bytes = Encoding.UTF8.GetBytes(json);
45 ctx.Request.Body = new MemoryStream(bytes);
46 ctx.Request.ContentLength = bytes.Length;
47 return ctx;
48 }
49
50 private static MessageActivity CreateMessageActivity(string? serviceUrl = null)
51 {
52 return new MessageActivity("hi")
53 {
54 From = new() { Id = "user" },
55 Recipient = new() { Id = "bot" },
56 Conversation = new Conversation() { Id = "conv", Type = ConversationType.Personal },
57 ServiceUrl = serviceUrl
58 };
59 }
60
61 // Builds an unsigned-but-parseable JWT carrying the given serviceurl claim (omitted when null).
62 // JsonWebToken/JwtSecurityTokenHandler.ReadJwtToken parses without verifying the signature.
63 private static string CreateJwt(string? serviceUrl)
64 {
65 var payload = new Dictionary<string, object> { ["exp"] = 4702515200L };
66 if (serviceUrl is not null)
67 {
68 payload["serviceurl"] = serviceUrl;
69 }
70 return $"{Base64Url(new { alg = "HS256", typ = "JWT" })}.{Base64Url(payload)}.signature";
71 }
72
73 private static string Base64Url(object value) =>
74 Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value)))
75 .TrimEnd('=').Replace('+', '-').Replace('/', '_');
76
77 [Fact]
78 public async Task Test_Do_Http_CallsExtractTokenAndActivity_AndCallsCoreDo()
79 {
80 // Arrange
81 var activity = CreateMessageActivity();
82 var coreResponse = new Response(HttpStatusCode.Accepted, new { ok = true });
83 var eventsCalled = new List<string>();
84
85 EventFunction events = (plugin, name, payload, ct) =>
86 {
87 eventsCalled.Add(name);
88 if (name == "activity") return Task.FromResult<object?>(coreResponse); // returned directly by core Do
89 return Task.FromResult<object?>(null);
90 };
91
92 var logger = new Mock<ILogger>();
93 var plugin = CreatePlugin(logger, events);
94 var ctx = CreateHttpContext(activity);
95
96 // Act
97 var result = await plugin.Do(ctx);
98
99 // Assert
100 Assert.Contains("activity", eventsCalled);
101 var jsonResult = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<object?>>(result);
102 Assert.Equal((int)coreResponse.Status, jsonResult.StatusCode);
103 }
104
105 [Fact]
106 public async Task Test_Do_Http_SetsHeadersFromResponseMeta()
107 {
108 // Arrange
109 var activity = CreateMessageActivity();
110 var response = new Response(HttpStatusCode.OK, new { hello = "world" });
111 response.Meta.Add("routes", 3);
112 response.Meta.Add("custom", "value");
113
114 EventFunction events = (plugin, name, payload, ct) =>
115 {
116 if (name == "activity") return Task.FromResult<object?>(response);
117 return Task.FromResult<object?>(null);
118 };
119
120 var plugin = CreatePlugin(new Mock<ILogger>(), events);
121 var ctx = CreateHttpContext(activity);
122
123 // Act
124 var result = await plugin.Do(ctx);
125
126 // Assert body result type
127 var jsonResult = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<object?>>(result);
128 Assert.Equal((int)HttpStatusCode.OK, jsonResult.StatusCode);
129 // Headers: routes & custom should be present
130 Assert.Contains("X-Teams-Routes", ctx.Response.Headers.Keys); // capitalized first char
131 Assert.Contains("X-Teams-Custom", ctx.Response.Headers.Keys);
132 }
133
134 [Fact]
135 public async Task Test_Do_Http_ErrorPath_ProducesProblemResult()
136 {
137 // Arrange -> throw inside events
138 EventFunction events = (plugin, name, payload, ct) =>
139 {
140 if (name == "activity") throw new InvalidOperationException("boom");
141 return Task.FromResult<object?>(null);
142 };
143
144 var logger = new Mock<ILogger>();
145 var plugin = CreatePlugin(logger, events);
146 var ctx = CreateHttpContext(CreateMessageActivity());
147
148 // Act
149 var result = await plugin.Do(ctx);
150
151 // Assert
152 var problem = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<object>>(result);
153 Assert.Equal(500, problem.StatusCode);
154 Assert.Contains("boom", problem.Value!.ToString());
155 logger.Verify(l => l.Error(It.IsAny<object[]>()), Times.AtLeastOnce);
156 }
157
158 [Fact]
159 public void Test_ExtractToken_ReturnsToken()
160 {
161 var plugin = CreatePlugin();
162 var ctx = CreateHttpContext(CreateMessageActivity(), "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ3MDI1MTUyMDB9.token123");
163
164 var token = plugin.ExtractToken(ctx.Request);
165 Assert.NotNull(token);
166 Assert.Contains("token123", token.ToString());
167 }
168
169 [Fact]
170 public async Task Test_ExtractActivity_ReturnsActivity()
171 {
172 var plugin = CreatePlugin();
173 var activity = CreateMessageActivity();
174 var ctx = CreateHttpContext(activity);
175
176 var extracted = await plugin.ParseActivity(ctx.Request);
177 Assert.NotNull(extracted);
178 Assert.True(activity.Type.Equals(extracted.Type));
179 }
180
181 [Fact]
182 public async Task Test_ExtractActivity_HttpRequestBodyAlreadyRead_ReturnsActivity()
183 {
184 var plugin = CreatePlugin();
185 var activity = CreateMessageActivity();
186 var ctx = CreateHttpContext(activity);
187 // simulate body already read by setting position to end
188 ctx.Request.Body.Position = ctx.Request.Body.Length;
189
190 var extracted = await plugin.ParseActivity(ctx.Request);
191 Assert.NotNull(extracted);
192 Assert.True(activity.Type.Equals(extracted.Type));
193 }
194
195 [Fact]
196 public async Task Test_Do_Core_ReturnsResponseAndLogs()
197 {
198 // Arrange core path tests the ActivityEvent Do(ActivityEvent)
199 var response = new Response(HttpStatusCode.OK, new { test = 1 });
200 EventFunction events = (plugin, name, payload, ct) =>
201 {
202 if (name == "activity") return Task.FromResult<object?>(response);
203 return Task.FromResult<object?>(null);
204 };
205 var logger = new Mock<ILogger>();
206 var plugin = CreatePlugin(logger, events);
207 var evt = new ActivityEvent() { Token = new JsonWebToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ3MDI1MTUyMDB9.signature"), Activity = CreateMessageActivity() };
208
209 // Act
210 var res = await plugin.Do(evt);
211
212 // Assert
213 Assert.Same(response, res);
214 logger.Verify(l => l.Debug(It.IsAny<object[]>()), Times.AtLeastOnce);
215 }
216
217 [Theory]
218 [InlineData("https://smba.trafficmanager.net/teams/", "https://smba.trafficmanager.net/teams/")] // exact match
219 [InlineData("HTTPS://SMBA.TRAFFICMANAGER.NET/teams/", "https://smba.trafficmanager.net/teams/")] // case-insensitive
220 [InlineData("https://smba.trafficmanager.net/teams", "https://smba.trafficmanager.net/teams/")] // trailing-slash normalized
221 public async Task Test_Do_Http_ServiceUrlClaimMatches_Processes(string claimServiceUrl, string activityServiceUrl)
222 {
223 // Arrange
224 var activity = CreateMessageActivity(activityServiceUrl);
225 var coreResponse = new Response(HttpStatusCode.Accepted, new { ok = true });
226 var eventsCalled = new List<string>();
227 EventFunction events = (plugin, name, payload, ct) =>
228 {
229 eventsCalled.Add(name);
230 if (name == "activity") return Task.FromResult<object?>(coreResponse);
231 return Task.FromResult<object?>(null);
232 };
233 var plugin = CreatePlugin(new Mock<ILogger>(), events);
234 var ctx = CreateHttpContext(activity, CreateJwt(claimServiceUrl));
235
236 // Act
237 var result = await plugin.Do(ctx);
238
239 // Assert
240 Assert.Contains("activity", eventsCalled);
241 var jsonResult = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<object?>>(result);
242 Assert.Equal((int)coreResponse.Status, jsonResult.StatusCode);
243 }
244
245 [Fact]
246 public async Task Test_Do_Http_ServiceUrlClaimMismatch_ReturnsUnauthorized()
247 {
248 // Arrange
249 var activity = CreateMessageActivity("https://smba.trafficmanager.net/teams/");
250 var eventsCalled = new List<string>();
251 EventFunction events = (plugin, name, payload, ct) =>
252 {
253 eventsCalled.Add(name);
254 if (name == "activity") return Task.FromResult<object?>(new Response(HttpStatusCode.OK, new { ok = true }));
255 return Task.FromResult<object?>(null);
256 };
257 var logger = new Mock<ILogger>();
258 var plugin = CreatePlugin(logger, events);
259 var ctx = CreateHttpContext(activity, CreateJwt("https://evil.example.com/"));
260
261 // Act
262 var result = await plugin.Do(ctx);
263
264 // Assert: rejected with 401, activity never dispatched, neither URL echoed, logged server-side.
265 var unauthorized = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.UnauthorizedHttpResult>(result);
266 Assert.Equal(401, unauthorized.StatusCode);
267 Assert.DoesNotContain("activity", eventsCalled);
268 logger.Verify(l => l.Warn(It.IsAny<object[]>()), Times.Once);
269 }
270
271 [Fact]
272 public async Task Test_Do_Http_NoServiceUrlClaim_ReturnsUnauthorized()
273 {
274 // Arrange: activity carries a serviceUrl but the token has no serviceurl claim.
275 var activity = CreateMessageActivity("https://smba.trafficmanager.net/teams/");
276 var eventsCalled = new List<string>();
277 EventFunction events = (plugin, name, payload, ct) =>
278 {
279 eventsCalled.Add(name);
280 if (name == "activity") return Task.FromResult<object?>(new Response(HttpStatusCode.OK, new { ok = true }));
281 return Task.FromResult<object?>(null);
282 };
283 var logger = new Mock<ILogger>();
284 var plugin = CreatePlugin(logger, events);
285 var ctx = CreateHttpContext(activity, CreateJwt(null));
286
287 // Act
288 var result = await plugin.Do(ctx);
289
290 // Assert: a token without a serviceurl claim is rejected when the activity has one.
291 var unauthorized = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.UnauthorizedHttpResult>(result);
292 Assert.Equal(401, unauthorized.StatusCode);
293 Assert.DoesNotContain("activity", eventsCalled);
294 logger.Verify(l => l.Warn(It.IsAny<object[]>()), Times.Once);
295 }
296}