@ -0,0 +1,32 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using Microsoft.AspNetCore.Mvc; | |||
using System.IO; | |||
using Microsoft.AspNetCore.Hosting; | |||
// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers | |||
{ | |||
[Route("api/v1/[controller]")] | |||
public class PicController : Controller | |||
{ | |||
private readonly IHostingEnvironment _env; | |||
public PicController(IHostingEnvironment env) | |||
{ | |||
_env = env; | |||
} | |||
[HttpGet("{id}")] | |||
// GET: /<controller>/ | |||
public IActionResult GetImage(int id) | |||
{ | |||
var webRoot = _env.WebRootPath; | |||
var path = Path.Combine(webRoot, id + ".png"); | |||
Byte[] b = System.IO.File.ReadAllBytes(path); | |||
return File(b, "image/png"); | |||
} | |||
} | |||
} |
@ -1,360 +0,0 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using Microsoft.AspNetCore.Authorization; | |||
using Microsoft.AspNetCore.Identity; | |||
using Microsoft.AspNetCore.Mvc; | |||
using Microsoft.Extensions.Logging; | |||
using eShopOnContainers.Identity.Models; | |||
using eShopOnContainers.Identity.Models.ManageViewModels; | |||
using eShopOnContainers.Identity.Services; | |||
namespace eShopOnContainers.Identity.Controllers | |||
{ | |||
[Authorize] | |||
public class ManageController : Controller | |||
{ | |||
private readonly UserManager<ApplicationUser> _userManager; | |||
private readonly SignInManager<ApplicationUser> _signInManager; | |||
private readonly IEmailSender _emailSender; | |||
private readonly ISmsSender _smsSender; | |||
private readonly ILogger _logger; | |||
public ManageController( | |||
UserManager<ApplicationUser> userManager, | |||
SignInManager<ApplicationUser> signInManager, | |||
IEmailSender emailSender, | |||
ISmsSender smsSender, | |||
ILoggerFactory loggerFactory) | |||
{ | |||
_userManager = userManager; | |||
_signInManager = signInManager; | |||
_emailSender = emailSender; | |||
_smsSender = smsSender; | |||
_logger = loggerFactory.CreateLogger<ManageController>(); | |||
} | |||
// | |||
// GET: /Manage/Index | |||
[HttpGet] | |||
public async Task<IActionResult> Index(ManageMessageId? message = null) | |||
{ | |||
ViewData["StatusMessage"] = | |||
message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." | |||
: message == ManageMessageId.SetPasswordSuccess ? "Your password has been set." | |||
: message == ManageMessageId.SetTwoFactorSuccess ? "Your two-factor authentication provider has been set." | |||
: message == ManageMessageId.Error ? "An error has occurred." | |||
: message == ManageMessageId.AddPhoneSuccess ? "Your phone number was added." | |||
: message == ManageMessageId.RemovePhoneSuccess ? "Your phone number was removed." | |||
: ""; | |||
var user = await GetCurrentUserAsync(); | |||
if (user == null) | |||
{ | |||
return View("Error"); | |||
} | |||
var model = new IndexViewModel | |||
{ | |||
HasPassword = await _userManager.HasPasswordAsync(user), | |||
PhoneNumber = await _userManager.GetPhoneNumberAsync(user), | |||
TwoFactor = await _userManager.GetTwoFactorEnabledAsync(user), | |||
Logins = await _userManager.GetLoginsAsync(user), | |||
BrowserRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user) | |||
}; | |||
return View(model); | |||
} | |||
// | |||
// POST: /Manage/RemoveLogin | |||
[HttpPost] | |||
[ValidateAntiForgeryToken] | |||
public async Task<IActionResult> RemoveLogin(RemoveLoginViewModel account) | |||
{ | |||
ManageMessageId? message = ManageMessageId.Error; | |||
var user = await GetCurrentUserAsync(); | |||
if (user != null) | |||
{ | |||
var result = await _userManager.RemoveLoginAsync(user, account.LoginProvider, account.ProviderKey); | |||
if (result.Succeeded) | |||
{ | |||
await _signInManager.SignInAsync(user, isPersistent: false); | |||
message = ManageMessageId.RemoveLoginSuccess; | |||
} | |||
} | |||
return RedirectToAction(nameof(ManageLogins), new { Message = message }); | |||
} | |||
// | |||
// GET: /Manage/AddPhoneNumber | |||
public IActionResult AddPhoneNumber() | |||
{ | |||
return View(); | |||
} | |||
// | |||
// POST: /Manage/AddPhoneNumber | |||
[HttpPost] | |||
[ValidateAntiForgeryToken] | |||
public async Task<IActionResult> AddPhoneNumber(AddPhoneNumberViewModel model) | |||
{ | |||
if (!ModelState.IsValid) | |||
{ | |||
return View(model); | |||
} | |||
// Generate the token and send it | |||
var user = await GetCurrentUserAsync(); | |||
if (user == null) | |||
{ | |||
return View("Error"); | |||
} | |||
var code = await _userManager.GenerateChangePhoneNumberTokenAsync(user, model.PhoneNumber); | |||
await _smsSender.SendSmsAsync(model.PhoneNumber, "Your security code is: " + code); | |||
return RedirectToAction(nameof(VerifyPhoneNumber), new { PhoneNumber = model.PhoneNumber }); | |||
} | |||
// | |||
// POST: /Manage/EnableTwoFactorAuthentication | |||
[HttpPost] | |||
[ValidateAntiForgeryToken] | |||
public async Task<IActionResult> EnableTwoFactorAuthentication() | |||
{ | |||
var user = await GetCurrentUserAsync(); | |||
if (user != null) | |||
{ | |||
await _userManager.SetTwoFactorEnabledAsync(user, true); | |||
await _signInManager.SignInAsync(user, isPersistent: false); | |||
_logger.LogInformation(1, "User enabled two-factor authentication."); | |||
} | |||
return RedirectToAction(nameof(Index), "Manage"); | |||
} | |||
// | |||
// POST: /Manage/DisableTwoFactorAuthentication | |||
[HttpPost] | |||
[ValidateAntiForgeryToken] | |||
public async Task<IActionResult> DisableTwoFactorAuthentication() | |||
{ | |||
var user = await GetCurrentUserAsync(); | |||
if (user != null) | |||
{ | |||
await _userManager.SetTwoFactorEnabledAsync(user, false); | |||
await _signInManager.SignInAsync(user, isPersistent: false); | |||
_logger.LogInformation(2, "User disabled two-factor authentication."); | |||
} | |||
return RedirectToAction(nameof(Index), "Manage"); | |||
} | |||
// | |||
// GET: /Manage/VerifyPhoneNumber | |||
[HttpGet] | |||
public async Task<IActionResult> VerifyPhoneNumber(string phoneNumber) | |||
{ | |||
var user = await GetCurrentUserAsync(); | |||
if (user == null) | |||
{ | |||
return View("Error"); | |||
} | |||
var code = await _userManager.GenerateChangePhoneNumberTokenAsync(user, phoneNumber); | |||
// Send an SMS to verify the phone number | |||
return phoneNumber == null ? View("Error") : View(new VerifyPhoneNumberViewModel { PhoneNumber = phoneNumber }); | |||
} | |||
// | |||
// POST: /Manage/VerifyPhoneNumber | |||
[HttpPost] | |||
[ValidateAntiForgeryToken] | |||
public async Task<IActionResult> VerifyPhoneNumber(VerifyPhoneNumberViewModel model) | |||
{ | |||
if (!ModelState.IsValid) | |||
{ | |||
return View(model); | |||
} | |||
var user = await GetCurrentUserAsync(); | |||
if (user != null) | |||
{ | |||
var result = await _userManager.ChangePhoneNumberAsync(user, model.PhoneNumber, model.Code); | |||
if (result.Succeeded) | |||
{ | |||
await _signInManager.SignInAsync(user, isPersistent: false); | |||
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.AddPhoneSuccess }); | |||
} | |||
} | |||
// If we got this far, something failed, redisplay the form | |||
ModelState.AddModelError(string.Empty, "Failed to verify phone number"); | |||
return View(model); | |||
} | |||
// | |||
// POST: /Manage/RemovePhoneNumber | |||
[HttpPost] | |||
[ValidateAntiForgeryToken] | |||
public async Task<IActionResult> RemovePhoneNumber() | |||
{ | |||
var user = await GetCurrentUserAsync(); | |||
if (user != null) | |||
{ | |||
var result = await _userManager.SetPhoneNumberAsync(user, null); | |||
if (result.Succeeded) | |||
{ | |||
await _signInManager.SignInAsync(user, isPersistent: false); | |||
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.RemovePhoneSuccess }); | |||
} | |||
} | |||
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); | |||
} | |||
// | |||
// GET: /Manage/ChangePassword | |||
[HttpGet] | |||
public IActionResult ChangePassword() | |||
{ | |||
return View(); | |||
} | |||
// | |||
// POST: /Manage/ChangePassword | |||
[HttpPost] | |||
[ValidateAntiForgeryToken] | |||
public async Task<IActionResult> ChangePassword(ChangePasswordViewModel model) | |||
{ | |||
if (!ModelState.IsValid) | |||
{ | |||
return View(model); | |||
} | |||
var user = await GetCurrentUserAsync(); | |||
if (user != null) | |||
{ | |||
var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); | |||
if (result.Succeeded) | |||
{ | |||
await _signInManager.SignInAsync(user, isPersistent: false); | |||
_logger.LogInformation(3, "User changed their password successfully."); | |||
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.ChangePasswordSuccess }); | |||
} | |||
AddErrors(result); | |||
return View(model); | |||
} | |||
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); | |||
} | |||
// | |||
// GET: /Manage/SetPassword | |||
[HttpGet] | |||
public IActionResult SetPassword() | |||
{ | |||
return View(); | |||
} | |||
// | |||
// POST: /Manage/SetPassword | |||
[HttpPost] | |||
[ValidateAntiForgeryToken] | |||
public async Task<IActionResult> SetPassword(SetPasswordViewModel model) | |||
{ | |||
if (!ModelState.IsValid) | |||
{ | |||
return View(model); | |||
} | |||
var user = await GetCurrentUserAsync(); | |||
if (user != null) | |||
{ | |||
var result = await _userManager.AddPasswordAsync(user, model.NewPassword); | |||
if (result.Succeeded) | |||
{ | |||
await _signInManager.SignInAsync(user, isPersistent: false); | |||
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetPasswordSuccess }); | |||
} | |||
AddErrors(result); | |||
return View(model); | |||
} | |||
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); | |||
} | |||
//GET: /Manage/ManageLogins | |||
[HttpGet] | |||
public async Task<IActionResult> ManageLogins(ManageMessageId? message = null) | |||
{ | |||
ViewData["StatusMessage"] = | |||
message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed." | |||
: message == ManageMessageId.AddLoginSuccess ? "The external login was added." | |||
: message == ManageMessageId.Error ? "An error has occurred." | |||
: ""; | |||
var user = await GetCurrentUserAsync(); | |||
if (user == null) | |||
{ | |||
return View("Error"); | |||
} | |||
var userLogins = await _userManager.GetLoginsAsync(user); | |||
var otherLogins = _signInManager.GetExternalAuthenticationSchemes().Where(auth => userLogins.All(ul => auth.AuthenticationScheme != ul.LoginProvider)).ToList(); | |||
ViewData["ShowRemoveButton"] = user.PasswordHash != null || userLogins.Count > 1; | |||
return View(new ManageLoginsViewModel | |||
{ | |||
CurrentLogins = userLogins, | |||
OtherLogins = otherLogins | |||
}); | |||
} | |||
// | |||
// POST: /Manage/LinkLogin | |||
[HttpPost] | |||
[ValidateAntiForgeryToken] | |||
public IActionResult LinkLogin(string provider) | |||
{ | |||
// Request a redirect to the external login provider to link a login for the current user | |||
var redirectUrl = Url.Action("LinkLoginCallback", "Manage"); | |||
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); | |||
return Challenge(properties, provider); | |||
} | |||
// | |||
// GET: /Manage/LinkLoginCallback | |||
[HttpGet] | |||
public async Task<ActionResult> LinkLoginCallback() | |||
{ | |||
var user = await GetCurrentUserAsync(); | |||
if (user == null) | |||
{ | |||
return View("Error"); | |||
} | |||
var info = await _signInManager.GetExternalLoginInfoAsync(await _userManager.GetUserIdAsync(user)); | |||
if (info == null) | |||
{ | |||
return RedirectToAction(nameof(ManageLogins), new { Message = ManageMessageId.Error }); | |||
} | |||
var result = await _userManager.AddLoginAsync(user, info); | |||
var message = result.Succeeded ? ManageMessageId.AddLoginSuccess : ManageMessageId.Error; | |||
return RedirectToAction(nameof(ManageLogins), new { Message = message }); | |||
} | |||
#region Helpers | |||
private void AddErrors(IdentityResult result) | |||
{ | |||
foreach (var error in result.Errors) | |||
{ | |||
ModelState.AddModelError(string.Empty, error.Description); | |||
} | |||
} | |||
public enum ManageMessageId | |||
{ | |||
AddPhoneSuccess, | |||
AddLoginSuccess, | |||
ChangePasswordSuccess, | |||
SetTwoFactorSuccess, | |||
SetPasswordSuccess, | |||
RemoveLoginSuccess, | |||
RemovePhoneSuccess, | |||
Error | |||
} | |||
private Task<ApplicationUser> GetCurrentUserAsync() | |||
{ | |||
return _userManager.GetUserAsync(HttpContext.User); | |||
} | |||
#endregion | |||
} | |||
} |
@ -0,0 +1,12 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace eShopOnContainers.Identity.Services | |||
{ | |||
public interface IRedirectService | |||
{ | |||
string ExtractRedirectUriFromReturnUrl(string url); | |||
} | |||
} |
@ -0,0 +1,36 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text.RegularExpressions; | |||
using System.Threading.Tasks; | |||
namespace eShopOnContainers.Identity.Services | |||
{ | |||
public class RedirectService : IRedirectService | |||
{ | |||
public string ExtractRedirectUriFromReturnUrl(string url) | |||
{ | |||
var result = ""; | |||
var decodedUrl = System.Net.WebUtility.HtmlDecode(url); | |||
var results = Regex.Split(decodedUrl, "redirect_uri="); | |||
if (results.Length < 2) | |||
return ""; | |||
result = results[1]; | |||
var splitKey = ""; | |||
if (result.Contains("signin-oidc")) | |||
splitKey = "signin-oidc"; | |||
else | |||
splitKey = "scope"; | |||
results = Regex.Split(result, splitKey); | |||
if (results.Length < 2) | |||
return ""; | |||
result = results[0]; | |||
return result.Replace("%3A", ":").Replace("%2F", "/").Replace("&", ""); | |||
} | |||
} | |||
} |
@ -1,118 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Ordering.Application.Commands | |||
{ | |||
using Domain.Repositories; | |||
using MediatR; | |||
using System.Linq; | |||
using System; | |||
using System.Threading.Tasks; | |||
using Domain; | |||
public class NewOrderRequestHandler | |||
: IAsyncRequestHandler<NewOrderRequest, bool> | |||
{ | |||
private readonly IBuyerRepository _buyerRepository; | |||
private readonly IOrderRepository _orderRepository; | |||
public NewOrderRequestHandler(IBuyerRepository buyerRepository,IOrderRepository orderRepository) | |||
{ | |||
if (buyerRepository == null) | |||
{ | |||
throw new ArgumentNullException(nameof(buyerRepository)); | |||
} | |||
if (orderRepository == null) | |||
{ | |||
throw new ArgumentNullException(nameof(orderRepository)); | |||
} | |||
_buyerRepository = buyerRepository; | |||
_orderRepository = orderRepository; | |||
} | |||
public async Task<bool> Handle(NewOrderRequest message) | |||
{ | |||
//find buyer/payment or add a new one buyer/payment | |||
var buyer = await _buyerRepository.FindAsync(message.Buyer); | |||
if (buyer == null) | |||
{ | |||
buyer = CreateBuyer(message); | |||
} | |||
var payment = GetExistingPaymentOrAddANewOne(buyer, message); | |||
await _buyerRepository.UnitOfWork | |||
.SaveChangesAsync(); | |||
//create order for buyer and payment method | |||
var order = CreateOrder(buyer.Id, payment.Id, 0); | |||
order.SetAddress( new Address() | |||
{ | |||
City = message.City, | |||
State = message.State, | |||
Street = message.Street, | |||
ZipCode = message.ZipCode | |||
}); | |||
foreach (var item in message.OrderItems) | |||
{ | |||
order.AddOrderItem(item); | |||
} | |||
_orderRepository.Add(order); | |||
var result = await _orderRepository.UnitOfWork | |||
.SaveChangesAsync(); | |||
return result > 0; | |||
} | |||
Payment GetExistingPaymentOrAddANewOne(Buyer buyer, NewOrderRequest message) | |||
{ | |||
Payment payment = PaymentAlreadyExist(buyer, message); | |||
if (payment == null) | |||
{ | |||
payment = CreatePayment(message); | |||
buyer.Payments.Add(payment); | |||
} | |||
return payment; | |||
} | |||
Payment PaymentAlreadyExist(Domain.Buyer buyer, NewOrderRequest message) | |||
{ | |||
return buyer.Payments | |||
.SingleOrDefault(p => | |||
{ | |||
return p.CardHolderName == message.CardHolderName | |||
&& | |||
p.CardNumber == message.CardNumber | |||
&& | |||
p.Expiration == message.CardExpiration | |||
&& | |||
p.SecurityNumber == message.CardSecurityNumber; | |||
}); | |||
} | |||
Buyer CreateBuyer(NewOrderRequest message) | |||
{ | |||
return _buyerRepository.Add( | |||
new Buyer(message.Buyer)); | |||
} | |||
Order CreateOrder(int buyerId, int paymentId, int addressId) | |||
{ | |||
return new Order(buyerId, paymentId); | |||
} | |||
Payment CreatePayment(NewOrderRequest message) | |||
{ | |||
return new Payment(message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration, message.CardTypeId); | |||
} | |||
} | |||
} |
@ -1,48 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Ordering.Application.Commands | |||
{ | |||
using System; | |||
using MediatR; | |||
using Domain; | |||
using System.Collections; | |||
using System.Collections.Generic; | |||
public class NewOrderRequest | |||
:IAsyncRequest<bool> | |||
{ | |||
private readonly List<OrderItem> _orderItems; | |||
public string City { get; set; } | |||
public string Street { get; set; } | |||
public string State { get; set; } | |||
public string Country { get; set; } | |||
public string ZipCode { get; set; } | |||
public string CardNumber { get; set; } | |||
public string CardHolderName { get; set; } | |||
public DateTime CardExpiration { get; set; } | |||
public string CardSecurityNumber { get; set; } | |||
public int CardTypeId { get; set; } | |||
public string Buyer { get; set; } | |||
public IEnumerable<OrderItem> OrderItems => _orderItems; | |||
public void AddOrderItem(OrderItem item) | |||
{ | |||
_orderItems.Add(item); | |||
} | |||
public NewOrderRequest() | |||
{ | |||
_orderItems = new List<OrderItem>(); | |||
} | |||
} | |||
} |
@ -1,34 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Ordering.Application.Decorators | |||
{ | |||
using Extensions.Logging; | |||
using MediatR; | |||
using System.Threading.Tasks; | |||
public class LogDecorator<TRequest, TResponse> | |||
: IAsyncRequestHandler<TRequest, TResponse> | |||
where TRequest : IAsyncRequest<TResponse> | |||
{ | |||
private readonly IAsyncRequestHandler<TRequest, TResponse> _inner; | |||
private readonly ILogger<LogDecorator<TRequest, TResponse>> _logger; | |||
public LogDecorator( | |||
IAsyncRequestHandler<TRequest, TResponse> inner, | |||
ILogger<LogDecorator<TRequest, TResponse>> logger) | |||
{ | |||
_inner = inner; | |||
_logger = logger; | |||
} | |||
public async Task<TResponse> Handle(TRequest message) | |||
{ | |||
_logger.LogInformation($"Executing command {_inner.GetType().FullName}"); | |||
var response = await _inner.Handle(message); | |||
_logger.LogInformation($"Succedded executed command {_inner.GetType().FullName}"); | |||
return response; | |||
} | |||
} | |||
} |
@ -1,19 +0,0 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||
<PropertyGroup> | |||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion> | |||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> | |||
</PropertyGroup> | |||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" /> | |||
<PropertyGroup Label="Globals"> | |||
<ProjectGuid>4193caa3-a1c3-4818-a06f-a2d85fde77e7</ProjectGuid> | |||
<RootNamespace>Microsoft.eShopOnContainers.Services.Ordering.Application</RootNamespace> | |||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath> | |||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> | |||
<TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion> | |||
</PropertyGroup> | |||
<PropertyGroup> | |||
<SchemaVersion>2.0</SchemaVersion> | |||
</PropertyGroup> | |||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" /> | |||
</Project> |
@ -1,19 +0,0 @@ | |||
using System.Reflection; | |||
using System.Runtime.CompilerServices; | |||
using System.Runtime.InteropServices; | |||
// General Information about an assembly is controlled through the following | |||
// set of attributes. Change these attribute values to modify the information | |||
// associated with an assembly. | |||
[assembly: AssemblyConfiguration("")] | |||
[assembly: AssemblyCompany("")] | |||
[assembly: AssemblyProduct("Ordering.Application")] | |||
[assembly: AssemblyTrademark("")] | |||
// Setting ComVisible to false makes the types in this assembly not visible | |||
// to COM components. If you need to access a type in this assembly from | |||
// COM, set the ComVisible attribute to true on that type. | |||
[assembly: ComVisible(false)] | |||
// The following GUID is for the ID of the typelib if this project is exposed to COM | |||
[assembly: Guid("4193caa3-a1c3-4818-a06f-a2d85fde77e7")] |
@ -1,13 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Ordering.Application.Queries | |||
{ | |||
using System.Threading.Tasks; | |||
public interface IOrderQueries | |||
{ | |||
Task<dynamic> GetOrder(int id); | |||
Task<dynamic> GetOrders(); | |||
Task<dynamic> GetCardTypes(); | |||
} | |||
} |
@ -1,128 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Ordering.Application.Queries | |||
{ | |||
using Dapper; | |||
using Microsoft.Extensions.Configuration; | |||
using System.Data.SqlClient; | |||
using System.Threading.Tasks; | |||
using System; | |||
using System.Dynamic; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
public class OrderQueries | |||
: IOrderQueries | |||
{ | |||
private string _connectionString = string.Empty; | |||
public OrderQueries(IConfiguration configuration) | |||
{ | |||
_connectionString = configuration["ConnectionString"]; | |||
} | |||
public async Task<dynamic> GetOrder(int id) | |||
{ | |||
using (var connection = new SqlConnection(_connectionString)) | |||
{ | |||
connection.Open(); | |||
var result = await connection.QueryAsync<dynamic>( | |||
@"select o.[Id] as ordernumber,o.OrderDate as date, os.Name as status, | |||
oi.ProductName as productname, oi.Units as units, oi.UnitPrice as unitprice, oi.PictureUrl as pictureurl, | |||
oa.Street as street, oa.City as city, oa.Country as country, oa.State as state, oa.ZipCode as zipcode | |||
FROM ordering.Orders o | |||
LEFT JOIN ordering.Orderitems oi ON o.Id = oi.orderid | |||
LEFT JOIN ordering.orderstatus os on o.StatusId = os.Id | |||
LEFT JOIN ordering.address oa on o.ShippingAddressId = oa.Id | |||
WHERE o.Id=@id" | |||
, new { id } | |||
); | |||
if (result.AsList().Count == 0) | |||
throw new KeyNotFoundException(); | |||
return MapOrderItems(result); | |||
} | |||
} | |||
public async Task<dynamic> GetOrders() | |||
{ | |||
using (var connection = new SqlConnection(_connectionString)) | |||
{ | |||
connection.Open(); | |||
return await connection.QueryAsync<dynamic>(@"SELECT o.[Id] as ordernumber,o.[OrderDate] as [date],os.[Name] as [status],SUM(oi.units*oi.unitprice) as total | |||
FROM [ordering].[Orders] o | |||
LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid | |||
LEFT JOIN[ordering].[orderstatus] os on o.StatusId = os.Id | |||
GROUP BY o.[Id], o.[OrderDate], os.[Name]"); | |||
} | |||
} | |||
public async Task<dynamic> GetCardTypes() | |||
{ | |||
using (var connection = new SqlConnection(_connectionString)) | |||
{ | |||
connection.Open(); | |||
return await connection.QueryAsync<dynamic>("SELECT * FROM ordering.cardtypes"); | |||
} | |||
} | |||
private dynamic MapOrderItems(dynamic result) | |||
{ | |||
dynamic order = new ExpandoObject(); | |||
order.ordernumber = result[0].ordernumber; | |||
order.date = result[0].date; | |||
order.status = result[0].status; | |||
order.street = result[0].street; | |||
order.city = result[0].city; | |||
order.zipcode = result[0].zipcode; | |||
order.country = result[0].country; | |||
order.orderitems = new List<dynamic>(); | |||
order.total = 0; | |||
foreach (dynamic item in result) | |||
{ | |||
dynamic orderitem = new ExpandoObject(); | |||
orderitem.productname = item.productname; | |||
orderitem.units = item.units; | |||
orderitem.unitprice = item.unitprice; | |||
orderitem.pictureurl = item.pictureurl; | |||
order.total += item.units * item.unitprice; | |||
order.orderitems.Add(orderitem); | |||
} | |||
return order; | |||
} | |||
//TODO/CCE: try to use this method instead actual. | |||
//private object MapOrderItems(dynamic result) | |||
//{ | |||
// IEnumerable<dynamic> items = (result as System.Collections.IEnumerable).Cast<dynamic>(); | |||
// var order = new | |||
// { | |||
// ordernumber = result[0].ordernumbe, | |||
// date = result[0].date, | |||
// status = result[0].status, | |||
// street = result[0].street, | |||
// city = result[0].city, | |||
// zipcode = result[0].zipcode, | |||
// country = result[0].country, | |||
// total = items.Select(r => (int)r.units * (int)r.unitprice).Sum(), | |||
// orderItems = items.Select(r => new | |||
// { | |||
// productname = r.productname, | |||
// units = r.units, | |||
// unitprice = r.unitprice, | |||
// pictureurl = r.pictureurl | |||
// }) | |||
// }; | |||
// return order; | |||
//} | |||
} | |||
} |
@ -1,21 +0,0 @@ | |||
{ | |||
"version": "1.0.0-*", | |||
"dependencies": { | |||
"NETStandard.Library": "1.6.0", | |||
"MediatR": "2.1.0", | |||
"Dapper": "1.50.2", | |||
"System.Dynamic.Runtime": "4.0.11", | |||
"Microsoft.CSharp": "4.0.1", | |||
"Microsoft.Extensions.Configuration": "1.0.0", | |||
"System.Data.SqlClient": "4.1.0", | |||
"Microsoft.Extensions.Logging.Abstractions": "1.0.0", | |||
"Ordering.Domain": "1.0.0-*" | |||
}, | |||
"frameworks": { | |||
"netstandard1.6": { | |||
"imports": "dnxcore50" | |||
} | |||
} | |||
} |
@ -0,0 +1,63 @@ | |||
| |||
//namespace FunctionalTests.Services.Basket | |||
//{ | |||
// using Microsoft.eShopOnContainers.Services.Basket.API; | |||
// using Microsoft.eShopOnContainers.Services.Basket.API.Model; | |||
// using Microsoft.Extensions.Logging; | |||
// using Microsoft.Extensions.Options; | |||
// using System.Collections.Generic; | |||
// using System.Threading.Tasks; | |||
// using Xunit; | |||
// public class RedisBasketRepositoryTests | |||
// { | |||
// [Fact] | |||
// public async Task UpdateBasket_return_and_add_basket() | |||
// { | |||
// var redisBasketRepository = BuildBasketRepository(); | |||
// var basket = await redisBasketRepository.UpdateBasket(new CustomerBasket("customerId") | |||
// { | |||
// BuyerId = "buyerId", | |||
// Items = BuildBasketItems() | |||
// }); | |||
// Assert.NotNull(basket); | |||
// Assert.Equal(1, basket.Items.Count); | |||
// } | |||
// [Fact] | |||
// public async Task GetBasket_return_existing_basket() | |||
// { | |||
// } | |||
// RedisBasketRepository BuildBasketRepository() | |||
// { | |||
// var loggerFactory = new LoggerFactory(); | |||
// var options = Options.Create<BasketSettings>(new BasketSettings() | |||
// { | |||
// ConnectionString = "127.0.0.1" | |||
// }); | |||
// return new RedisBasketRepository(options, loggerFactory); | |||
// } | |||
// List<BasketItem> BuildBasketItems() | |||
// { | |||
// return new List<BasketItem>() | |||
// { | |||
// new BasketItem() | |||
// { | |||
// Id = "basketId", | |||
// PictureUrl = "pictureurl", | |||
// ProductId = "productId", | |||
// ProductName = "productName", | |||
// Quantity = 1, | |||
// UnitPrice = 1 | |||
// } | |||
// }; | |||
// } | |||
// } | |||
//} |
@ -1,136 +0,0 @@ | |||
using System; | |||
using Xunit; | |||
using System.Threading.Tasks; | |||
using Moq; | |||
using MediatR; | |||
using Microsoft.eShopOnContainers.Services.Ordering.API.Controllers; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Api.Application.Commands; | |||
using Microsoft.AspNetCore.Mvc; | |||
using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Api.Application.Queries; | |||
using System.Collections.Generic; | |||
namespace UnitTest.Ordering.Controllers | |||
{ | |||
public class OrderControllerTest | |||
{ | |||
private readonly Mock<IMediator> _mediatorMock; | |||
private readonly Mock<IIdentityService> _identityMock; | |||
private readonly Mock<IOrderQueries> _queriesMock; | |||
private readonly string _userIdentity; | |||
public OrderControllerTest() | |||
{ | |||
//Mocks; | |||
_mediatorMock = new Mock<IMediator>(); | |||
_identityMock = new Mock<IIdentityService>(); | |||
_queriesMock = new Mock<IOrderQueries>(); | |||
_userIdentity = Guid.NewGuid().ToString(); | |||
} | |||
[Fact] | |||
public async Task AddOrder_ReturnsBadRequestResult_WhenPersitenceOperationFails() | |||
{ | |||
// Arrange | |||
var orderRequest = new object() as IAsyncRequest<bool>; | |||
_mediatorMock.Setup(mediator => mediator.SendAsync(OrderFakeNotExpired())) | |||
.Returns(Task.FromResult(false)); | |||
_identityMock.Setup(identity => identity.GetUserIdentity()) | |||
.Returns(Guid.NewGuid().ToString()); | |||
var controller = new OrdersController(_mediatorMock.Object, _queriesMock.Object, _identityMock.Object); | |||
// Act | |||
var badRequestResult = await controller.AddOrder(OrderFakeNotExpired()); | |||
// Assert | |||
Assert.IsType<BadRequestResult>(badRequestResult); | |||
} | |||
[Fact] | |||
public async Task GetOrder_ReturnsNotFound_WhenItemNotFound() | |||
{ | |||
// Arrange | |||
_queriesMock.Setup(queries => queries.GetOrder(1)) | |||
.Throws(new KeyNotFoundException()); | |||
var controller = new OrdersController(_mediatorMock.Object, _queriesMock.Object, _identityMock.Object); | |||
// Act | |||
var notFoundResult = await controller.GetOrder(1); | |||
// Assert | |||
Assert.IsType<NotFoundResult>(notFoundResult); | |||
} | |||
[Fact] | |||
public async Task GetOrder_ReturnsOkObjecResult_WheItemFound() | |||
{ | |||
// Arrange | |||
_queriesMock.Setup(queries => queries.GetOrder(1)) | |||
.Returns(Task.FromResult(new object())); | |||
var controller = new OrdersController(_mediatorMock.Object, _queriesMock.Object, _identityMock.Object); | |||
// Act | |||
var OkObjectResult = await controller.GetOrder(1); | |||
// Assert | |||
Assert.IsType<OkObjectResult>(OkObjectResult); | |||
} | |||
[Fact] | |||
public async Task GetOrders_ReturnsOkObjectResult() | |||
{ | |||
// Arrange | |||
_queriesMock.Setup(queries => queries.GetOrders()) | |||
.Returns(Task.FromResult(new object())); | |||
var controller = new OrdersController(_mediatorMock.Object, _queriesMock.Object, _identityMock.Object); | |||
// Act | |||
var OkObjectResult = await controller.GetOrders(); | |||
// Assert | |||
Assert.IsType<OkObjectResult>(OkObjectResult); | |||
} | |||
[Fact] | |||
public async Task GetCardTypes() | |||
{ | |||
// Arrange | |||
_queriesMock.Setup(queries => queries.GetCardTypes()) | |||
.Returns(Task.FromResult(new object())); | |||
var controller = new OrdersController(_mediatorMock.Object, _queriesMock.Object, _identityMock.Object); | |||
// Act | |||
var OkObjectResult = await controller.GetCardTypes(); | |||
// Assert | |||
Assert.IsType<OkObjectResult>(OkObjectResult); | |||
} | |||
//Fakes | |||
private NewOrderRequest OrderFakeNotExpired() | |||
{ | |||
return new NewOrderRequest() | |||
{ | |||
CardTypeId = 1, | |||
CardExpiration = new DateTime(2020, 12, 12), | |||
Buyer = _userIdentity | |||
}; | |||
} | |||
private NewOrderRequest OrderFakeExpired() | |||
{ | |||
return new NewOrderRequest() | |||
{ | |||
CardTypeId = 1, | |||
CardExpiration = DateTime.Now.AddYears(-1) | |||
}; | |||
} | |||
} | |||
} |
@ -1,50 +0,0 @@ | |||
using System; | |||
using Microsoft.VisualStudio.TestTools.UnitTesting; | |||
using Xunit; | |||
using System.Threading.Tasks; | |||
using Moq; | |||
using MediatR; | |||
namespace UnitTests | |||
{ | |||
public class OrderControllerTest | |||
{ | |||
private readonly Mock<IMediator> _mock; | |||
public OrderControllerTest() | |||
{ | |||
//config mock; | |||
_mock = new Mock<IMediator>(); | |||
} | |||
[Fact] | |||
public async Task AddOrder_ReturnsBadRequestResult_WhenPersitenceOperationFails() | |||
{ | |||
//Add order: | |||
var orderRequest = new object() as IAsyncRequest<bool>; | |||
_mock.Setup(mediator => mediator.SendAsync(orderRequest)) | |||
.Returns(Task.FromResult(false)); | |||
// Arrange | |||
var controller = new OrdersController(mockRepo.Object); | |||
controller.ModelState.AddModelError("SessionName", "Required"); | |||
var newSession = new HomeController.NewSessionModel(); | |||
// Act | |||
var result = await controller.Index(newSession); | |||
// Assert | |||
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result); | |||
Assert.IsType<SerializableError>(badRequestResult.Value); | |||
} | |||
// Implement Fake method for mock. | |||
private MediatorMockForAddOrder() | |||
{ | |||
} | |||
} | |||
} |
@ -1,36 +0,0 @@ | |||
using System.Reflection; | |||
using System.Runtime.CompilerServices; | |||
using System.Runtime.InteropServices; | |||
// General Information about an assembly is controlled through the following | |||
// set of attributes. Change these attribute values to modify the information | |||
// associated with an assembly. | |||
[assembly: AssemblyTitle("UnitTests")] | |||
[assembly: AssemblyDescription("")] | |||
[assembly: AssemblyConfiguration("")] | |||
[assembly: AssemblyCompany("")] | |||
[assembly: AssemblyProduct("UnitTests")] | |||
[assembly: AssemblyCopyright("Copyright © 2016")] | |||
[assembly: AssemblyTrademark("")] | |||
[assembly: AssemblyCulture("")] | |||
// Setting ComVisible to false makes the types in this assembly not visible | |||
// to COM components. If you need to access a type in this assembly from | |||
// COM, set the ComVisible attribute to true on that type. | |||
[assembly: ComVisible(false)] | |||
// The following GUID is for the ID of the typelib if this project is exposed to COM | |||
[assembly: Guid("ecbb8dc1-22ea-42d2-a45a-4ae800c73356")] | |||
// Version information for an assembly consists of the following four values: | |||
// | |||
// Major Version | |||
// Minor Version | |||
// Build Number | |||
// Revision | |||
// | |||
// You can specify all the values or you can default the Build and Revision Numbers | |||
// by using the '*' as shown below: | |||
// [assembly: AssemblyVersion("1.0.*")] | |||
[assembly: AssemblyVersion("1.0.0.0")] | |||
[assembly: AssemblyFileVersion("1.0.0.0")] |
@ -1,117 +0,0 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||
<PropertyGroup> | |||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> | |||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> | |||
<ProjectGuid>{ECBB8DC1-22EA-42D2-A45A-4AE800C73356}</ProjectGuid> | |||
<OutputType>Library</OutputType> | |||
<AppDesignerFolder>Properties</AppDesignerFolder> | |||
<RootNamespace>UnitTests</RootNamespace> | |||
<AssemblyName>UnitTests</AssemblyName> | |||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> | |||
<FileAlignment>512</FileAlignment> | |||
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> | |||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion> | |||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> | |||
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath> | |||
<IsCodedUITest>False</IsCodedUITest> | |||
<TestProjectType>UnitTest</TestProjectType> | |||
</PropertyGroup> | |||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | |||
<DebugSymbols>true</DebugSymbols> | |||
<DebugType>full</DebugType> | |||
<Optimize>false</Optimize> | |||
<OutputPath>bin\Debug\</OutputPath> | |||
<DefineConstants>DEBUG;TRACE</DefineConstants> | |||
<ErrorReport>prompt</ErrorReport> | |||
<WarningLevel>4</WarningLevel> | |||
</PropertyGroup> | |||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> | |||
<DebugType>pdbonly</DebugType> | |||
<Optimize>true</Optimize> | |||
<OutputPath>bin\Release\</OutputPath> | |||
<DefineConstants>TRACE</DefineConstants> | |||
<ErrorReport>prompt</ErrorReport> | |||
<WarningLevel>4</WarningLevel> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<Reference Include="Castle.Core, Version=3.3.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL"> | |||
<HintPath>..\..\..\..\packages\Castle.Core.3.3.3\lib\net45\Castle.Core.dll</HintPath> | |||
<Private>True</Private> | |||
</Reference> | |||
<Reference Include="MediatR, Version=2.1.0.0, Culture=neutral, processorArchitecture=MSIL"> | |||
<HintPath>..\..\..\..\packages\MediatR.2.1.0\lib\net45\MediatR.dll</HintPath> | |||
<Private>True</Private> | |||
</Reference> | |||
<Reference Include="Moq, Version=4.6.38.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL"> | |||
<HintPath>..\..\..\..\packages\Moq.4.6.38-alpha\lib\net45\Moq.dll</HintPath> | |||
<Private>True</Private> | |||
</Reference> | |||
<Reference Include="System" /> | |||
<Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> | |||
<HintPath>..\..\..\..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll</HintPath> | |||
<Private>True</Private> | |||
</Reference> | |||
<Reference Include="xunit.assert, Version=2.2.0.3444, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> | |||
<HintPath>..\..\..\..\packages\xunit.assert.2.2.0-beta4-build3444\lib\netstandard1.0\xunit.assert.dll</HintPath> | |||
<Private>True</Private> | |||
</Reference> | |||
<Reference Include="xunit.core, Version=2.2.0.3444, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> | |||
<HintPath>..\..\..\..\packages\xunit.extensibility.core.2.2.0-beta4-build3444\lib\net45\xunit.core.dll</HintPath> | |||
<Private>True</Private> | |||
</Reference> | |||
<Reference Include="xunit.execution.desktop, Version=2.2.0.3444, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL"> | |||
<HintPath>..\..\..\..\packages\xunit.extensibility.execution.2.2.0-beta4-build3444\lib\net45\xunit.execution.desktop.dll</HintPath> | |||
<Private>True</Private> | |||
</Reference> | |||
</ItemGroup> | |||
<Choose> | |||
<When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'"> | |||
<ItemGroup> | |||
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" /> | |||
</ItemGroup> | |||
</When> | |||
<Otherwise> | |||
<ItemGroup> | |||
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework" /> | |||
</ItemGroup> | |||
</Otherwise> | |||
</Choose> | |||
<ItemGroup> | |||
<Compile Include="Ordering\OrderControllerTest.cs" /> | |||
<Compile Include="Properties\AssemblyInfo.cs" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<Folder Include="Catalog\" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<None Include="packages.config" /> | |||
</ItemGroup> | |||
<Choose> | |||
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'"> | |||
<ItemGroup> | |||
<Reference Include="Microsoft.VisualStudio.QualityTools.CodedUITestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||
<Private>False</Private> | |||
</Reference> | |||
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||
<Private>False</Private> | |||
</Reference> | |||
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Extension, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||
<Private>False</Private> | |||
</Reference> | |||
<Reference Include="Microsoft.VisualStudio.TestTools.UITesting, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||
<Private>False</Private> | |||
</Reference> | |||
</ItemGroup> | |||
</When> | |||
</Choose> | |||
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" /> | |||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | |||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. | |||
Other similar extension points exist, see Microsoft.Common.targets. | |||
<Target Name="BeforeBuild"> | |||
</Target> | |||
<Target Name="AfterBuild"> | |||
</Target> | |||
--> | |||
</Project> |
@ -1,12 +0,0 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<packages> | |||
<package id="Castle.Core" version="3.3.3" targetFramework="net452" /> | |||
<package id="MediatR" version="2.1.0" targetFramework="net452" /> | |||
<package id="Moq" version="4.6.38-alpha" targetFramework="net452" /> | |||
<package id="xunit" version="2.2.0-beta4-build3444" targetFramework="net452" /> | |||
<package id="xunit.abstractions" version="2.0.1" targetFramework="net452" /> | |||
<package id="xunit.assert" version="2.2.0-beta4-build3444" targetFramework="net452" /> | |||
<package id="xunit.core" version="2.2.0-beta4-build3444" targetFramework="net452" /> | |||
<package id="xunit.extensibility.core" version="2.2.0-beta4-build3444" targetFramework="net452" /> | |||
<package id="xunit.extensibility.execution" version="2.2.0-beta4-build3444" targetFramework="net452" /> | |||
</packages> |