Merge pull request #1884 from szukuro/dev
Adding various language enhancements, removing unused/duplicated extension
This commit is contained in:
commit
59805331cd
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
operation.Security = new List<OpenApiSecurityRequirement>
|
operation.Security = new List<OpenApiSecurityRequirement>
|
||||||
{
|
{
|
||||||
new OpenApiSecurityRequirement
|
new()
|
||||||
{
|
{
|
||||||
[ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator" }
|
[ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator" }
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
operation.Security = new List<OpenApiSecurityRequirement>
|
operation.Security = new List<OpenApiSecurityRequirement>
|
||||||
{
|
{
|
||||||
new OpenApiSecurityRequirement
|
new()
|
||||||
{
|
{
|
||||||
[ oAuthScheme ] = new[] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" }
|
[ oAuthScheme ] = new[] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" }
|
||||||
}
|
}
|
||||||
|
@ -34,17 +34,15 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
|
|||||||
_persistentConnection.TryConnect();
|
_persistentConnection.TryConnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var channel = _persistentConnection.CreateModel())
|
using var channel = _persistentConnection.CreateModel();
|
||||||
{
|
channel.QueueUnbind(queue: _queueName,
|
||||||
channel.QueueUnbind(queue: _queueName,
|
exchange: BROKER_NAME,
|
||||||
exchange: BROKER_NAME,
|
routingKey: eventName);
|
||||||
routingKey: eventName);
|
|
||||||
|
|
||||||
if (_subsManager.IsEmpty)
|
if (_subsManager.IsEmpty)
|
||||||
{
|
{
|
||||||
_queueName = string.Empty;
|
_queueName = string.Empty;
|
||||||
_consumerChannel.Close();
|
_consumerChannel.Close();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,32 +64,30 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
|
|||||||
|
|
||||||
_logger.LogTrace("Creating RabbitMQ channel to publish event: {EventId} ({EventName})", @event.Id, eventName);
|
_logger.LogTrace("Creating RabbitMQ channel to publish event: {EventId} ({EventName})", @event.Id, eventName);
|
||||||
|
|
||||||
using (var channel = _persistentConnection.CreateModel())
|
using var channel = _persistentConnection.CreateModel();
|
||||||
|
_logger.LogTrace("Declaring RabbitMQ exchange to publish event: {EventId}", @event.Id);
|
||||||
|
|
||||||
|
channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct");
|
||||||
|
|
||||||
|
var body = JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Declaring RabbitMQ exchange to publish event: {EventId}", @event.Id);
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
|
||||||
channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct");
|
policy.Execute(() =>
|
||||||
|
{
|
||||||
var body = JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), new JsonSerializerOptions
|
var properties = channel.CreateBasicProperties();
|
||||||
{
|
properties.DeliveryMode = 2; // persistent
|
||||||
WriteIndented = true
|
|
||||||
});
|
|
||||||
|
|
||||||
policy.Execute(() =>
|
|
||||||
{
|
|
||||||
var properties = channel.CreateBasicProperties();
|
|
||||||
properties.DeliveryMode = 2; // persistent
|
|
||||||
|
|
||||||
_logger.LogTrace("Publishing event to RabbitMQ: {EventId}", @event.Id);
|
_logger.LogTrace("Publishing event to RabbitMQ: {EventId}", @event.Id);
|
||||||
|
|
||||||
channel.BasicPublish(
|
channel.BasicPublish(
|
||||||
exchange: BROKER_NAME,
|
exchange: BROKER_NAME,
|
||||||
routingKey: eventName,
|
routingKey: eventName,
|
||||||
mandatory: true,
|
mandatory: true,
|
||||||
basicProperties: properties,
|
basicProperties: properties,
|
||||||
body: body);
|
body: body);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SubscribeDynamic<TH>(string eventName)
|
public void SubscribeDynamic<TH>(string eventName)
|
||||||
@ -244,30 +240,27 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
|
|||||||
|
|
||||||
if (_subsManager.HasSubscriptionsForEvent(eventName))
|
if (_subsManager.HasSubscriptionsForEvent(eventName))
|
||||||
{
|
{
|
||||||
using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME))
|
using var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME);
|
||||||
|
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
|
||||||
|
foreach (var subscription in subscriptions)
|
||||||
{
|
{
|
||||||
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
|
if (subscription.IsDynamic)
|
||||||
foreach (var subscription in subscriptions)
|
|
||||||
{
|
{
|
||||||
if (subscription.IsDynamic)
|
if (scope.ResolveOptional(subscription.HandlerType) is not IDynamicIntegrationEventHandler handler) continue;
|
||||||
{
|
using dynamic eventData = JsonDocument.Parse(message);
|
||||||
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler;
|
await Task.Yield();
|
||||||
if (handler == null) continue;
|
await handler.Handle(eventData);
|
||||||
using dynamic eventData = JsonDocument.Parse(message);
|
}
|
||||||
await Task.Yield();
|
else
|
||||||
await handler.Handle(eventData);
|
{
|
||||||
}
|
var handler = scope.ResolveOptional(subscription.HandlerType);
|
||||||
else
|
if (handler == null) continue;
|
||||||
{
|
var eventType = _subsManager.GetEventTypeByName(eventName);
|
||||||
var handler = scope.ResolveOptional(subscription.HandlerType);
|
var integrationEvent = JsonSerializer.Deserialize(message, eventType, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
|
||||||
if (handler == null) continue;
|
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
|
||||||
var eventType = _subsManager.GetEventTypeByName(eventName);
|
|
||||||
var integrationEvent = JsonSerializer.Deserialize(message, eventType, new JsonSerializerOptions() { PropertyNameCaseInsensitive= true});
|
|
||||||
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
|
|
||||||
|
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
|
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,32 +155,29 @@ public class EventBusServiceBus : IEventBus, IDisposable
|
|||||||
var processed = false;
|
var processed = false;
|
||||||
if (_subsManager.HasSubscriptionsForEvent(eventName))
|
if (_subsManager.HasSubscriptionsForEvent(eventName))
|
||||||
{
|
{
|
||||||
using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME))
|
var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME);
|
||||||
|
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
|
||||||
|
foreach (var subscription in subscriptions)
|
||||||
{
|
{
|
||||||
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
|
if (subscription.IsDynamic)
|
||||||
foreach (var subscription in subscriptions)
|
|
||||||
{
|
{
|
||||||
if (subscription.IsDynamic)
|
if (scope.ResolveOptional(subscription.HandlerType) is not IDynamicIntegrationEventHandler handler) continue;
|
||||||
{
|
|
||||||
var handler = scope.ResolveOptional(subscription.HandlerType) as IDynamicIntegrationEventHandler;
|
using dynamic eventData = JsonDocument.Parse(message);
|
||||||
if (handler == null) continue;
|
await handler.Handle(eventData);
|
||||||
|
}
|
||||||
using dynamic eventData = JsonDocument.Parse(message);
|
else
|
||||||
await handler.Handle(eventData);
|
{
|
||||||
}
|
var handler = scope.ResolveOptional(subscription.HandlerType);
|
||||||
else
|
if (handler == null) continue;
|
||||||
{
|
var eventType = _subsManager.GetEventTypeByName(eventName);
|
||||||
var handler = scope.ResolveOptional(subscription.HandlerType);
|
var integrationEvent = JsonSerializer.Deserialize(message, eventType);
|
||||||
if (handler == null) continue;
|
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
|
||||||
var eventType = _subsManager.GetEventTypeByName(eventName);
|
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
|
||||||
var integrationEvent = JsonSerializer.Deserialize(message, eventType);
|
|
||||||
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
|
|
||||||
await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
processed = true;
|
|
||||||
}
|
}
|
||||||
|
processed = true;
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,11 +15,9 @@ public class ResilientTransaction
|
|||||||
var strategy = _context.Database.CreateExecutionStrategy();
|
var strategy = _context.Database.CreateExecutionStrategy();
|
||||||
await strategy.ExecuteAsync(async () =>
|
await strategy.ExecuteAsync(async () =>
|
||||||
{
|
{
|
||||||
using (var transaction = _context.Database.BeginTransaction())
|
using var transaction = _context.Database.BeginTransaction();
|
||||||
{
|
await action();
|
||||||
await action();
|
transaction.Commit();
|
||||||
transaction.Commit();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,48 +21,46 @@ namespace Microsoft.AspNetCore.Hosting
|
|||||||
{
|
{
|
||||||
var underK8s = webHost.IsInKubernetes();
|
var underK8s = webHost.IsInKubernetes();
|
||||||
|
|
||||||
using (var scope = webHost.Services.CreateScope())
|
using var scope = webHost.Services.CreateScope();
|
||||||
|
var services = scope.ServiceProvider;
|
||||||
|
var logger = services.GetRequiredService<ILogger<TContext>>();
|
||||||
|
var context = services.GetService<TContext>();
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var services = scope.ServiceProvider;
|
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
|
||||||
var logger = services.GetRequiredService<ILogger<TContext>>();
|
|
||||||
var context = services.GetService<TContext>();
|
|
||||||
|
|
||||||
try
|
if (underK8s)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
|
InvokeSeeder(seeder, context, services);
|
||||||
|
|
||||||
if (underK8s)
|
|
||||||
{
|
|
||||||
InvokeSeeder(seeder, context, services);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var retries = 10;
|
|
||||||
var retry = Policy.Handle<SqlException>()
|
|
||||||
.WaitAndRetry(
|
|
||||||
retryCount: retries,
|
|
||||||
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
|
|
||||||
onRetry: (exception, timeSpan, retry, ctx) =>
|
|
||||||
{
|
|
||||||
logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", nameof(TContext), exception.GetType().Name, exception.Message, retry, retries);
|
|
||||||
});
|
|
||||||
|
|
||||||
//if the sql server container is not created on run docker compose this
|
|
||||||
//migration can't fail for network related exception. The retry options for DbContext only
|
|
||||||
//apply to transient exceptions
|
|
||||||
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
|
|
||||||
retry.Execute(() => InvokeSeeder(seeder, context, services));
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
else
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
|
var retries = 10;
|
||||||
if (underK8s)
|
var retry = Policy.Handle<SqlException>()
|
||||||
{
|
.WaitAndRetry(
|
||||||
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
|
retryCount: retries,
|
||||||
}
|
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
|
||||||
|
onRetry: (exception, timeSpan, retry, ctx) =>
|
||||||
|
{
|
||||||
|
logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", nameof(TContext), exception.GetType().Name, exception.Message, retry, retries);
|
||||||
|
});
|
||||||
|
|
||||||
|
//if the sql server container is not created on run docker compose this
|
||||||
|
//migration can't fail for network related exception. The retry options for DbContext only
|
||||||
|
//apply to transient exceptions
|
||||||
|
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
|
||||||
|
retry.Execute(() => InvokeSeeder(seeder, context, services));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
|
||||||
|
if (underK8s)
|
||||||
|
{
|
||||||
|
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ public class AuthorizeCheckOperationFilter : IOperationFilter
|
|||||||
|
|
||||||
operation.Security = new List<OpenApiSecurityRequirement>
|
operation.Security = new List<OpenApiSecurityRequirement>
|
||||||
{
|
{
|
||||||
new OpenApiSecurityRequirement
|
new()
|
||||||
{
|
{
|
||||||
[ oAuthScheme ] = new [] { "basketapi" }
|
[ oAuthScheme ] = new [] { "basketapi" }
|
||||||
}
|
}
|
||||||
|
@ -6,45 +6,39 @@ public class BasketScenarios
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Post_basket_and_response_ok_status_code()
|
public async Task Post_basket_and_response_ok_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
|
||||||
var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.PostAsync(Post.Basket, content);
|
||||||
.PostAsync(Post.Basket, content);
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_basket_and_response_ok_status_code()
|
public async Task Get_basket_and_response_ok_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.GetAsync(Get.GetBasket(1));
|
||||||
.GetAsync(Get.GetBasket(1));
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Send_Checkout_basket_and_response_ok_status_code()
|
public async Task Send_Checkout_basket_and_response_ok_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
|
||||||
var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
|
|
||||||
|
|
||||||
await server.CreateClient()
|
await server.CreateClient()
|
||||||
.PostAsync(Post.Basket, contentBasket);
|
.PostAsync(Post.Basket, contentBasket);
|
||||||
|
|
||||||
var contentCheckout = new StringContent(BuildCheckout(), UTF8Encoding.UTF8, "application/json");
|
var contentCheckout = new StringContent(BuildCheckout(), UTF8Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
var response = await server.CreateIdempotentClient()
|
var response = await server.CreateIdempotentClient()
|
||||||
.PostAsync(Post.CheckoutOrder, contentCheckout);
|
.PostAsync(Post.CheckoutOrder, contentCheckout);
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
string BuildBasket()
|
string BuildBasket()
|
||||||
|
@ -9,21 +9,19 @@ namespace Basket.FunctionalTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task UpdateBasket_return_and_add_basket()
|
public async Task UpdateBasket_return_and_add_basket()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
|
var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>();
|
||||||
|
|
||||||
|
var redisBasketRepository = BuildBasketRepository(redis);
|
||||||
|
|
||||||
|
var basket = await redisBasketRepository.UpdateBasketAsync(new CustomerBasket("customerId")
|
||||||
{
|
{
|
||||||
var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>();
|
BuyerId = "buyerId",
|
||||||
|
Items = BuildBasketItems()
|
||||||
|
});
|
||||||
|
|
||||||
var redisBasketRepository = BuildBasketRepository(redis);
|
Assert.NotNull(basket);
|
||||||
|
Assert.Single(basket.Items);
|
||||||
var basket = await redisBasketRepository.UpdateBasketAsync(new CustomerBasket("customerId")
|
|
||||||
{
|
|
||||||
BuyerId = "buyerId",
|
|
||||||
Items = BuildBasketItems()
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.NotNull(basket);
|
|
||||||
Assert.Single(basket.Items);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -32,25 +30,23 @@ namespace Basket.FunctionalTests
|
|||||||
public async Task Delete_Basket_return_null()
|
public async Task Delete_Basket_return_null()
|
||||||
{
|
{
|
||||||
|
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
|
var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>();
|
||||||
|
|
||||||
|
var redisBasketRepository = BuildBasketRepository(redis);
|
||||||
|
|
||||||
|
var basket = await redisBasketRepository.UpdateBasketAsync(new CustomerBasket("customerId")
|
||||||
{
|
{
|
||||||
var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>();
|
BuyerId = "buyerId",
|
||||||
|
Items = BuildBasketItems()
|
||||||
|
});
|
||||||
|
|
||||||
var redisBasketRepository = BuildBasketRepository(redis);
|
var deleteResult = await redisBasketRepository.DeleteBasketAsync("buyerId");
|
||||||
|
|
||||||
var basket = await redisBasketRepository.UpdateBasketAsync(new CustomerBasket("customerId")
|
var result = await redisBasketRepository.GetBasketAsync(basket.BuyerId);
|
||||||
{
|
|
||||||
BuyerId = "buyerId",
|
|
||||||
Items = BuildBasketItems()
|
|
||||||
});
|
|
||||||
|
|
||||||
var deleteResult = await redisBasketRepository.DeleteBasketAsync("buyerId");
|
Assert.True(deleteResult);
|
||||||
|
Assert.Null(result);
|
||||||
var result = await redisBasketRepository.GetBasketAsync(basket.BuyerId);
|
|
||||||
|
|
||||||
Assert.True(deleteResult);
|
|
||||||
Assert.Null(result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RedisBasketRepository BuildBasketRepository(ConnectionMultiplexer connMux)
|
RedisBasketRepository BuildBasketRepository(ConnectionMultiplexer connMux)
|
||||||
|
@ -47,40 +47,18 @@ public class PicController : ControllerBase
|
|||||||
|
|
||||||
private string GetImageMimeTypeFromImageFileExtension(string extension)
|
private string GetImageMimeTypeFromImageFileExtension(string extension)
|
||||||
{
|
{
|
||||||
string mimetype;
|
string mimetype = extension switch
|
||||||
|
|
||||||
switch (extension)
|
|
||||||
{
|
{
|
||||||
case ".png":
|
".png" => "image/png",
|
||||||
mimetype = "image/png";
|
".gif" => "image/gif",
|
||||||
break;
|
".jpg" or ".jpeg" => "image/jpeg",
|
||||||
case ".gif":
|
".bmp" => "image/bmp",
|
||||||
mimetype = "image/gif";
|
".tiff" => "image/tiff",
|
||||||
break;
|
".wmf" => "image/wmf",
|
||||||
case ".jpg":
|
".jp2" => "image/jp2",
|
||||||
case ".jpeg":
|
".svg" => "image/svg+xml",
|
||||||
mimetype = "image/jpeg";
|
_ => "application/octet-stream",
|
||||||
break;
|
};
|
||||||
case ".bmp":
|
|
||||||
mimetype = "image/bmp";
|
|
||||||
break;
|
|
||||||
case ".tiff":
|
|
||||||
mimetype = "image/tiff";
|
|
||||||
break;
|
|
||||||
case ".wmf":
|
|
||||||
mimetype = "image/wmf";
|
|
||||||
break;
|
|
||||||
case ".jp2":
|
|
||||||
mimetype = "image/jp2";
|
|
||||||
break;
|
|
||||||
case ".svg":
|
|
||||||
mimetype = "image/svg+xml";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
mimetype = "application/octet-stream";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mimetype;
|
return mimetype;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,122 +0,0 @@
|
|||||||
namespace Catalog.API.Extensions;
|
|
||||||
|
|
||||||
public static class HostExtensions
|
|
||||||
{
|
|
||||||
public static bool IsInKubernetes(this IHost host)
|
|
||||||
{
|
|
||||||
var cfg = host.Services.GetService<IConfiguration>();
|
|
||||||
var orchestratorType = cfg.GetValue<string>("OrchestratorType");
|
|
||||||
return orchestratorType?.ToUpper() == "K8S";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IHost MigrateDbContext<TContext>(this IHost host, Action<TContext, IServiceProvider> seeder) where TContext : DbContext
|
|
||||||
{
|
|
||||||
var underK8s = host.IsInKubernetes();
|
|
||||||
|
|
||||||
using (var scope = host.Services.CreateScope())
|
|
||||||
{
|
|
||||||
var services = scope.ServiceProvider;
|
|
||||||
|
|
||||||
var logger = services.GetRequiredService<ILogger<TContext>>();
|
|
||||||
|
|
||||||
var context = services.GetService<TContext>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
|
|
||||||
|
|
||||||
if (underK8s)
|
|
||||||
{
|
|
||||||
InvokeSeeder(seeder, context, services);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var retry = Policy.Handle<SqlException>()
|
|
||||||
.WaitAndRetry(new TimeSpan[]
|
|
||||||
{
|
|
||||||
TimeSpan.FromSeconds(3),
|
|
||||||
TimeSpan.FromSeconds(5),
|
|
||||||
TimeSpan.FromSeconds(8),
|
|
||||||
});
|
|
||||||
|
|
||||||
//if the sql server container is not created on run docker compose this
|
|
||||||
//migration can't fail for network related exception. The retry options for DbContext only
|
|
||||||
//apply to transient exceptions
|
|
||||||
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
|
|
||||||
retry.Execute(() => InvokeSeeder(seeder, context, services));
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
|
|
||||||
if (underK8s)
|
|
||||||
{
|
|
||||||
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IWebHost MigrateDbContext<TContext>(this IWebHost host, Action<TContext, IServiceProvider> seeder) where TContext : DbContext
|
|
||||||
{
|
|
||||||
var underK8s = host.IsInKubernetes();
|
|
||||||
|
|
||||||
using (var scope = host.Services.CreateScope())
|
|
||||||
{
|
|
||||||
var services = scope.ServiceProvider;
|
|
||||||
|
|
||||||
var logger = services.GetRequiredService<ILogger<TContext>>();
|
|
||||||
|
|
||||||
var context = services.GetService<TContext>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
|
|
||||||
|
|
||||||
if (underK8s)
|
|
||||||
{
|
|
||||||
InvokeSeeder(seeder, context, services);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var retry = Policy.Handle<SqlException>()
|
|
||||||
.WaitAndRetry(new TimeSpan[]
|
|
||||||
{
|
|
||||||
TimeSpan.FromSeconds(3),
|
|
||||||
TimeSpan.FromSeconds(5),
|
|
||||||
TimeSpan.FromSeconds(8),
|
|
||||||
});
|
|
||||||
|
|
||||||
//if the sql server container is not created on run docker compose this
|
|
||||||
//migration can't fail for network related exception. The retry options for DbContext only
|
|
||||||
//apply to transient exceptions
|
|
||||||
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
|
|
||||||
retry.Execute(() => InvokeSeeder(seeder, context, services));
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
|
|
||||||
if (underK8s)
|
|
||||||
{
|
|
||||||
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services)
|
|
||||||
where TContext : DbContext
|
|
||||||
{
|
|
||||||
context.Database.Migrate();
|
|
||||||
seeder(context, services);
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,48 +13,46 @@ public static class WebHostExtensions
|
|||||||
{
|
{
|
||||||
var underK8s = host.IsInKubernetes();
|
var underK8s = host.IsInKubernetes();
|
||||||
|
|
||||||
using (var scope = host.Services.CreateScope())
|
using var scope = host.Services.CreateScope();
|
||||||
|
var services = scope.ServiceProvider;
|
||||||
|
|
||||||
|
var logger = services.GetRequiredService<ILogger<TContext>>();
|
||||||
|
|
||||||
|
var context = services.GetService<TContext>();
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var services = scope.ServiceProvider;
|
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
|
||||||
|
|
||||||
var logger = services.GetRequiredService<ILogger<TContext>>();
|
if (underK8s)
|
||||||
|
|
||||||
var context = services.GetService<TContext>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
|
InvokeSeeder(seeder, context, services);
|
||||||
|
|
||||||
if (underK8s)
|
|
||||||
{
|
|
||||||
InvokeSeeder(seeder, context, services);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var retry = Policy.Handle<SqlException>()
|
|
||||||
.WaitAndRetry(new TimeSpan[]
|
|
||||||
{
|
|
||||||
TimeSpan.FromSeconds(3),
|
|
||||||
TimeSpan.FromSeconds(5),
|
|
||||||
TimeSpan.FromSeconds(8),
|
|
||||||
});
|
|
||||||
|
|
||||||
//if the sql server container is not created on run docker compose this
|
|
||||||
//migration can't fail for network related exception. The retry options for DbContext only
|
|
||||||
//apply to transient exceptions
|
|
||||||
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
|
|
||||||
retry.Execute(() => InvokeSeeder(seeder, context, services));
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
else
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
|
var retry = Policy.Handle<SqlException>()
|
||||||
if (underK8s)
|
.WaitAndRetry(new TimeSpan[]
|
||||||
{
|
{
|
||||||
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
|
TimeSpan.FromSeconds(3),
|
||||||
}
|
TimeSpan.FromSeconds(5),
|
||||||
|
TimeSpan.FromSeconds(8),
|
||||||
|
});
|
||||||
|
|
||||||
|
//if the sql server container is not created on run docker compose this
|
||||||
|
//migration can't fail for network related exception. The retry options for DbContext only
|
||||||
|
//apply to transient exceptions
|
||||||
|
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
|
||||||
|
retry.Execute(() => InvokeSeeder(seeder, context, services));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
|
||||||
|
if (underK8s)
|
||||||
|
{
|
||||||
|
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,11 +92,11 @@ public class CatalogContextSeed
|
|||||||
{
|
{
|
||||||
return new List<CatalogBrand>()
|
return new List<CatalogBrand>()
|
||||||
{
|
{
|
||||||
new CatalogBrand() { Brand = "Azure"},
|
new() { Brand = "Azure"},
|
||||||
new CatalogBrand() { Brand = ".NET" },
|
new() { Brand = ".NET" },
|
||||||
new CatalogBrand() { Brand = "Visual Studio" },
|
new() { Brand = "Visual Studio" },
|
||||||
new CatalogBrand() { Brand = "SQL Server" },
|
new() { Brand = "SQL Server" },
|
||||||
new CatalogBrand() { Brand = "Other" }
|
new() { Brand = "Other" }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,10 +147,10 @@ public class CatalogContextSeed
|
|||||||
{
|
{
|
||||||
return new List<CatalogType>()
|
return new List<CatalogType>()
|
||||||
{
|
{
|
||||||
new CatalogType() { Type = "Mug"},
|
new() { Type = "Mug"},
|
||||||
new CatalogType() { Type = "T-Shirt" },
|
new() { Type = "T-Shirt" },
|
||||||
new CatalogType() { Type = "Sheet" },
|
new() { Type = "Sheet" },
|
||||||
new CatalogType() { Type = "USB Memory Stick" }
|
new() { Type = "USB Memory Stick" }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,18 +297,18 @@ public class CatalogContextSeed
|
|||||||
{
|
{
|
||||||
return new List<CatalogItem>()
|
return new List<CatalogItem>()
|
||||||
{
|
{
|
||||||
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Bot Black Hoodie", Name = ".NET Bot Black Hoodie", Price = 19.5M, PictureFileName = "1.png" },
|
new() { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Bot Black Hoodie", Name = ".NET Bot Black Hoodie", Price = 19.5M, PictureFileName = "1.png" },
|
||||||
new CatalogItem { CatalogTypeId = 1, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Black & White Mug", Name = ".NET Black & White Mug", Price= 8.50M, PictureFileName = "2.png" },
|
new() { CatalogTypeId = 1, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Black & White Mug", Name = ".NET Black & White Mug", Price= 8.50M, PictureFileName = "2.png" },
|
||||||
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Prism White T-Shirt", Name = "Prism White T-Shirt", Price = 12, PictureFileName = "3.png" },
|
new() { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Prism White T-Shirt", Name = "Prism White T-Shirt", Price = 12, PictureFileName = "3.png" },
|
||||||
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Foundation T-shirt", Name = ".NET Foundation T-shirt", Price = 12, PictureFileName = "4.png" },
|
new() { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Foundation T-shirt", Name = ".NET Foundation T-shirt", Price = 12, PictureFileName = "4.png" },
|
||||||
new CatalogItem { CatalogTypeId = 3, CatalogBrandId = 5, AvailableStock = 100, Description = "Roslyn Red Sheet", Name = "Roslyn Red Sheet", Price = 8.5M, PictureFileName = "5.png" },
|
new() { CatalogTypeId = 3, CatalogBrandId = 5, AvailableStock = 100, Description = "Roslyn Red Sheet", Name = "Roslyn Red Sheet", Price = 8.5M, PictureFileName = "5.png" },
|
||||||
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Blue Hoodie", Name = ".NET Blue Hoodie", Price = 12, PictureFileName = "6.png" },
|
new() { CatalogTypeId = 2, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Blue Hoodie", Name = ".NET Blue Hoodie", Price = 12, PictureFileName = "6.png" },
|
||||||
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Roslyn Red T-Shirt", Name = "Roslyn Red T-Shirt", Price = 12, PictureFileName = "7.png" },
|
new() { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Roslyn Red T-Shirt", Name = "Roslyn Red T-Shirt", Price = 12, PictureFileName = "7.png" },
|
||||||
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Kudu Purple Hoodie", Name = "Kudu Purple Hoodie", Price = 8.5M, PictureFileName = "8.png" },
|
new() { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Kudu Purple Hoodie", Name = "Kudu Purple Hoodie", Price = 8.5M, PictureFileName = "8.png" },
|
||||||
new CatalogItem { CatalogTypeId = 1, CatalogBrandId = 5, AvailableStock = 100, Description = "Cup<T> White Mug", Name = "Cup<T> White Mug", Price = 12, PictureFileName = "9.png" },
|
new() { CatalogTypeId = 1, CatalogBrandId = 5, AvailableStock = 100, Description = "Cup<T> White Mug", Name = "Cup<T> White Mug", Price = 12, PictureFileName = "9.png" },
|
||||||
new CatalogItem { CatalogTypeId = 3, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Foundation Sheet", Name = ".NET Foundation Sheet", Price = 12, PictureFileName = "10.png" },
|
new() { CatalogTypeId = 3, CatalogBrandId = 2, AvailableStock = 100, Description = ".NET Foundation Sheet", Name = ".NET Foundation Sheet", Price = 12, PictureFileName = "10.png" },
|
||||||
new CatalogItem { CatalogTypeId = 3, CatalogBrandId = 2, AvailableStock = 100, Description = "Cup<T> Sheet", Name = "Cup<T> Sheet", Price = 8.5M, PictureFileName = "11.png" },
|
new() { CatalogTypeId = 3, CatalogBrandId = 2, AvailableStock = 100, Description = "Cup<T> Sheet", Name = "Cup<T> Sheet", Price = 8.5M, PictureFileName = "11.png" },
|
||||||
new CatalogItem { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Prism White TShirt", Name = "Prism White TShirt", Price = 12, PictureFileName = "12.png" },
|
new() { CatalogTypeId = 2, CatalogBrandId = 5, AvailableStock = 100, Description = "Prism White TShirt", Name = "Prism White TShirt", Price = 12, PictureFileName = "12.png" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,135 +6,113 @@ public class CatalogScenarios
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_get_all_catalogitems_and_response_ok_status_code()
|
public async Task Get_get_all_catalogitems_and_response_ok_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.GetAsync(Get.Items());
|
||||||
.GetAsync(Get.Items());
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_get_catalogitem_by_id_and_response_ok_status_code()
|
public async Task Get_get_catalogitem_by_id_and_response_ok_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.GetAsync(Get.ItemById(1));
|
||||||
.GetAsync(Get.ItemById(1));
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_get_catalogitem_by_id_and_response_bad_request_status_code()
|
public async Task Get_get_catalogitem_by_id_and_response_bad_request_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.GetAsync(Get.ItemById(int.MinValue));
|
||||||
.GetAsync(Get.ItemById(int.MinValue));
|
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_get_catalogitem_by_id_and_response_not_found_status_code()
|
public async Task Get_get_catalogitem_by_id_and_response_not_found_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.GetAsync(Get.ItemById(int.MaxValue));
|
||||||
.GetAsync(Get.ItemById(int.MaxValue));
|
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_get_catalogitem_by_name_and_response_ok_status_code()
|
public async Task Get_get_catalogitem_by_name_and_response_ok_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.GetAsync(Get.ItemByName(".NET"));
|
||||||
.GetAsync(Get.ItemByName(".NET"));
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_get_paginated_catalogitem_by_name_and_response_ok_status_code()
|
public async Task Get_get_paginated_catalogitem_by_name_and_response_ok_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
const bool paginated = true;
|
||||||
const bool paginated = true;
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.GetAsync(Get.ItemByName(".NET", paginated));
|
||||||
.GetAsync(Get.ItemByName(".NET", paginated));
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_get_paginated_catalog_items_and_response_ok_status_code()
|
public async Task Get_get_paginated_catalog_items_and_response_ok_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
const bool paginated = true;
|
||||||
const bool paginated = true;
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.GetAsync(Get.Items(paginated));
|
||||||
.GetAsync(Get.Items(paginated));
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_get_filtered_catalog_items_and_response_ok_status_code()
|
public async Task Get_get_filtered_catalog_items_and_response_ok_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.GetAsync(Get.Filtered(1, 1));
|
||||||
.GetAsync(Get.Filtered(1, 1));
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_get_paginated_filtered_catalog_items_and_response_ok_status_code()
|
public async Task Get_get_paginated_filtered_catalog_items_and_response_ok_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
const bool paginated = true;
|
||||||
const bool paginated = true;
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.GetAsync(Get.Filtered(1, 1, paginated));
|
||||||
.GetAsync(Get.Filtered(1, 1, paginated));
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_catalog_types_response_ok_status_code()
|
public async Task Get_catalog_types_response_ok_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.GetAsync(Get.Types);
|
||||||
.GetAsync(Get.Types);
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_catalog_brands_response_ok_status_code()
|
public async Task Get_catalog_brands_response_ok_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.GetAsync(Get.Brands);
|
||||||
.GetAsync(Get.Brands);
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
global using Catalog.API.Extensions;
|
global using Microsoft.AspNetCore.Hosting;
|
||||||
global using Microsoft.AspNetCore.Hosting;
|
|
||||||
global using Microsoft.AspNetCore.TestHost;
|
global using Microsoft.AspNetCore.TestHost;
|
||||||
global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF;
|
global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF;
|
||||||
|
global using Microsoft.eShopOnContainers.Services.Catalog.API.Extensions;
|
||||||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure;
|
global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure;
|
||||||
global using Microsoft.eShopOnContainers.Services.Catalog.API;
|
global using Microsoft.eShopOnContainers.Services.Catalog.API;
|
||||||
global using Microsoft.Extensions.Configuration;
|
global using Microsoft.Extensions.Configuration;
|
||||||
|
@ -25,11 +25,9 @@ public class CatalogControllerTest
|
|||||||
.UseInMemoryDatabase(databaseName: "in-memory")
|
.UseInMemoryDatabase(databaseName: "in-memory")
|
||||||
.Options;
|
.Options;
|
||||||
|
|
||||||
using (var dbContext = new CatalogContext(_dbOptions))
|
using var dbContext = new CatalogContext(_dbOptions);
|
||||||
{
|
dbContext.AddRange(GetFakeCatalog());
|
||||||
dbContext.AddRange(GetFakeCatalog());
|
dbContext.SaveChanges();
|
||||||
dbContext.SaveChanges();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -66,7 +64,7 @@ public class CatalogControllerTest
|
|||||||
{
|
{
|
||||||
return new List<CatalogItem>()
|
return new List<CatalogItem>()
|
||||||
{
|
{
|
||||||
new CatalogItem()
|
new()
|
||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
Name = "fakeItemA",
|
Name = "fakeItemA",
|
||||||
@ -74,7 +72,7 @@ public class CatalogControllerTest
|
|||||||
CatalogBrandId = 1,
|
CatalogBrandId = 1,
|
||||||
PictureFileName = "fakeItemA.png"
|
PictureFileName = "fakeItemA.png"
|
||||||
},
|
},
|
||||||
new CatalogItem()
|
new()
|
||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
Name = "fakeItemB",
|
Name = "fakeItemB",
|
||||||
@ -82,7 +80,7 @@ public class CatalogControllerTest
|
|||||||
CatalogBrandId = 1,
|
CatalogBrandId = 1,
|
||||||
PictureFileName = "fakeItemB.png"
|
PictureFileName = "fakeItemB.png"
|
||||||
},
|
},
|
||||||
new CatalogItem()
|
new()
|
||||||
{
|
{
|
||||||
Id = 3,
|
Id = 3,
|
||||||
Name = "fakeItemC",
|
Name = "fakeItemC",
|
||||||
@ -90,7 +88,7 @@ public class CatalogControllerTest
|
|||||||
CatalogBrandId = 1,
|
CatalogBrandId = 1,
|
||||||
PictureFileName = "fakeItemC.png"
|
PictureFileName = "fakeItemC.png"
|
||||||
},
|
},
|
||||||
new CatalogItem()
|
new()
|
||||||
{
|
{
|
||||||
Id = 4,
|
Id = 4,
|
||||||
Name = "fakeItemD",
|
Name = "fakeItemD",
|
||||||
@ -98,7 +96,7 @@ public class CatalogControllerTest
|
|||||||
CatalogBrandId = 1,
|
CatalogBrandId = 1,
|
||||||
PictureFileName = "fakeItemD.png"
|
PictureFileName = "fakeItemD.png"
|
||||||
},
|
},
|
||||||
new CatalogItem()
|
new()
|
||||||
{
|
{
|
||||||
Id = 5,
|
Id = 5,
|
||||||
Name = "fakeItemE",
|
Name = "fakeItemE",
|
||||||
@ -106,7 +104,7 @@ public class CatalogControllerTest
|
|||||||
CatalogBrandId = 1,
|
CatalogBrandId = 1,
|
||||||
PictureFileName = "fakeItemE.png"
|
PictureFileName = "fakeItemE.png"
|
||||||
},
|
},
|
||||||
new CatalogItem()
|
new()
|
||||||
{
|
{
|
||||||
Id = 6,
|
Id = 6,
|
||||||
Name = "fakeItemF",
|
Name = "fakeItemF",
|
||||||
@ -120,7 +118,7 @@ public class CatalogControllerTest
|
|||||||
|
|
||||||
public class TestCatalogSettings : IOptionsSnapshot<CatalogSettings>
|
public class TestCatalogSettings : IOptionsSnapshot<CatalogSettings>
|
||||||
{
|
{
|
||||||
public CatalogSettings Value => new CatalogSettings
|
public CatalogSettings Value => new()
|
||||||
{
|
{
|
||||||
PicBaseUrl = "http://image-server.com/",
|
PicBaseUrl = "http://image-server.com/",
|
||||||
AzureStorageEnabled = true
|
AzureStorageEnabled = true
|
||||||
|
@ -12,24 +12,20 @@
|
|||||||
* real environment the certificate should be created and stored in a secure way, which is out
|
* real environment the certificate should be created and stored in a secure way, which is out
|
||||||
* of the scope of this project.
|
* of the scope of this project.
|
||||||
**********************************************************************************************/
|
**********************************************************************************************/
|
||||||
using (var stream = assembly.GetManifestResourceStream("Identity.API.Certificate.idsrv3test.pfx"))
|
using var stream = assembly.GetManifestResourceStream("Identity.API.Certificate.idsrv3test.pfx");
|
||||||
{
|
return new X509Certificate2(ReadStream(stream), "idsrv3test");
|
||||||
return new X509Certificate2(ReadStream(stream), "idsrv3test");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] ReadStream(Stream input)
|
private static byte[] ReadStream(Stream input)
|
||||||
{
|
{
|
||||||
byte[] buffer = new byte[16 * 1024];
|
byte[] buffer = new byte[16 * 1024];
|
||||||
using (MemoryStream ms = new MemoryStream())
|
using MemoryStream ms = new MemoryStream();
|
||||||
|
int read;
|
||||||
|
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
|
||||||
{
|
{
|
||||||
int read;
|
ms.Write(buffer, 0, read);
|
||||||
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
|
|
||||||
{
|
|
||||||
ms.Write(buffer, 0, read);
|
|
||||||
}
|
|
||||||
return ms.ToArray();
|
|
||||||
}
|
}
|
||||||
|
return ms.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -191,23 +191,21 @@
|
|||||||
string imagePath = Path.Combine(webroot, "images");
|
string imagePath = Path.Combine(webroot, "images");
|
||||||
string[] imageFiles = Directory.GetFiles(imagePath).Select(file => Path.GetFileName(file)).ToArray();
|
string[] imageFiles = Directory.GetFiles(imagePath).Select(file => Path.GetFileName(file)).ToArray();
|
||||||
|
|
||||||
using (ZipArchive zip = ZipFile.Open(imagesZipFile, ZipArchiveMode.Read))
|
using ZipArchive zip = ZipFile.Open(imagesZipFile, ZipArchiveMode.Read);
|
||||||
|
foreach (ZipArchiveEntry entry in zip.Entries)
|
||||||
{
|
{
|
||||||
foreach (ZipArchiveEntry entry in zip.Entries)
|
if (imageFiles.Contains(entry.Name))
|
||||||
{
|
{
|
||||||
if (imageFiles.Contains(entry.Name))
|
string destinationFilename = Path.Combine(imagePath, entry.Name);
|
||||||
|
if (File.Exists(destinationFilename))
|
||||||
{
|
{
|
||||||
string destinationFilename = Path.Combine(imagePath, entry.Name);
|
File.Delete(destinationFilename);
|
||||||
if (File.Exists(destinationFilename))
|
|
||||||
{
|
|
||||||
File.Delete(destinationFilename);
|
|
||||||
}
|
|
||||||
entry.ExtractToFile(destinationFilename);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logger.LogWarning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
|
|
||||||
}
|
}
|
||||||
|
entry.ExtractToFile(destinationFilename);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,52 +13,50 @@ namespace Microsoft.AspNetCore.Hosting
|
|||||||
{
|
{
|
||||||
var underK8s = webHost.IsInKubernetes();
|
var underK8s = webHost.IsInKubernetes();
|
||||||
|
|
||||||
using (var scope = webHost.Services.CreateScope())
|
using var scope = webHost.Services.CreateScope();
|
||||||
|
var services = scope.ServiceProvider;
|
||||||
|
var logger = services.GetRequiredService<ILogger<TContext>>();
|
||||||
|
var context = services.GetService<TContext>();
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var services = scope.ServiceProvider;
|
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
|
||||||
var logger = services.GetRequiredService<ILogger<TContext>>();
|
|
||||||
var context = services.GetService<TContext>();
|
|
||||||
|
|
||||||
try
|
if (underK8s)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
|
InvokeSeeder(seeder, context, services);
|
||||||
|
|
||||||
if (underK8s)
|
|
||||||
{
|
|
||||||
InvokeSeeder(seeder, context, services);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var retries = 10;
|
|
||||||
var retry = Policy.Handle<SqlException>()
|
|
||||||
.WaitAndRetry(
|
|
||||||
retryCount: retries,
|
|
||||||
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
|
|
||||||
onRetry: (exception, timeSpan, retry, ctx) =>
|
|
||||||
{
|
|
||||||
logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", nameof(TContext), exception.GetType().Name, exception.Message, retry, retries);
|
|
||||||
});
|
|
||||||
|
|
||||||
//if the sql server container is not created on run docker compose this
|
|
||||||
//migration can't fail for network related exception. The retry options for DbContext only
|
|
||||||
//apply to transient exceptions
|
|
||||||
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
|
|
||||||
retry.Execute(() => InvokeSeeder(seeder, context, services));
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
else
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
|
var retries = 10;
|
||||||
if (underK8s)
|
var retry = Policy.Handle<SqlException>()
|
||||||
{
|
.WaitAndRetry(
|
||||||
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
|
retryCount: retries,
|
||||||
}
|
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
|
||||||
|
onRetry: (exception, timeSpan, retry, ctx) =>
|
||||||
|
{
|
||||||
|
logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", nameof(TContext), exception.GetType().Name, exception.Message, retry, retries);
|
||||||
|
});
|
||||||
|
|
||||||
|
//if the sql server container is not created on run docker compose this
|
||||||
|
//migration can't fail for network related exception. The retry options for DbContext only
|
||||||
|
//apply to transient exceptions
|
||||||
|
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
|
||||||
|
retry.Execute(() => InvokeSeeder(seeder, context, services));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
|
||||||
|
if (underK8s)
|
||||||
|
{
|
||||||
|
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return webHost;
|
return webHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services)
|
private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services)
|
||||||
|
@ -35,7 +35,7 @@ public class TransactionBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequ
|
|||||||
{
|
{
|
||||||
Guid transactionId;
|
Guid transactionId;
|
||||||
|
|
||||||
using (var transaction = await _dbContext.BeginTransactionAsync())
|
using var transaction = await _dbContext.BeginTransactionAsync();
|
||||||
using (LogContext.PushProperty("TransactionContext", transaction.TransactionId))
|
using (LogContext.PushProperty("TransactionContext", transaction.TransactionId))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("----- Begin transaction {TransactionId} for {CommandName} ({@Command})", transaction.TransactionId, typeName, request);
|
_logger.LogInformation("----- Begin transaction {TransactionId} for {CommandName} ({@Command})", transaction.TransactionId, typeName, request);
|
||||||
|
@ -13,12 +13,11 @@ public class OrderQueries
|
|||||||
|
|
||||||
public async Task<Order> GetOrderAsync(int id)
|
public async Task<Order> GetOrderAsync(int id)
|
||||||
{
|
{
|
||||||
using (var connection = new SqlConnection(_connectionString))
|
using var connection = new SqlConnection(_connectionString);
|
||||||
{
|
connection.Open();
|
||||||
connection.Open();
|
|
||||||
|
|
||||||
var result = await connection.QueryAsync<dynamic>(
|
var result = await connection.QueryAsync<dynamic>(
|
||||||
@"select o.[Id] as ordernumber,o.OrderDate as date, o.Description as description,
|
@"select o.[Id] as ordernumber,o.OrderDate as date, o.Description as description,
|
||||||
o.Address_City as city, o.Address_Country as country, o.Address_State as state, o.Address_Street as street, o.Address_ZipCode as zipcode,
|
o.Address_City as city, o.Address_Country as country, o.Address_State as state, o.Address_Street as street, o.Address_ZipCode as zipcode,
|
||||||
os.Name as status,
|
os.Name as status,
|
||||||
oi.ProductName as productname, oi.Units as units, oi.UnitPrice as unitprice, oi.PictureUrl as pictureurl
|
oi.ProductName as productname, oi.Units as units, oi.UnitPrice as unitprice, oi.PictureUrl as pictureurl
|
||||||
@ -26,23 +25,21 @@ public class OrderQueries
|
|||||||
LEFT JOIN ordering.Orderitems oi ON o.Id = oi.orderid
|
LEFT JOIN ordering.Orderitems oi ON o.Id = oi.orderid
|
||||||
LEFT JOIN ordering.orderstatus os on o.OrderStatusId = os.Id
|
LEFT JOIN ordering.orderstatus os on o.OrderStatusId = os.Id
|
||||||
WHERE o.Id=@id"
|
WHERE o.Id=@id"
|
||||||
, new { id }
|
, new { id }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.AsList().Count == 0)
|
if (result.AsList().Count == 0)
|
||||||
throw new KeyNotFoundException();
|
throw new KeyNotFoundException();
|
||||||
|
|
||||||
return MapOrderItems(result);
|
return MapOrderItems(result);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<OrderSummary>> GetOrdersFromUserAsync(Guid userId)
|
public async Task<IEnumerable<OrderSummary>> GetOrdersFromUserAsync(Guid userId)
|
||||||
{
|
{
|
||||||
using (var connection = new SqlConnection(_connectionString))
|
using var connection = new SqlConnection(_connectionString);
|
||||||
{
|
connection.Open();
|
||||||
connection.Open();
|
|
||||||
|
|
||||||
return await connection.QueryAsync<OrderSummary>(@"SELECT o.[Id] as ordernumber,o.[OrderDate] as [date],os.[Name] as [status], SUM(oi.units*oi.unitprice) as total
|
return await connection.QueryAsync<OrderSummary>(@"SELECT o.[Id] as ordernumber,o.[OrderDate] as [date],os.[Name] as [status], SUM(oi.units*oi.unitprice) as total
|
||||||
FROM [ordering].[Orders] o
|
FROM [ordering].[Orders] o
|
||||||
LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid
|
LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid
|
||||||
LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id
|
LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id
|
||||||
@ -50,17 +47,14 @@ public class OrderQueries
|
|||||||
WHERE ob.IdentityGuid = @userId
|
WHERE ob.IdentityGuid = @userId
|
||||||
GROUP BY o.[Id], o.[OrderDate], os.[Name]
|
GROUP BY o.[Id], o.[OrderDate], os.[Name]
|
||||||
ORDER BY o.[Id]", new { userId });
|
ORDER BY o.[Id]", new { userId });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<CardType>> GetCardTypesAsync()
|
public async Task<IEnumerable<CardType>> GetCardTypesAsync()
|
||||||
{
|
{
|
||||||
using (var connection = new SqlConnection(_connectionString))
|
using var connection = new SqlConnection(_connectionString);
|
||||||
{
|
connection.Open();
|
||||||
connection.Open();
|
|
||||||
|
|
||||||
return await connection.QueryAsync<CardType>("SELECT * FROM ordering.cardtypes");
|
return await connection.QueryAsync<CardType>("SELECT * FROM ordering.cardtypes");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Order MapOrderItems(dynamic result)
|
private Order MapOrderItems(dynamic result)
|
||||||
|
@ -20,7 +20,7 @@ public class AuthorizeCheckOperationFilter : IOperationFilter
|
|||||||
|
|
||||||
operation.Security = new List<OpenApiSecurityRequirement>
|
operation.Security = new List<OpenApiSecurityRequirement>
|
||||||
{
|
{
|
||||||
new OpenApiSecurityRequirement
|
new()
|
||||||
{
|
{
|
||||||
[ oAuthScheme ] = new [] { "orderingapi" }
|
[ oAuthScheme ] = new [] { "orderingapi" }
|
||||||
}
|
}
|
||||||
|
@ -63,23 +63,21 @@ namespace Ordering.BackgroundTasks.Services
|
|||||||
{
|
{
|
||||||
IEnumerable<int> orderIds = new List<int>();
|
IEnumerable<int> orderIds = new List<int>();
|
||||||
|
|
||||||
using (var conn = new SqlConnection(_settings.ConnectionString))
|
using var conn = new SqlConnection(_settings.ConnectionString);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try
|
conn.Open();
|
||||||
{
|
orderIds = conn.Query<int>(
|
||||||
conn.Open();
|
@"SELECT Id FROM [ordering].[orders]
|
||||||
orderIds = conn.Query<int>(
|
WHERE DATEDIFF(minute, [OrderDate], GETDATE()) >= @GracePeriodTime
|
||||||
@"SELECT Id FROM [ordering].[orders]
|
AND [OrderStatusId] = 1",
|
||||||
WHERE DATEDIFF(minute, [OrderDate], GETDATE()) >= @GracePeriodTime
|
new { _settings.GracePeriodTime });
|
||||||
AND [OrderStatusId] = 1",
|
|
||||||
new { _settings.GracePeriodTime });
|
|
||||||
}
|
|
||||||
catch (SqlException exception)
|
|
||||||
{
|
|
||||||
_logger.LogCritical(exception, "FATAL ERROR: Database connections could not be opened: {Message}", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
catch (SqlException exception)
|
||||||
|
{
|
||||||
|
_logger.LogCritical(exception, "FATAL ERROR: Database connections could not be opened: {Message}", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return orderIds;
|
return orderIds;
|
||||||
}
|
}
|
||||||
|
@ -14,39 +14,33 @@ namespace Ordering.FunctionalTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Get_get_all_stored_orders_and_response_ok_status_code()
|
public async Task Get_get_all_stored_orders_and_response_ok_status_code()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var response = await server.CreateClient()
|
||||||
var response = await server.CreateClient()
|
.GetAsync(Get.Orders);
|
||||||
.GetAsync(Get.Orders);
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Cancel_order_no_order_created_bad_request_response()
|
public async Task Cancel_order_no_order_created_bad_request_response()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json");
|
||||||
var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json");
|
var response = await server.CreateIdempotentClient()
|
||||||
var response = await server.CreateIdempotentClient()
|
.PutAsync(Put.CancelOrder, content);
|
||||||
.PutAsync(Put.CancelOrder, content);
|
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Ship_order_no_order_created_bad_request_response()
|
public async Task Ship_order_no_order_created_bad_request_response()
|
||||||
{
|
{
|
||||||
using (var server = CreateServer())
|
using var server = CreateServer();
|
||||||
{
|
var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json");
|
||||||
var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json");
|
var response = await server.CreateIdempotentClient()
|
||||||
var response = await server.CreateIdempotentClient()
|
.PutAsync(Put.ShipOrder, content);
|
||||||
.PutAsync(Put.ShipOrder, content);
|
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
string BuildOrder()
|
string BuildOrder()
|
||||||
|
@ -20,7 +20,7 @@ public class AuthorizeCheckOperationFilter : IOperationFilter
|
|||||||
|
|
||||||
operation.Security = new List<OpenApiSecurityRequirement>
|
operation.Security = new List<OpenApiSecurityRequirement>
|
||||||
{
|
{
|
||||||
new OpenApiSecurityRequirement
|
new()
|
||||||
{
|
{
|
||||||
[ oAuthScheme ] = new [] { "webhooksapi" }
|
[ oAuthScheme ] = new [] { "webhooksapi" }
|
||||||
}
|
}
|
||||||
|
@ -11,45 +11,43 @@ public class IntegrationEventsScenarios
|
|||||||
decimal priceModification = 0.15M;
|
decimal priceModification = 0.15M;
|
||||||
string userId = "JohnId";
|
string userId = "JohnId";
|
||||||
|
|
||||||
using (var catalogServer = new CatalogScenariosBase().CreateServer())
|
using var catalogServer = new CatalogScenariosBase().CreateServer();
|
||||||
using (var basketServer = new BasketScenariosBase().CreateServer())
|
using var basketServer = new BasketScenariosBase().CreateServer();
|
||||||
|
var catalogClient = catalogServer.CreateClient();
|
||||||
|
var basketClient = basketServer.CreateClient();
|
||||||
|
|
||||||
|
// GIVEN a product catalog list
|
||||||
|
var originalCatalogProducts = await GetCatalogAsync(catalogClient);
|
||||||
|
|
||||||
|
// AND a user basket filled with products
|
||||||
|
var basket = ComposeBasket(userId, originalCatalogProducts.Data.Take(3));
|
||||||
|
var res = await basketClient.PostAsync(
|
||||||
|
BasketScenariosBase.Post.CreateBasket,
|
||||||
|
new StringContent(JsonSerializer.Serialize(basket), UTF8Encoding.UTF8, "application/json")
|
||||||
|
);
|
||||||
|
|
||||||
|
// WHEN the price of one product is modified in the catalog
|
||||||
|
var itemToModify = basket.Items[2];
|
||||||
|
var oldPrice = itemToModify.UnitPrice;
|
||||||
|
var newPrice = oldPrice + priceModification;
|
||||||
|
var pRes = await catalogClient.PutAsync(CatalogScenariosBase.Put.UpdateCatalogProduct, new StringContent(ChangePrice(itemToModify, newPrice, originalCatalogProducts), UTF8Encoding.UTF8, "application/json"));
|
||||||
|
|
||||||
|
var modifiedCatalogProducts = await GetCatalogAsync(catalogClient);
|
||||||
|
|
||||||
|
var itemUpdated = await GetUpdatedBasketItem(newPrice, itemToModify.ProductId, userId, basketClient);
|
||||||
|
|
||||||
|
if (itemUpdated == null)
|
||||||
{
|
{
|
||||||
var catalogClient = catalogServer.CreateClient();
|
Assert.False(true, $"The basket service has not been updated.");
|
||||||
var basketClient = basketServer.CreateClient();
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//THEN the product price changes in the catalog
|
||||||
|
Assert.Equal(newPrice, modifiedCatalogProducts.Data.Single(it => it.Id == itemToModify.ProductId).Price);
|
||||||
|
|
||||||
// GIVEN a product catalog list
|
// AND the products in the basket reflects the changed priced and the original price
|
||||||
var originalCatalogProducts = await GetCatalogAsync(catalogClient);
|
Assert.Equal(newPrice, itemUpdated.UnitPrice);
|
||||||
|
Assert.Equal(oldPrice, itemUpdated.OldUnitPrice);
|
||||||
// AND a user basket filled with products
|
|
||||||
var basket = ComposeBasket(userId, originalCatalogProducts.Data.Take(3));
|
|
||||||
var res = await basketClient.PostAsync(
|
|
||||||
BasketScenariosBase.Post.CreateBasket,
|
|
||||||
new StringContent(JsonSerializer.Serialize(basket), UTF8Encoding.UTF8, "application/json")
|
|
||||||
);
|
|
||||||
|
|
||||||
// WHEN the price of one product is modified in the catalog
|
|
||||||
var itemToModify = basket.Items[2];
|
|
||||||
var oldPrice = itemToModify.UnitPrice;
|
|
||||||
var newPrice = oldPrice + priceModification;
|
|
||||||
var pRes = await catalogClient.PutAsync(CatalogScenariosBase.Put.UpdateCatalogProduct, new StringContent(ChangePrice(itemToModify, newPrice, originalCatalogProducts), UTF8Encoding.UTF8, "application/json"));
|
|
||||||
|
|
||||||
var modifiedCatalogProducts = await GetCatalogAsync(catalogClient);
|
|
||||||
|
|
||||||
var itemUpdated = await GetUpdatedBasketItem(newPrice, itemToModify.ProductId, userId, basketClient);
|
|
||||||
|
|
||||||
if (itemUpdated == null)
|
|
||||||
{
|
|
||||||
Assert.False(true, $"The basket service has not been updated.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//THEN the product price changes in the catalog
|
|
||||||
Assert.Equal(newPrice, modifiedCatalogProducts.Data.Single(it => it.Id == itemToModify.ProductId).Price);
|
|
||||||
|
|
||||||
// AND the products in the basket reflects the changed priced and the original price
|
|
||||||
Assert.Equal(newPrice, itemUpdated.UnitPrice);
|
|
||||||
Assert.Equal(oldPrice, itemUpdated.OldUnitPrice);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,35 +5,33 @@ public class OrderingScenarios : OrderingScenariosBase
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Cancel_basket_and_check_order_status_cancelled()
|
public async Task Cancel_basket_and_check_order_status_cancelled()
|
||||||
{
|
{
|
||||||
using (var orderServer = new OrderingScenariosBase().CreateServer())
|
using var orderServer = new OrderingScenariosBase().CreateServer();
|
||||||
using (var basketServer = new BasketScenariosBase().CreateServer())
|
using var basketServer = new BasketScenariosBase().CreateServer();
|
||||||
{
|
// Expected data
|
||||||
// Expected data
|
var cityExpected = $"city-{Guid.NewGuid()}";
|
||||||
var cityExpected = $"city-{Guid.NewGuid()}";
|
var orderStatusExpected = "cancelled";
|
||||||
var orderStatusExpected = "cancelled";
|
|
||||||
|
|
||||||
var basketClient = basketServer.CreateIdempotentClient();
|
var basketClient = basketServer.CreateIdempotentClient();
|
||||||
var orderClient = orderServer.CreateIdempotentClient();
|
var orderClient = orderServer.CreateIdempotentClient();
|
||||||
|
|
||||||
// GIVEN a basket is created
|
// GIVEN a basket is created
|
||||||
var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
|
var contentBasket = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
|
||||||
await basketClient.PostAsync(BasketScenariosBase.Post.CreateBasket, contentBasket);
|
await basketClient.PostAsync(BasketScenariosBase.Post.CreateBasket, contentBasket);
|
||||||
|
|
||||||
// AND basket checkout is sent
|
// AND basket checkout is sent
|
||||||
await basketClient.PostAsync(BasketScenariosBase.Post.CheckoutOrder, new StringContent(BuildCheckout(cityExpected), UTF8Encoding.UTF8, "application/json"));
|
await basketClient.PostAsync(BasketScenariosBase.Post.CheckoutOrder, new StringContent(BuildCheckout(cityExpected), UTF8Encoding.UTF8, "application/json"));
|
||||||
|
|
||||||
// WHEN Order is created in Ordering.api
|
// WHEN Order is created in Ordering.api
|
||||||
var newOrder = await TryGetNewOrderCreated(cityExpected, orderClient);
|
var newOrder = await TryGetNewOrderCreated(cityExpected, orderClient);
|
||||||
|
|
||||||
// AND Order is cancelled in Ordering.api
|
// AND Order is cancelled in Ordering.api
|
||||||
await orderClient.PutAsync(OrderingScenariosBase.Put.CancelOrder, new StringContent(BuildCancelOrder(newOrder.OrderNumber), UTF8Encoding.UTF8, "application/json"));
|
await orderClient.PutAsync(OrderingScenariosBase.Put.CancelOrder, new StringContent(BuildCancelOrder(newOrder.OrderNumber), UTF8Encoding.UTF8, "application/json"));
|
||||||
|
|
||||||
// AND the requested order is retrieved
|
// AND the requested order is retrieved
|
||||||
var order = await TryGetOrder(newOrder.OrderNumber, orderClient);
|
var order = await TryGetOrder(newOrder.OrderNumber, orderClient);
|
||||||
|
|
||||||
// THEN check status
|
// THEN check status
|
||||||
Assert.Equal(orderStatusExpected, order.Status);
|
Assert.Equal(orderStatusExpected, order.Status);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<Order> TryGetOrder(string orderNumber, HttpClient orderClient)
|
async Task<Order> TryGetOrder(string orderNumber, HttpClient orderClient)
|
||||||
|
@ -56,23 +56,21 @@ public class WebContextSeed
|
|||||||
string imagePath = Path.Combine(webroot, "images");
|
string imagePath = Path.Combine(webroot, "images");
|
||||||
string[] imageFiles = Directory.GetFiles(imagePath).Select(file => Path.GetFileName(file)).ToArray();
|
string[] imageFiles = Directory.GetFiles(imagePath).Select(file => Path.GetFileName(file)).ToArray();
|
||||||
|
|
||||||
using (ZipArchive zip = ZipFile.Open(imagesZipFile, ZipArchiveMode.Read))
|
using ZipArchive zip = ZipFile.Open(imagesZipFile, ZipArchiveMode.Read);
|
||||||
|
foreach (ZipArchiveEntry entry in zip.Entries)
|
||||||
{
|
{
|
||||||
foreach (ZipArchiveEntry entry in zip.Entries)
|
if (imageFiles.Contains(entry.Name))
|
||||||
{
|
{
|
||||||
if (imageFiles.Contains(entry.Name))
|
string destinationFilename = Path.Combine(imagePath, entry.Name);
|
||||||
|
if (File.Exists(destinationFilename))
|
||||||
{
|
{
|
||||||
string destinationFilename = Path.Combine(imagePath, entry.Name);
|
File.Delete(destinationFilename);
|
||||||
if (File.Exists(destinationFilename))
|
|
||||||
{
|
|
||||||
File.Delete(destinationFilename);
|
|
||||||
}
|
|
||||||
entry.ExtractToFile(destinationFilename);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
log.Warning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
|
|
||||||
}
|
}
|
||||||
|
entry.ExtractToFile(destinationFilename);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Warning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,23 +39,21 @@ public class WebContextSeed
|
|||||||
}
|
}
|
||||||
string[] imageFiles = Directory.GetFiles(imagePath).Select(file => Path.GetFileName(file)).ToArray();
|
string[] imageFiles = Directory.GetFiles(imagePath).Select(file => Path.GetFileName(file)).ToArray();
|
||||||
|
|
||||||
using (ZipArchive zip = ZipFile.Open(imagesZipFile, ZipArchiveMode.Read))
|
using ZipArchive zip = ZipFile.Open(imagesZipFile, ZipArchiveMode.Read);
|
||||||
|
foreach (ZipArchiveEntry entry in zip.Entries)
|
||||||
{
|
{
|
||||||
foreach (ZipArchiveEntry entry in zip.Entries)
|
if (!imageFiles.Contains(entry.Name))
|
||||||
{
|
{
|
||||||
if (!imageFiles.Contains(entry.Name))
|
string destinationFilename = Path.Combine(imagePath, entry.Name);
|
||||||
|
if (File.Exists(destinationFilename))
|
||||||
{
|
{
|
||||||
string destinationFilename = Path.Combine(imagePath, entry.Name);
|
File.Delete(destinationFilename);
|
||||||
if (File.Exists(destinationFilename))
|
|
||||||
{
|
|
||||||
File.Delete(destinationFilename);
|
|
||||||
}
|
|
||||||
entry.ExtractToFile(destinationFilename);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
log.LogWarning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
|
|
||||||
}
|
}
|
||||||
|
entry.ExtractToFile(destinationFilename);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.LogWarning("Skipped file '{FileName}' in zipfile '{ZipFileName}'", entry.Name, imagesZipFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user