8000 Merge pull request #105 from twilio-labs/releases/v7 · twilio-labs/twilio-aspnet@9dc8ef0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9dc8ef0

Browse files
authored
Merge pull request #105 from twilio-labs/releases/v7
Merge v7 changes into main
2 parents fabd42b + ecf078d commit 9dc8ef0

35 files changed

+424
-473
lines changed

README.md

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,46 @@ app.MapPost("/sms", () => ...)
374374
.AddEndpointFilter<ValidateTwilioRequestFilter>();
375375
```
376376

377+
##### ASP.NET Core Middleware
378+
When you can't use the `[ValidateRequest]` filter or `ValidateTwilioRequestFilter`, you can use the `ValidateTwilioRequestMiddleware` instead.
379+
You can add add the `ValidateTwilioRequestFilter` like this:
380+
381+
```csharp
382+
app.UseTwilioRequestValidation();
383+
// or the equivalent: app.UseMiddleware<ValidateTwilioRequestMiddleware>();
384+
```
385+
386+
This middleware will perform the validation for all requests.
387+
If you don't want to apply the validation to all requests, you can use `app.UseWhen()` to run the middleware conditionally.
388+
389+
Here's an example of how to validate requests that start with path _/twilio-media_, as to protect media files that only the Twilio Proxy should be able to access:
390+
391+
```csharp
392+
using System.Net;
393+
using Microsoft.Extensions.FileProviders;
394+
using Microsoft.Extensions.Options;
395+
using Twilio.AspNet.Core;
396+
397+
var builder = WebApplication.CreateBuilder(args);
398+
399+
builder.Services.AddTwilioRequestValidation();
400+
401+
var app = builder.Build();
402+
403+
app.UseWhen(
404+
context => context.Request.Path.StartsWithSegments("/twilio-media", StringComparison.OrdinalIgnoreCase),
405+
app => app.UseTwilioRequestValidation()
406+
);
407+
408+
app.UseStaticFiles(new StaticFileOptions
409+
{
410+
FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "TwilioMedia")),
411+
RequestPath = "/twilio-media"
412+
});
413+
414+
app.Run();
415+
```
416+
377417
#### Validate requests in ASP.NET MVC on .NET Framework
378418

379419
In your _Web.config_ you can configure request validation like shown below:
@@ -441,7 +481,7 @@ public class SmsController : TwilioController
441481
}
442482
```
443483

444-
#### Validate requests outside of MVC
484+
#### Validate requests using the RequestValidationHelper
445485

446486
The `[ValidateRequest]` attribute only works for MVC. If you need to validate requests outside of MVC, you can use the `RequestValidationHelper` class provided by `Twilio.AspNet`.
447487
Alternatively, the `RequestValidator` class from the [Twilio SDK](https://github.com/twilio/twilio-csharp) can also help you with this.
@@ -487,8 +527,7 @@ bool IsValidTwilioRequest(HttpContext httpContext)
487527
urlOverride = $"{options.BaseUrlOverride.TrimEnd('/')}{request.Path}{request.QueryString}";
488528
}
489529

