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 virtual async Task<IEnumerable<InvoiceData>> GetInvoices(string storeId, string orderId = null, InvoiceStatus[] status = null,
long? startDate = null,
long? endDate = null,
DateTimeOffset? startDate = null,
DateTimeOffset? endDate = null,
bool includeArchived = false,
CancellationToken token = default)
{
Dictionary<string, object> queryPayload = new Dictionary<string, object>();
queryPayload.Add(nameof(includeArchived), includeArchived);
if (startDate != null)
queryPayload.Add(nameof(startDate), startDate);
if (startDate is DateTimeOffset s)
queryPayload.Add(nameof(startDate), Utils.DateTimeToUnixTime(s));
if (endDate != null)
queryPayload.Add(nameof(endDate), endDate);
if (endDate is DateTimeOffset e)
queryPayload.Add(nameof(endDate), Utils.DateTimeToUnixTime(e));
if (orderId != null)
queryPayload.Add(nameof(orderId), orderId);

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
@@ -69,6 +70,13 @@ namespace BTCPayServer.Client
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,
Dictionary<string, object> queryPayload = null,
HttpMethod method = null)

View File

@@ -966,8 +966,8 @@ namespace BTCPayServer.Tests
//list Filtered
var invoicesFiltered = await viewOnly.GetInvoices(user.StoreId,
orderId: null, status: null, NBitcoin.Utils.DateTimeToUnixTime(DateTimeOffset.Now.AddHours(-1)),
NBitcoin.Utils.DateTimeToUnixTime(DateTimeOffset.Now.AddHours(1)));
orderId: null, status: null, DateTimeOffset.Now.AddHours(-1),
DateTimeOffset.Now.AddHours(1));
Assert.NotNull(invoicesFiltered);
Assert.Single(invoicesFiltered);
@@ -975,11 +975,24 @@ namespace BTCPayServer.Tests
//list Yesterday
var invoicesYesterday = await viewOnly.GetInvoices(user.StoreId,
orderId: null, status: null, NBitcoin.Utils.DateTimeToUnixTime(DateTimeOffset.Now.AddDays(-2)),
NBitcoin.Utils.DateTimeToUnixTime(DateTimeOffset.Now.AddDays(-1)));
orderId: null, status: null, DateTimeOffset.Now.AddDays(-2),
DateTimeOffset.Now.AddDays(-1));
Assert.NotNull(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
var invoicesExistingOrderId =
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 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>();
foreach (var error in modelState)
@@ -17,8 +22,10 @@ namespace BTCPayServer.Controllers.GreenField
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)
{
return controller.BadRequest(new GreenfieldAPIError(errorCode, errorMessage));

View File

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

View File

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