Make sure model binder error are returning error 422, use DateTimeOffsetModelBinder

This commit is contained in:
nicolas.dorier
2021-04-26 12:37:56 +09:00
parent ded55a1440
commit dcc4214dcb
7 changed files with 107 additions and 23 deletions

View File

@@ -12,19 +12,19 @@ namespace BTCPayServer.Client
public partial class BTCPayServerClient public partial class BTCPayServerClient
{ {
public virtual async Task<IEnumerable<InvoiceData>> GetInvoices(string storeId, string orderId = null, InvoiceStatus[] status = null, public virtual async Task<IEnumerable<InvoiceData>> GetInvoices(string storeId, string orderId = null, InvoiceStatus[] status = null,
long? startDate = null, DateTimeOffset? startDate = null,
long? endDate = null, DateTimeOffset? endDate = null,
bool includeArchived = false, bool includeArchived = false,
CancellationToken token = default) CancellationToken token = default)
{ {
Dictionary<string, object> queryPayload = new Dictionary<string, object>(); Dictionary<string, object> queryPayload = new Dictionary<string, object>();
queryPayload.Add(nameof(includeArchived), includeArchived); queryPayload.Add(nameof(includeArchived), includeArchived);
if (startDate != null) if (startDate is DateTimeOffset s)
queryPayload.Add(nameof(startDate), startDate); queryPayload.Add(nameof(startDate), Utils.DateTimeToUnixTime(s));
if (endDate != null) if (endDate is DateTimeOffset e)
queryPayload.Add(nameof(endDate), endDate); queryPayload.Add(nameof(endDate), Utils.DateTimeToUnixTime(e));
if (orderId != null) if (orderId != null)
queryPayload.Add(nameof(orderId), orderId); queryPayload.Add(nameof(orderId), orderId);

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -69,6 +70,13 @@ namespace BTCPayServer.Client
return JsonConvert.DeserializeObject<T>(str); return JsonConvert.DeserializeObject<T>(str);
} }
public async Task<T> SendHttpRequest<T>(string path,
Dictionary<string, object> queryPayload = null,
HttpMethod method = null, CancellationToken cancellationToken = default)
{
using var resp = await _httpClient.SendAsync(CreateHttpRequest(path, queryPayload, method), cancellationToken);
return await HandleResponse<T>(resp);
}
protected virtual HttpRequestMessage CreateHttpRequest(string path, protected virtual HttpRequestMessage CreateHttpRequest(string path,
Dictionary<string, object> queryPayload = null, Dictionary<string, object> queryPayload = null,
HttpMethod method = null) HttpMethod method = null)

View File

@@ -966,8 +966,8 @@ namespace BTCPayServer.Tests
//list Filtered //list Filtered
var invoicesFiltered = await viewOnly.GetInvoices(user.StoreId, var invoicesFiltered = await viewOnly.GetInvoices(user.StoreId,
orderId: null, status: null, NBitcoin.Utils.DateTimeToUnixTime(DateTimeOffset.Now.AddHours(-1)), orderId: null, status: null, DateTimeOffset.Now.AddHours(-1),
NBitcoin.Utils.DateTimeToUnixTime(DateTimeOffset.Now.AddHours(1))); DateTimeOffset.Now.AddHours(1));
Assert.NotNull(invoicesFiltered); Assert.NotNull(invoicesFiltered);
Assert.Single(invoicesFiltered); Assert.Single(invoicesFiltered);
@@ -975,11 +975,24 @@ namespace BTCPayServer.Tests
//list Yesterday //list Yesterday
var invoicesYesterday = await viewOnly.GetInvoices(user.StoreId, var invoicesYesterday = await viewOnly.GetInvoices(user.StoreId,
orderId: null, status: null, NBitcoin.Utils.DateTimeToUnixTime(DateTimeOffset.Now.AddDays(-2)), orderId: null, status: null, DateTimeOffset.Now.AddDays(-2),
NBitcoin.Utils.DateTimeToUnixTime(DateTimeOffset.Now.AddDays(-1))); DateTimeOffset.Now.AddDays(-1));
Assert.NotNull(invoicesYesterday); Assert.NotNull(invoicesYesterday);
Assert.Empty(invoicesYesterday); Assert.Empty(invoicesYesterday);
// Error, startDate and endDate inverted
await AssertValidationError(new[] { "startDate", "endDate" },
() => viewOnly.GetInvoices(user.StoreId,
orderId: null, status: null, DateTimeOffset.Now.AddDays(-1),
DateTimeOffset.Now.AddDays(-2)));
await AssertValidationError(new[] { "startDate" },
() => viewOnly.SendHttpRequest<Client.Models.InvoiceData[]>($"api/v1/stores/{user.StoreId}/invoices", new Dictionary<string, object>()
{
{ "startDate", "blah" }
}));
//list Existing OrderId //list Existing OrderId
var invoicesExistingOrderId = var invoicesExistingOrderId =
await viewOnly.GetInvoices(user.StoreId, orderId: newInvoice.Metadata["orderId"].ToString()); await viewOnly.GetInvoices(user.StoreId, orderId: newInvoice.Metadata["orderId"].ToString());

View File

@@ -8,6 +8,11 @@ namespace BTCPayServer.Controllers.GreenField
public static class GreenFieldUtils public static class GreenFieldUtils
{ {
public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState) public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState)
{
return controller.UnprocessableEntity(modelState.ToGreenfieldValidationError());
}
public static List<GreenfieldValidationError> ToGreenfieldValidationError(this ModelStateDictionary modelState)
{ {
List<GreenfieldValidationError> errors = new List<GreenfieldValidationError>(); List<GreenfieldValidationError> errors = new List<GreenfieldValidationError>();
foreach (var error in modelState) foreach (var error in modelState)
@@ -17,8 +22,10 @@ namespace BTCPayServer.Controllers.GreenField
errors.Add(new GreenfieldValidationError(error.Key, errorMessage.ErrorMessage)); errors.Add(new GreenfieldValidationError(error.Key, errorMessage.ErrorMessage));
} }
} }
return controller.UnprocessableEntity(errors.ToArray());
return errors;
} }
public static IActionResult CreateAPIError(this ControllerBase controller, string errorCode, string errorMessage) public static IActionResult CreateAPIError(this ControllerBase controller, string errorCode, string errorMessage)
{ {
return controller.BadRequest(new GreenfieldAPIError(errorCode, errorMessage)); return controller.BadRequest(new GreenfieldAPIError(errorCode, errorMessage));

View File

@@ -49,25 +49,35 @@ namespace BTCPayServer.Controllers.GreenField
AuthenticationSchemes = AuthenticationSchemes.Greenfield)] AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/invoices")] [HttpGet("~/api/v1/stores/{storeId}/invoices")]
public async Task<IActionResult> GetInvoices(string storeId, [FromQuery] string[] orderId = null, [FromQuery] string[] status = null, public async Task<IActionResult> GetInvoices(string storeId, [FromQuery] string[] orderId = null, [FromQuery] string[] status = null,
[FromQuery] long? startDate = null, [FromQuery]
[FromQuery] long? endDate = null, [FromQuery] bool includeArchived = false) [ModelBinder(typeof(ModelBinders.DateTimeOffsetModelBinder))]
DateTimeOffset? startDate = null,
[FromQuery]
[ModelBinder(typeof(ModelBinders.DateTimeOffsetModelBinder))]
DateTimeOffset? endDate = null, [FromQuery] bool includeArchived = false)
{ {
var store = HttpContext.GetStoreData(); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
{ {
return StoreNotFound(); return StoreNotFound();
} }
if (startDate is DateTimeOffset s &&
endDate is DateTimeOffset e &&
s > e)
{
this.ModelState.AddModelError(nameof(startDate), "startDate should not be above endDate");
this.ModelState.AddModelError(nameof(endDate), "endDate should not be below startDate");
}
DateTimeOffset startDateTimeOffset = Utils.UnixTimeToDateTime(startDate.GetValueOrDefault(DateTimeOffset.MinValue.ToUnixTimeSeconds())); if (!ModelState.IsValid)
DateTimeOffset endDateTimeOffset = Utils.UnixTimeToDateTime(endDate.GetValueOrDefault(DateTimeOffset.MaxValue.ToUnixTimeSeconds())); return this.CreateValidationError(ModelState);
var invoices = var invoices =
await _invoiceRepository.GetInvoices(new InvoiceQuery() await _invoiceRepository.GetInvoices(new InvoiceQuery()
{ {
StoreId = new[] {store.Id}, StoreId = new[] {store.Id},
IncludeArchived = includeArchived, IncludeArchived = includeArchived,
StartDate = startDateTimeOffset, StartDate = startDate,
EndDate = endDateTimeOffset, EndDate = endDate,
OrderId = orderId, OrderId = orderId,
Status = status Status = status
}); });

View File

@@ -30,6 +30,8 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using NBitcoin; using NBitcoin;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Controllers.GreenField;
namespace BTCPayServer.Hosting namespace BTCPayServer.Hosting
{ {
@@ -122,11 +124,9 @@ namespace BTCPayServer.Hosting
}) })
.ConfigureApiBehaviorOptions(options => .ConfigureApiBehaviorOptions(options =>
{ {
var builtInFactory = options.InvalidModelStateResponseFactory;
options.InvalidModelStateResponseFactory = context => options.InvalidModelStateResponseFactory = context =>
{ {
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.UnprocessableEntity; return new UnprocessableEntityObjectResult(context.ModelState.ToGreenfieldValidationError());
return builtInFactory(context);
}; };
}) })
.AddRazorOptions(o => .AddRazorOptions(o =>

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.VisualBasic.CompilerServices;
namespace BTCPayServer.ModelBinders
{
public class DateTimeOffsetModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (!typeof(DateTimeOffset).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType) &&
!typeof(DateTimeOffset?).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType))
{
return Task.CompletedTask;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (val == null)
{
return Task.CompletedTask;
}
string v = val.FirstValue as string;
if (v == null)
{
return Task.CompletedTask;
}
try
{
var sec = long.Parse(v, CultureInfo.InvariantCulture);
bindingContext.Result = ModelBindingResult.Success(NBitcoin.Utils.UnixTimeToDateTime(sec));
}
catch
{
bindingContext.Result = ModelBindingResult.Failed();
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Invalid unix timestamp");
}
return Task.CompletedTask;
}
}
}