490-
var validator = new RequestValidationHelper();
491-
return validator.IsValidRequest(httpContext, options.AuthToken, urlOverride, options.AllowLocal ?? true);
530+
return RequestValidationHelper.IsValidRequest(httpContext, options.AuthToken, urlOverride, options.AllowLocal ?? true);
492531
}
493532
```
494533

src/Twilio.AspNet.Common/SmsRequest.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ namespace Twilio.AspNet.Common
33
/// <summary>
44
/// This class can be used as the parameter on your SMS action. Incoming parameters will be bound here.
55
/// </summary>
6-
/// <remarks>http://www.twilio.com/docs/api/twiml/sms/twilio_request</remarks>
6+
/// <remarks>https://www.twilio.com/docs/messaging/guides/webhook-request</remarks>
77
public class SmsRequest : TwilioRequest
88
{
99
/// <summary>
10-
/// A 34 character unique identifier for the message. May be used to later retrieve this message from the REST API
10+
/// A 34 character unique identifier for the message. May be used to later retrieve this message from the REST API.
11+
/// </summary>
12+
public string MessageSid { get; set; }
13+
14+
/// <summary>
15+
/// Same value as MessageSid. Deprecated and included for backward compatibility.
1116
/// </summary>
1217
public string SmsSid { get; set; }
1318

@@ -30,6 +35,15 @@ public class SmsRequest : TwilioRequest
3035
/// A unique identifier of the messaging service
3136
/// </summary>
3237
public string MessagingServiceSid { get; set; }
33-
38+
39+
/// <summary>
40+
/// The number of media items associated with your message
41+
/// </summary>
42+
public int NumMedia { get; set; }
43+
44+
/// <summary>
45+
/// The number of media items associated with a "Click to WhatsApp" advertisement.
46+
/// </summary>
47+
public int ReferralNumMedia { get; set; }
3448
}
3549
}

src/Twilio.AspNet.Common/SmsStatusCallbackRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public class SmsStatusCallbackRequest: SmsRequest
77
/// status is failed or undelivered, the ErrorCode can give you more information
88
/// about the failure. If the message was delivered successfully, no ErrorCode
99
/// will be present. Find the possible values here:
10-
/// https://www.twilio.com/docs/sms/api/message#delivery-related-errors
10+
/// https://www.twilio.com/docs/sms/api/message-resource#delivery-related-errors
1111
/// </summary>
1212
public string ErrorCode { get; set; }
1313

src/Twilio.AspNet.Common/StatusCallbackRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/// <summary>
44
/// This class can be used as the 179B parameter on your StatusCallback action. Incoming parameters will be bound here.
55
/// </summary>
6-
/// <remarks>http://www.twilio.com/docs/api/twiml/twilio_request#asynchronous</remarks>
6+
/// <remarks>https://www.twilio.com/docs/voice/twiml#ending-the-call-callback-requests</remarks>
77
public class StatusCallbackRequest : VoiceRequest
88
{
99
/// <summary>

src/Twilio.AspNet.Common/TwilioRequest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ public abstract class TwilioRequest
2626
/// </remarks>
2727
public string To { get; set; }
2828

29-
3029
/// <summary>
3130
/// The city of the caller
3231
/// </summary>

src/Twilio.AspNet.Common/VoiceRequest.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/// <summary>
44
/// This class can be used as the parameter on your voice action. Incoming parameters will be bound here.
55
/// </summary>
6-
/// <remarks>http://www.twilio.com/docs/api/twiml/twilio_request</remarks>
6+
/// <remarks>https://www.twilio.com/docs/usage/webhooks/voice-webhooks</remarks>
77
public class VoiceRequest : TwilioRequest
88
{
99
/// <summary>
@@ -35,7 +35,15 @@ public class VoiceRequest : TwilioRequest
3535
/// This parameter is set when the IncomingPhoneNumber that received the call has had its VoiceCallerIdLookup value set to true.
3636
/// </summary>
3737
public string CallerName { get; set; }
38-
38+
39+
/// <summary>
40+
/// A unique identifier for the call that created this leg. This parameter is not passed if this is the first leg of a call.
41+
/// </summary>
42+
public string ParentCallSid { get; set; }
43+
44+
/// <summary>A token string needed to invoke a forwarded call.</summary>
45+
public string CallToken { get; set; }
46+
3947
#region Gather & Record Parameters
4048

4149
/// <summary>

src/Twilio.AspNet.Core.UnitTests/MinimalApiTwiMLResultTests.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
using System.IO;
22
using System.Threading.Tasks;
33
using Microsoft.AspNetCore.Http;
4-
using Twilio.AspNet.Core.MinimalApi;
54
using Twilio.TwiML;
65
using Xunit;
76

87
namespace Twilio.AspNet.Core.UnitTests;
98

10-
// ReSharper disable once InconsistentNaming
119
public class MinimalApiTwiMLResultTests
1210
{
1311
[Fact]
@@ -32,6 +30,6 @@ private static async Task ValidateTwimlResultWritesToResponseBody(TwiML.TwiML tw
3230
Assert.Equal(twiMlResponse.ToString(), responseBody);
3331
}
3432

35-
private static VoiceResponse GetVoiceResponse() => new VoiceResponse().Say("Hello World");
36-
private static MessagingResponse GetMessagingResponse() => new MessagingResponse().Message("Hello World");
33+
private static VoiceResponse GetVoiceResponse() => new VoiceResponse().Say("Ahoy!");
34+
private static MessagingResponse GetMessagingResponse() => new MessagingResponse().Message("Ahoy!");
3735
}

src/Twilio.AspNet.Core.UnitTests/RequestValidationHelperTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ public class ContextMocks
1515
public Moq.Mock<HttpContext> HttpContext { get; set; }
1616
public Moq.Mock<HttpRequest> Request { get; set; }
1717

18-
public ContextMocks(bool isLocal, FormCollection? form = null, bool isProxied = false) : this("", isLocal, form, isProxied)
18+
public ContextMocks(bool isLocal, FormCollection form = null, bool isProxied = false) : this("", isLocal, form, isProxied)
1919
{
2020
}
2121

22-
public ContextMocks(string urlOverride, bool isLocal, FormCollection? form = null, bool isProxied = false)
22+
public ContextMocks(string urlOverride, bool isLocal, FormCollection form = null, bool isProxied = false)
2323
{
2424
var headers = new HeaderDictionary();
2525
headers.Add("X-Twilio-Signature", CalculateSignature(urlOverride, form));
@@ -55,7 +55,7 @@ public ContextMocks(string urlOverride, bool isLocal, FormCollection? form = nul
5555
public static string fakeUrl = "https://api.example.com/webhook";
5656
public static string fakeAuthToken = "thisisafakeauthtoken";
5757

58-
private string CalculateSignature(string urlOverride, FormCollection? form)
58+
private string CalculateSignature(string urlOverride, FormCollection form)
5959
{
6060
var value = new StringBuilder();
6161
value.Append(string.IsNullOrEmpty(urlOverride) ? ContextMocks.fakeUrl : urlOverride);

src/Twilio.AspNet.Core.UnitTests/Twilio.AspNet.Core.UnitTests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFramework>net7.0</TargetFramework>
4-
<Nullable>enable</Nullable>
54
<IsPackable>false</IsPackable>
5+
<Nullable>disable</Nullable>
66
</PropertyGroup>
77
<ItemGroup>
88
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />

src/Twilio.AspNet.Core.UnitTests/TwilioClientTests.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,10 @@ public void AddTwilioClient_With_ApiKeyOptions_Should_Match_Properties()
213213
Assert.Equal(ValidTwilioOptions.Client.AccountSid, client.AccountSid);
214214
Assert.Equal(ValidTwilioOptions.Client.LogLevel, client.LogLevel);
215215
Assert.Equal(ValidTwilioOptions.Client.ApiKeySid,
216-
typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance)
216+
typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance)!
217217
.GetValue(client));
218218
Assert.Equal(ValidTwilioOptions.Client.ApiKeySecret,
219-
typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance)
219+
typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance)!
220220
.GetValue(client));
221221
}
222222

@@ -236,10 +236,10 @@ public void AddTwilioClient_With_AuthTokenOptions_Should_Match_Properties()
236236
Assert.Equal(ValidTwilioOptions.Client.AccountSid, client.AccountSid);
237237
Assert.Equal(ValidTwilioOptions.Client.LogLevel, client.LogLevel);
238238
Assert.Equal(ValidTwilioOptions.Client.AccountSid,
239-
typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance)
239+
typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance)!
240240
.GetValue(client));
241241
Assert.Equal(ValidTwilioOptions.Client.AuthToken,
242-
typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance)
242+
typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance)!
243243
.GetValue(client));
244244
}
245245
[Fact]
@@ -257,7 +257,7 @@ public void AddTwilioClient_Without_HttpClientProvider_Should_Named_HttpClient()
257257
var twilioRestClient = scope.ServiceProvider.GetService<TwilioRestClient>();
258258

259259
var actualHttpClient = (System.Net.Http.HttpClient) typeof(SystemNetHttpClient)
260-
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance)
260+
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance)!
261261
.GetValue(twilioRestClient.HttpClient);
262262

263263
Assert.NotNull(actualHttpClient);
@@ -271,14 +271,15 @@ public void AddTwilioClient_With_HttpClientProvider_Should_Use_HttpClient()
271271
serviceCollection.AddSingleton(BuildValidConfiguration());
272272

273273
using var httpClient = new System.Net.Http.HttpClient();
274+
// ReSharper disable once AccessToDisposedClosure
274275
serviceCollection.AddTwilioClient(_ => httpClient);
275276

276277
var serviceProvider = serviceCollection.BuildServiceProvider();
277278
using var scope = serviceProvider.CreateScope();
278279

279280
var twilioRestClient = scope.ServiceProvider.GetService<TwilioRestClient>();
280281
var httpClientFromTwilioClient = (System.Net.Http.HttpClient) typeof(SystemNetHttpClient)
281-
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance)
282+
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance)!
282283
.GetValue(twilioRestClient.HttpClient);
283284

284285
Assert.Equal(httpClient, httpClientFromTwilioClient);

src/Twilio.AspNet.Core.UnitTests/TwilioControllerExtensionTests.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.IO;
22
using System.Threading.Tasks;
3+
using System.Xml.Linq;
34
using Microsoft.AspNetCore.Http;
45
using Microsoft.AspNetCore.Mvc;
56
using Microsoft.AspNetCore.Mvc.Abstractions;
@@ -17,8 +18,8 @@ public class TwilioControllerExtensionTests
1718
[Fact]
1819
public async Task TwimlResult_Should_Write_VoiceResponse_To_ResponseBody()
1920
{
20-
var twiml = new VoiceResponse().Say("Hello World");
21-
var result = TwilioControllerExtensions.TwiML(Mock.Of<ControllerBase>(), twiml);
21+
var twiml = new VoiceResponse().Say("Ahoy!");
22+
var result = Mock.Of<ControllerBase>().TwiML(twiml);
2223
var actionContext = CreateActionContext();
2324
await result.ExecuteResultAsync(actionContext);
2425

@@ -27,12 +28,26 @@ public async Task TwimlResult_Should_Write_VoiceResponse_To_ResponseBody()
2728
var responseBody = await reader.ReadToEndAsync();
2829
Assert.Equal(twiml.ToString(), responseBody);
2930
}
31+
32+
[Fact]
33+
public async Task TwimlResult_Should_Write_VoiceResponse_To_ResponseBody_Unformatted()
34+
{
35+
var twiml = new VoiceResponse().Say("Ahoy!");
36+
var result = Mock.Of<ControllerBase>().TwiML(twiml, SaveOptions.DisableFormatting);
37+
var actionContext = CreateActionContext();
38+
await result.ExecuteResultAsync(actionContext);
39+
40+
actionContext.HttpContext.Response.Body.Seek(0, SeekOrigin.Begin);
41+
var reader = new StreamReader(actionContext.HttpContext.Response.Body);
42+
var responseBody = await reader.ReadToEndAsync();
43+
Assert.Equal(twiml.ToString(SaveOptions.DisableFormatting), responseBody);
44+
}
3045

3146
[Fact]
3247
public async Task TwimlResult_Should_Write_MessagingResponse_To_ResponseBody()
3348
{
34-
var twiml = new MessagingResponse().Message("Hello World");
35-
var result = TwilioControllerExtensions.TwiML(Mock.Of<ControllerBase>(), twiml);
49+
var twiml = new MessagingResponse().Message("Ahoy!");
50+
var result = Mock.Of<ControllerBase>().TwiML(twiml);
3651
var actionContext = CreateActionContext();
3752
await result.ExecuteResultAsync(actionContext);
3853

src/Twilio.AspNet.Core.UnitTests/ValidateRequestAttributeTests.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
using System.Collections.Generic;
33
using System.Net;
44
using System.Text.Json;
5-
using System.Threading.Tasks;
65
using Microsoft.AspNetCore.Http;
7-
using Microsoft.AspNetCore.Http.HttpResults;
86
using Microsoft.AspNetCore.Mvc;
97
using Microsoft.AspNetCore.Mvc.Abstractions;
108
using Microsoft.AspNetCore.Mvc.Filters;
@@ -128,13 +126,13 @@ public void ValidateRequestAttribute_Validates_Request_Successfully()
128126
var actionExecutingContext = new ActionExecutingContext(
129127
new ActionContext(fakeContext, new RouteData(), new ActionDescriptor()),
130128
new List<IFilterMetadata>(),
131-
new Dictionary<string, object?>(),
129+
new Dictionary<string, object>(),
132130
new object()
133131
);
134132

135133
attribute.OnActionExecuting(actionExecutingContext);
136134

137-
Assert.Equal(null, actionExecutingContext.Result);
135+
Assert.Null(actionExecutingContext.Result);
138136
}
139137

140138
[Fact]
@@ -156,13 +154,13 @@ public void ValidateRequestFilter_Validates_Request_Forbid()
156154
var actionExecutingContext = new ActionExecutingContext(
157155
new ActionContext(fakeContext, new RouteData(), new ActionDescriptor()),
158156
new List<IFilterMetadata>(),
159-
new Dictionary<string, object?>(),
157+
new Dictionary<string, object>(),
160158
new object()
161159
);
162160

163161
attribute.OnActionExecuting(actionExecutingContext);
164162

165-
var statusCodeResult = (HttpStatusCodeResult)actionExecutingContext.Result!;
163+
var statusCodeResult = (StatusCodeResult)actionExecutingContext.Result!;
166164
Assert.NotNull(statusCodeResult);
167165
Assert.Equal((int)HttpStatusCode.Forbidden, statusCodeResult.StatusCode);
168166
}

src/Twilio.AspNet.Core.UnitTests/ValidateTwilioRequestFilterTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public async Task ValidateRequestFilter_Validates_Request_Successfully()
7575

7676
var result = await filter.InvokeAsync(
7777
new DefaultEndpointFilterInvocationContext(fakeContext),
78-
_ => ValueTask.FromResult<object?>(Results.Ok())
78+
_ => ValueTask.FromResult<object>(Results.Ok())
7979
);
8080

8181
Assert.IsType<Ok>(result);
@@ -103,7 +103,7 @@ public async Task ValidateRequestFilter_Validates_Request_Forbid()
103103

104104
var result = await filter.InvokeAsync(
105105
new DefaultEndpointFilterInvocationContext(fakeContext),
106-
_ => ValueTask.FromResult<object?>(Results.Ok())
106+
_ => ValueTask.FromResult<object>(Results.Ok())
107107
);
108108

109109
var statusCodeResult = (StatusCodeHttpResult)result!;

0 commit comments

Comments
 (0)
0