@ -0,0 +1,18 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Locations.API.IntegrationEvents.Events | |||||
{ | |||||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||||
using Microsoft.eShopOnContainers.Services.Locations.API.Model; | |||||
using System.Collections.Generic; | |||||
public class UserLocationUpdatedIntegrationEvent : IntegrationEvent | |||||
{ | |||||
public string UserId { get; private set; } | |||||
public List<UserLocationDetails> LocationList { get; private set; } | |||||
public UserLocationUpdatedIntegrationEvent(string userId, List<UserLocationDetails> locationList) | |||||
{ | |||||
UserId = userId; | |||||
LocationList = locationList; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,14 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Services.Locations.API.Model | |||||
{ | |||||
public class UserLocationDetails | |||||
{ | |||||
public int LocationId { get; set; } | |||||
public string Code { get; set; } | |||||
public string Description { get; set; } | |||||
} | |||||
} |
@ -0,0 +1,12 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Dto | |||||
{ | |||||
public class UserLocationDTO | |||||
{ | |||||
public string Id { get; set; } | |||||
public Guid UserId { get; set; } | |||||
public int LocationId { get; set; } | |||||
public DateTime UpdateDate { get; set; } | |||||
} | |||||
} |
@ -0,0 +1,26 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure | |||||
{ | |||||
using Microsoft.eShopOnContainers.Services.Marketing.API.Model; | |||||
using Microsoft.Extensions.Options; | |||||
using MongoDB.Driver; | |||||
public class MarketingReadDataContext | |||||
{ | |||||
private readonly IMongoDatabase _database = null; | |||||
public MarketingReadDataContext(IOptions<MarketingSettings> settings) | |||||
{ | |||||
var client = new MongoClient(settings.Value.MongoConnectionString); | |||||
if (client != null) | |||||
_database = client.GetDatabase(settings.Value.MongoDatabase); | |||||
} | |||||
public IMongoCollection<MarketingData> MarketingData | |||||
{ | |||||
get | |||||
{ | |||||
return _database.GetCollection<MarketingData>("MarketingReadDataModel"); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,11 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Repositories | |||||
{ | |||||
using Model; | |||||
using System.Threading.Tasks; | |||||
public interface IMarketingDataRepository | |||||
{ | |||||
Task<MarketingData> GetAsync(string userId); | |||||
Task UpdateLocationAsync(MarketingData marketingData); | |||||
} | |||||
} |
@ -0,0 +1,41 @@ | |||||
using Microsoft.eShopOnContainers.Services.Marketing.API.Model; | |||||
using Microsoft.Extensions.Options; | |||||
using MongoDB.Bson; | |||||
using MongoDB.Driver; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Repositories | |||||
{ | |||||
public class MarketingDataRepository | |||||
: IMarketingDataRepository | |||||
{ | |||||
private readonly MarketingReadDataContext _context; | |||||
public MarketingDataRepository(IOptions<MarketingSettings> settings) | |||||
{ | |||||
_context = new MarketingReadDataContext(settings); | |||||
} | |||||
public async Task<MarketingData> GetAsync(string userId) | |||||
{ | |||||
var filter = Builders<MarketingData>.Filter.Eq("UserId", userId); | |||||
return await _context.MarketingData | |||||
.Find(filter) | |||||
.FirstOrDefaultAsync(); | |||||
} | |||||
public async Task UpdateLocationAsync(MarketingData marketingData) | |||||
{ | |||||
var filter = Builders<MarketingData>.Filter.Eq("UserId", marketingData.UserId); | |||||
var update = Builders<MarketingData>.Update | |||||
.Set("Locations", marketingData.Locations) | |||||
.CurrentDate("UpdateDate"); | |||||
await _context.MarketingData | |||||
.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,18 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Marketing.API.IntegrationEvents.Events | |||||
{ | |||||
using Model; | |||||
using System.Collections.Generic; | |||||
using BuildingBlocks.EventBus.Events; | |||||
public class UserLocationUpdatedIntegrationEvent : IntegrationEvent | |||||
{ | |||||
public string UserId { get; private set; } | |||||
public List<UserLocationDetails> LocationList { get; private set; } | |||||
public UserLocationUpdatedIntegrationEvent(string userId, List<UserLocationDetails> locationList) | |||||
{ | |||||
UserId = userId; | |||||
LocationList = locationList; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,46 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Marketing.API.IntegrationEvents.Handlers | |||||
{ | |||||
using BuildingBlocks.EventBus.Abstractions; | |||||
using System.Threading.Tasks; | |||||
using Events; | |||||
using System; | |||||
using Infrastructure.Repositories; | |||||
using Model; | |||||
using System.Collections.Generic; | |||||
public class UserLocationUpdatedIntegrationEventHandler | |||||
: IIntegrationEventHandler<UserLocationUpdatedIntegrationEvent> | |||||
{ | |||||
private readonly IMarketingDataRepository _marketingDataRepository; | |||||
public UserLocationUpdatedIntegrationEventHandler(IMarketingDataRepository repository) | |||||
{ | |||||
_marketingDataRepository = repository ?? throw new ArgumentNullException(nameof(repository)); | |||||
} | |||||
public async Task Handle(UserLocationUpdatedIntegrationEvent @event) | |||||
{ | |||||
var userMarketingData = await _marketingDataRepository.GetAsync(@event.UserId); | |||||
userMarketingData = userMarketingData ?? | |||||
new MarketingData() { UserId = @event.UserId }; | |||||
userMarketingData.Locations = MapUpdatedUserLocations(@event.LocationList); | |||||
await _marketingDataRepository.UpdateLocationAsync(userMarketingData); | |||||
} | |||||
private List<Location> MapUpdatedUserLocations(List<UserLocationDetails> newUserLocations) | |||||
{ | |||||
var result = new List<Location>(); | |||||
newUserLocations.ForEach(location => { | |||||
result.Add(new Location() | |||||
{ | |||||
LocationId = location.LocationId, | |||||
Code = location.Code, | |||||
Description = location.Description | |||||
}); | |||||
}); | |||||
return result; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,16 @@ | |||||
using MongoDB.Bson; | |||||
using MongoDB.Bson.Serialization.Attributes; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model | |||||
{ | |||||
public class Location | |||||
{ | |||||
public int LocationId { get; set; } | |||||
public string Code { get; set; } | |||||
public string Description { get; set; } | |||||
} | |||||
} |
@ -0,0 +1,19 @@ | |||||
using MongoDB.Bson; | |||||
using MongoDB.Bson.Serialization.Attributes; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Threading.Tasks; | |||||
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model | |||||
{ | |||||
public class MarketingData | |||||
{ | |||||
[BsonIgnoreIfDefault] | |||||
[BsonRepresentation(BsonType.ObjectId)] | |||||
public string Id { get; set; } | |||||
public string UserId { get; set; } | |||||
public List<Location> Locations { get; set; } | |||||
public DateTime UpdateDate { get; set; } | |||||
} | |||||
} |
@ -0,0 +1,51 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model | |||||
{ | |||||
using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Exceptions; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
public sealed class RuleType | |||||
{ | |||||
public static readonly RuleType UserProfileRule = new RuleType(1, nameof(UserProfileRule)); | |||||
public static readonly RuleType PurchaseHistoryRule = new RuleType(2, nameof(UserProfileRule)); | |||||
public static readonly RuleType UserLocationRule = new RuleType(3, nameof(UserProfileRule)); | |||||
public readonly int Id; | |||||
public readonly string Name; | |||||
private RuleType(int id, string name) | |||||
{ | |||||
Id = id; | |||||
Name = name; | |||||
} | |||||
public static IEnumerable<RuleType> List() => | |||||
new[] { UserProfileRule, PurchaseHistoryRule, UserLocationRule }; | |||||
public static RuleType FromName(string name) | |||||
{ | |||||
var state = List() | |||||
.SingleOrDefault(s => String.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase)); | |||||
if (state == null) | |||||
{ | |||||
throw new MarketingDomainException($"Possible values for RuleType: {String.Join(",", List().Select(s => s.Name))}"); | |||||
} | |||||
return state; | |||||
} | |||||
public static RuleType From(int id) | |||||
{ | |||||
var state = List().SingleOrDefault(s => s.Id == id); | |||||
if (state == null) | |||||
{ | |||||
throw new MarketingDomainException($"Possible values for RuleType: {String.Join(",", List().Select(s => s.Name))}"); | |||||
} | |||||
return state; | |||||
} | |||||
} | |||||
} |
@ -1,20 +0,0 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model | |||||
{ | |||||
using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Exceptions; | |||||
using System; | |||||
public enum RuleTypeEnum { UserProfileRule = 1, PurchaseHistoryRule = 2, UserLocationRule = 3 } | |||||
public static class RuleType | |||||
{ | |||||
public static RuleTypeEnum From(int id) | |||||
{ | |||||
if (!Enum.IsDefined(typeof(RuleTypeEnum), id)) | |||||
{ | |||||
throw new MarketingDomainException($"Invalid value for RuleType, RuleTypeId: {id}"); | |||||
} | |||||
return (RuleTypeEnum)id; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,9 @@ | |||||
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model | |||||
{ | |||||
public class UserLocationDetails | |||||
{ | |||||
public int LocationId { get; set; } | |||||
public string Code { get; set; } | |||||
public string Description { get; set; } | |||||
} | |||||
} |
@ -0,0 +1,39 @@ | |||||
namespace FunctionalTests.Services.Locations | |||||
{ | |||||
using Microsoft.AspNetCore.Hosting; | |||||
using Microsoft.AspNetCore.TestHost; | |||||
using System; | |||||
using System.IO; | |||||
public class LocationsScenariosBase | |||||
{ | |||||
public TestServer CreateServer() | |||||
{ | |||||
var webHostBuilder = new WebHostBuilder(); | |||||
webHostBuilder.UseContentRoot(Directory.GetCurrentDirectory() + "\\Services\\Location"); | |||||
webHostBuilder.UseStartup<LocationsTestsStartup>(); | |||||
return new TestServer(webHostBuilder); | |||||
} | |||||
public static class Get | |||||
{ | |||||
public static string Locations = "api/v1/locations"; | |||||
public static string LocationBy(string id) | |||||
{ | |||||
return $"api/v1/locations/{id}"; | |||||
} | |||||
public static string UserLocationBy(Guid id) | |||||
{ | |||||
return $"api/v1/locations/user/{id}"; | |||||
} | |||||
} | |||||
public static class Post | |||||
{ | |||||
public static string AddNewLocation = "api/v1/locations/"; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,45 @@ | |||||
namespace FunctionalTests.Services.Locations | |||||
{ | |||||
using Microsoft.AspNetCore.Builder; | |||||
using Microsoft.AspNetCore.Hosting; | |||||
using Microsoft.AspNetCore.Http; | |||||
using Microsoft.eShopOnContainers.Services.Locations.API; | |||||
using System.Security.Claims; | |||||
using System.Threading.Tasks; | |||||
public class LocationsTestsStartup : Startup | |||||
{ | |||||
public LocationsTestsStartup(IHostingEnvironment env) : base(env) | |||||
{ | |||||
} | |||||
protected override void ConfigureAuth(IApplicationBuilder app) | |||||
{ | |||||
if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) | |||||
{ | |||||
app.UseMiddleware<LocationAuthorizeMiddleware>(); | |||||
} | |||||
else | |||||
{ | |||||
base.ConfigureAuth(app); | |||||
} | |||||
} | |||||
class LocationAuthorizeMiddleware | |||||
{ | |||||
private readonly RequestDelegate _next; | |||||
public LocationAuthorizeMiddleware(RequestDelegate rd) | |||||
{ | |||||
_next = rd; | |||||
} | |||||
public async Task Invoke(HttpContext httpContext) | |||||
{ | |||||
var identity = new ClaimsIdentity("cookies"); | |||||
identity.AddClaim(new Claim("sub", "4611ce3f-380d-4db5-8d76-87a8689058ed")); | |||||
httpContext.User.AddIdentity(identity); | |||||
await _next.Invoke(httpContext); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,8 @@ | |||||
{ | |||||
"ConnectionString": "mongodb://localhost:27017", | |||||
"Database": "LocationsDb", | |||||
"ExternalCatalogBaseUrl": "http://localhost:5101", | |||||
"IdentityUrl": "http://localhost:5105", | |||||
"isTest": "true", | |||||
"EventBusConnection": "localhost" | |||||
} |
@ -0,0 +1,35 @@ | |||||
namespace FunctionalTests.Services.Marketing | |||||
{ | |||||
using System; | |||||
public class CampaignScenariosBase : MarketingScenariosBase | |||||
{ | |||||
public static class Get | |||||
{ | |||||
public static string Campaigns = CampaignsUrlBase; | |||||
public static string CampaignBy(int id) | |||||
=> $"{CampaignsUrlBase}/{id}"; | |||||
public static string UserCampaignsByUserId(Guid userId) | |||||
=> $"{CampaignsUrlBase}/user/{userId}"; | |||||
} | |||||
public static class Post | |||||
{ | |||||
public static string AddNewCampaign = CampaignsUrlBase; | |||||
} | |||||
public static class Put | |||||
{ | |||||
public static string CampaignBy(int id) | |||||
=> $"{CampaignsUrlBase}/{id}"; | |||||
} | |||||
public static class Delete | |||||
{ | |||||
public static string CampaignBy(int id) | |||||
=> $"{CampaignsUrlBase}/{id}"; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,58 @@ | |||||
namespace FunctionalTests.Services.Marketing | |||||
{ | |||||
using UserLocation = Microsoft.eShopOnContainers.Services.Locations.API.Model.UserLocation; | |||||
using LocationRequest = Microsoft.eShopOnContainers.Services.Locations.API.ViewModel.LocationRequest; | |||||
using FunctionalTests.Services.Locations; | |||||
using Newtonsoft.Json; | |||||
using System; | |||||
using System.Net.Http; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
using Xunit; | |||||
using System.Collections.Generic; | |||||
using Microsoft.eShopOnContainers.Services.Marketing.API.Dto; | |||||
public class MarketingScenarios : MarketingScenariosBase | |||||
{ | |||||
[Fact] | |||||
public async Task Set_new_user_location_and_get_location_campaign_by_user_id() | |||||
{ | |||||
using (var locationsServer = new LocationsScenariosBase().CreateServer()) | |||||
using (var marketingServer = new MarketingScenariosBase().CreateServer()) | |||||
{ | |||||
var location = new LocationRequest | |||||
{ | |||||
Longitude = -122.315752, | |||||
Latitude = 47.604610 | |||||
}; | |||||
var content = new StringContent(JsonConvert.SerializeObject(location), | |||||
Encoding.UTF8, "application/json"); | |||||
var userId = new Guid("4611ce3f-380d-4db5-8d76-87a8689058ed"); | |||||
// GIVEN a new location of user is created | |||||
var response = await locationsServer.CreateClient() | |||||
.PostAsync(LocationsScenariosBase.Post.AddNewLocation, content); | |||||
//Get location user from Location.API | |||||
var userLocationResponse = await locationsServer.CreateClient() | |||||
.GetAsync(LocationsScenariosBase.Get.UserLocationBy(userId)); | |||||
var responseBody = await userLocationResponse.Content.ReadAsStringAsync(); | |||||
var userLocation = JsonConvert.DeserializeObject<UserLocation>(responseBody); | |||||
await Task.Delay(300); | |||||
//Get campaing from Marketing.API given a userId | |||||
var UserLocationCampaignResponse = await marketingServer.CreateClient() | |||||
.GetAsync(CampaignScenariosBase.Get.UserCampaignsByUserId(userId)); | |||||
responseBody = await UserLocationCampaignResponse.Content.ReadAsStringAsync(); | |||||
var userLocationCampaigns = JsonConvert.DeserializeObject<List<CampaignDTO>>(responseBody); | |||||
Assert.True(userLocationCampaigns.Count > 0); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,20 @@ | |||||
namespace FunctionalTests.Services.Marketing | |||||
{ | |||||
using Microsoft.AspNetCore.Hosting; | |||||
using Microsoft.AspNetCore.TestHost; | |||||
using System.IO; | |||||
public class MarketingScenariosBase | |||||
{ | |||||
public static string CampaignsUrlBase => "api/v1/campaigns"; | |||||
public TestServer CreateServer() | |||||
{ | |||||
var webHostBuilder = new WebHostBuilder(); | |||||
webHostBuilder.UseContentRoot(Directory.GetCurrentDirectory() + "\\Services\\Marketing"); | |||||
webHostBuilder.UseStartup<MarketingTestsStartup>(); | |||||
return new TestServer(webHostBuilder); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,26 @@ | |||||
namespace FunctionalTests.Services.Marketing | |||||
{ | |||||
using Microsoft.eShopOnContainers.Services.Marketing.API; | |||||
using Microsoft.AspNetCore.Hosting; | |||||
using Microsoft.AspNetCore.Builder; | |||||
using FunctionalTests.Middleware; | |||||
public class MarketingTestsStartup : Startup | |||||
{ | |||||
public MarketingTestsStartup(IHostingEnvironment env) : base(env) | |||||
{ | |||||
} | |||||
protected override void ConfigureAuth(IApplicationBuilder app) | |||||
{ | |||||
if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) | |||||
{ | |||||
app.UseMiddleware<AutoAuthorizeMiddleware>(); | |||||
} | |||||
else | |||||
{ | |||||
base.ConfigureAuth(app); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,40 @@ | |||||
namespace FunctionalTests.Services.Marketing | |||||
{ | |||||
public class UserLocationRoleScenariosBase : MarketingScenariosBase | |||||
{ | |||||
private const string EndpointLocationName = "locations"; | |||||
public static class Get | |||||
{ | |||||
public static string UserLocationRulesByCampaignId(int campaignId) | |||||
=> GetUserLocationRolesUrlBase(campaignId); | |||||
public static string UserLocationRuleByCampaignAndUserLocationRuleId(int campaignId, | |||||
int userLocationRuleId) | |||||
=> $"{GetUserLocationRolesUrlBase(campaignId)}/{userLocationRuleId}"; | |||||
} | |||||
public static class Post | |||||
{ | |||||
public static string AddNewuserLocationRule(int campaignId) | |||||
=> GetUserLocationRolesUrlBase(campaignId); | |||||
} | |||||
public static class Put | |||||
{ | |||||
public static string UserLocationRoleBy(int campaignId, | |||||
int userLocationRuleId) | |||||
=> $"{GetUserLocationRolesUrlBase(campaignId)}/{userLocationRuleId}"; | |||||
} | |||||
public static class Delete | |||||
{ | |||||
public static string UserLocationRoleBy(int campaignId, | |||||
int userLocationRuleId) | |||||
=> $"{GetUserLocationRolesUrlBase(campaignId)}/{userLocationRuleId}"; | |||||
} | |||||
private static string GetUserLocationRolesUrlBase(int campaignId) | |||||
=> $"{CampaignsUrlBase}/{campaignId}/{EndpointLocationName}"; | |||||
} | |||||
} |
@ -0,0 +1,8 @@ | |||||
{ | |||||
"ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word", | |||||
"MongoConnectionString": "mongodb://localhost:27017", | |||||
"MongoDatabase": "MarketingDb", | |||||
"IdentityUrl": "http://localhost:5105", | |||||
"isTest": "true", | |||||
"EventBusConnection": "localhost" | |||||
} |
@ -1,5 +1,8 @@ | |||||
{ | { | ||||
"ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word", | "ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word", | ||||
"MongoConnectionString": "mongodb://localhost:27017", | |||||
"MongoDatabase": "MarketingDb", | |||||
"IdentityUrl": "http://localhost:5105", | "IdentityUrl": "http://localhost:5105", | ||||
"isTest": "true" | |||||
"isTest": "true", | |||||
"EventBusConnection": "localhost" | |||||
} | } |