microsoft/teams.net

Public

mirrored from https://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fix/issue-274-skipauth-unauthorized

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

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

256lines · 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 DefaultHttpContext CreateHttpContextWithoutAuth(IActivity activity)
51 {
52 var ctx = new DefaultHttpContext();
53 ctx.TraceIdentifier = Guid.NewGuid().ToString();
54 // No Authorization header
55 var json = JsonSerializer.Serialize(activity, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull });
56 var bytes = Encoding.UTF8.GetBytes(json);
57 ctx.Request.Body = new MemoryStream(bytes);
58 ctx.Request.ContentLength = bytes.Length;
59 return ctx;
60 }
61
62 private static MessageActivity CreateMessageActivity()
63 {
64 return new MessageActivity("hi")
65 {
66 From = new() { Id = "user" },
67 Recipient = new() { Id = "bot" },
68 Conversation = new Conversation() { Id = "conv", Type = ConversationType.Personal }
69 };
70 }
71
72 [Fact]
73 public async Task Test_Do_Http_CallsExtractTokenAndActivity_AndCallsCoreDo()
74 {
75 // Arrange
76 var activity = CreateMessageActivity();
77 var coreResponse = new Response(HttpStatusCode.Accepted, new { ok = true });
78 var eventsCalled = new List<string>();
79
80 EventFunction events = (plugin, name, payload, ct) =>
81 {
82 eventsCalled.Add(name);
83 if (name == "activity") return Task.FromResult<object?>(coreResponse); // returned directly by core Do
84 return Task.FromResult<object?>(null);
85 };
86
87 var logger = new Mock<ILogger>();
88 var plugin = CreatePlugin(logger, events);
89 var ctx = CreateHttpContext(activity);
90
91 // Act
92 var result = await plugin.Do(ctx);
93
94 // Assert
95 Assert.Contains("activity", eventsCalled);
96 var jsonResult = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<object?>>(result);
97 Assert.Equal((int)coreResponse.Status, jsonResult.StatusCode);
98 }
99
100 [Fact]
101 public async Task Test_Do_Http_SetsHeadersFromResponseMeta()
102 {
103 // Arrange
104 var activity = CreateMessageActivity();
105 var response = new Response(HttpStatusCode.OK, new { hello = "world" });
106 response.Meta.Add("routes", 3);
107 response.Meta.Add("custom", "value");
108
109 EventFunction events = (plugin, name, payload, ct) =>
110 {
111 if (name == "activity") return Task.FromResult<object?>(response);
112 return Task.FromResult<object?>(null);
113 };
114
115 var plugin = CreatePlugin(new Mock<ILogger>(), events);
116 var ctx = CreateHttpContext(activity);
117
118 // Act
119 var result = await plugin.Do(ctx);
120
121 // Assert body result type
122 var jsonResult = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<object?>>(result);
123 Assert.Equal((int)HttpStatusCode.OK, jsonResult.StatusCode);
124 // Headers: routes & custom should be present
125 Assert.Contains("X-Teams-Routes", ctx.Response.Headers.Keys); // capitalized first char
126 Assert.Contains("X-Teams-Custom", ctx.Response.Headers.Keys);
127 }
128
129 [Fact]
130 public async Task Test_Do_Http_ErrorPath_ProducesProblemResult()
131 {
132 // Arrange -> throw inside events
133 EventFunction events = (plugin, name, payload, ct) =>
134 {
135 if (name == "activity") throw new InvalidOperationException("boom");
136 return Task.FromResult<object?>(null);
137 };
138
139 var logger = new Mock<ILogger>();
140 var plugin = CreatePlugin(logger, events);
141 var ctx = CreateHttpContext(CreateMessageActivity());
142
143 // Act
144 var result = await plugin.Do(ctx);
145
146 // Assert
147 var problem = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<object>>(result);
148 Assert.Equal(500, problem.StatusCode);
149 Assert.Contains("boom", problem.Value!.ToString());
150 logger.Verify(l => l.Error(It.IsAny<object[]>()), Times.AtLeastOnce);
151 }
152
153 [Fact]
154 public void Test_ExtractToken_ReturnsToken()
155 {
156 var plugin = CreatePlugin();
157 var ctx = CreateHttpContext(CreateMessageActivity(), "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ3MDI1MTUyMDB9.token123");
158
159 var token = plugin.ExtractToken(ctx.Request);
160 Assert.NotNull(token);
161 Assert.Contains("token123", token.ToString());
162 }
163
164 [Fact]
165 public async Task Test_ExtractActivity_ReturnsActivity()
166 {
167 var plugin = CreatePlugin();
168 var activity = CreateMessageActivity();
169 var ctx = CreateHttpContext(activity);
170
171 var extracted = await plugin.ParseActivity(ctx.Request);
172 Assert.NotNull(extracted);
173 Assert.True(activity.Type.Equals(extracted.Type));
174 }
175
176 [Fact]
177 public async Task Test_ExtractActivity_HttpRequestBodyAlreadyRead_ReturnsActivity()
178 {
179 var plugin = CreatePlugin();
180 var activity = CreateMessageActivity();
181 var ctx = CreateHttpContext(activity);
182 // simulate body already read by setting position to end
183 ctx.Request.Body.Position = ctx.Request.Body.Length;
184
185 var extracted = await plugin.ParseActivity(ctx.Request);
186 Assert.NotNull(extracted);
187 Assert.True(activity.Type.Equals(extracted.Type));
188 }
189
190 [Fact]
191 public async Task Test_Do_Core_ReturnsResponseAndLogs()
192 {
193 // Arrange core path tests the ActivityEvent Do(ActivityEvent)
194 var response = new Response(HttpStatusCode.OK, new { test = 1 });
195 EventFunction events = (plugin, name, payload, ct) =>
196 {
197 if (name == "activity") return Task.FromResult<object?>(response);
198 return Task.FromResult<object?>(null);
199 };
200 var logger = new Mock<ILogger>();
201 var plugin = CreatePlugin(logger, events);
202 var evt = new ActivityEvent() { Token = new JsonWebToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ3MDI1MTUyMDB9.signature"), Activity = CreateMessageActivity() };
203
204 // Act
205 var res = await plugin.Do(evt);
206
207 // Assert
208 Assert.Same(response, res);
209 logger.Verify(l => l.Debug(It.IsAny<object[]>()), Times.AtLeastOnce);
210 }
211
212 [Fact]
213 public void Test_ExtractToken_ReturnsNull_WhenNoAuthHeader()
214 {
215 // Arrange
216 var plugin = CreatePlugin();
217 var ctx = CreateHttpContextWithoutAuth(CreateMessageActivity());
218
219 // Act
220 var token = plugin.ExtractToken(ctx.Request);
221
222 // Assert
223 Assert.Null(token);
224 }
225
226 [Fact]
227 public async Task Test_Do_Http_WorksWithoutAuthHeader()
228 {
229 // Arrange - simulates skipAuth scenario where no Authorization header is present
230 var activity = CreateMessageActivity();
231 var coreResponse = new Response(HttpStatusCode.OK, new { ok = true });
232 EventFunction events = (plugin, name, payload, ct) =>
233 {
234 if (name == "activity")
235 {
236 var activityEvent = (ActivityEvent)payload;
237 // Token should be an AnonymousToken when no auth header (matches Python/TypeScript behavior)
238 Assert.NotNull(activityEvent.Token);
239 Assert.IsType<AnonymousToken>(activityEvent.Token);
240 Assert.Equal(string.Empty, activityEvent.Token.AppId);
241 return Task.FromResult<object?>(coreResponse);
242 }
243 return Task.FromResult<object?>(null);
244 };
245
246 var plugin = CreatePlugin(new Mock<ILogger>(), events);
247 var ctx = CreateHttpContextWithoutAuth(activity);
248
249 // Act
250 var result = await plugin.Do(ctx);
251
252 // Assert
253 var jsonResult = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<object?>>(result);
254 Assert.Equal(200, jsonResult.StatusCode);
255 }
256}