From 176da9af34b867bbd31ad32de362019002633caf Mon Sep 17 00:00:00 2001 From: Eli Gassert Date: Wed, 30 May 2018 12:58:35 +0200 Subject: [PATCH] Abandoned Commands in favor of straight services. Why? I was adding layers that were going to be heavily repeated where SS Service layer is already very "commandy" in their handlers. I'm going to try this route and see how she plays out. --- .../IsNsfw.Command.Interface.csproj | 10 +- src/IsNsfw.Command/CreateLinkCommand.cs | 26 -- .../Domains/LinkCommandHandlers.cs | 2 +- src/IsNsfw.Command/IsNsfw.Command.csproj | 2 +- src/IsNsfw.Model/ICreatedAt.cs | 11 + src/IsNsfw.Model/IsNsfw.Model.csproj | 2 +- src/IsNsfw.Model/Link.cs | 6 +- src/IsNsfw.Model/LinkEvent.cs | 6 +- src/IsNsfw.Model/LinkTag.cs | 4 + .../ILinkRepository.cs | 8 + .../IRepository.cs | 2 + .../ITagRepository.cs | 1 + .../IsNsfw.Repository.Interface.csproj | 7 +- .../IsNsfw.Repository.csproj | 2 +- src/IsNsfw.Repository/LinkRepository.cs | 55 +++++ src/IsNsfw.Repository/RepositoryBase.cs | 47 +++- src/IsNsfw.Repository/TagRepository.cs | 6 + src/IsNsfw.Service/AppHost.cs | 29 +++ src/IsNsfw.Service/IsNsfw.Service.csproj | 17 ++ src/IsNsfw.ServiceInterface/AppHost.cs | 21 -- .../IsNsfw.ServiceInterface.csproj | 2 + src/IsNsfw.ServiceInterface/LinkService.cs | 99 ++++++++ src/IsNsfw.ServiceInterface/ServiceBase.cs | 53 ++++ .../SimpleInjectorIocAdapter.cs | 26 ++ .../Validators/LinkValidators.cs | 37 +++ .../Validators/TagValidator.cs | 28 +++ .../Validators/ValidationHelpers.cs | 20 ++ .../IsNsfw.ServiceModel.csproj | 10 +- src/IsNsfw.ServiceModel/LinkRequests.cs | 22 ++ src/IsNsfw.ServiceModel/Types/.gitignore | 3 - src/IsNsfw.ServiceModel/Types/LinkResponse.cs | 26 ++ .../CreateLinkCommandValidatorTests.cs | 100 -------- .../CreateLinkEventCommandTests.cs | 209 ---------------- .../CreateLinkEventCommandValidatorTests.cs | 24 -- .../CreateLinkEventRequestTests.cs | 233 ++++++++++++++++++ ...mandTests.cs => CreateLinkRequestTests.cs} | 122 ++++----- .../CreateLinkRequestValidatorTests.cs | 161 ++++++++++++ src/IsNsfw.Tests/IntegrationTest.cs | 45 ---- src/IsNsfw.Tests/IsNsfw.Tests.csproj | 5 +- src/IsNsfw.Tests/LinkServiceTests.cs | 109 ++++++++ src/IsNsfw.sln | 24 +- src/IsNsfw/IsNsfw.csproj | 1 + 42 files changed, 1100 insertions(+), 523 deletions(-) create mode 100644 src/IsNsfw.Model/ICreatedAt.cs create mode 100644 src/IsNsfw.Service/AppHost.cs create mode 100644 src/IsNsfw.Service/IsNsfw.Service.csproj delete mode 100644 src/IsNsfw.ServiceInterface/AppHost.cs create mode 100644 src/IsNsfw.ServiceInterface/LinkService.cs create mode 100644 src/IsNsfw.ServiceInterface/ServiceBase.cs create mode 100644 src/IsNsfw.ServiceInterface/SimpleInjectorIocAdapter.cs create mode 100644 src/IsNsfw.ServiceInterface/Validators/LinkValidators.cs create mode 100644 src/IsNsfw.ServiceInterface/Validators/TagValidator.cs create mode 100644 src/IsNsfw.ServiceInterface/Validators/ValidationHelpers.cs create mode 100644 src/IsNsfw.ServiceModel/LinkRequests.cs delete mode 100644 src/IsNsfw.ServiceModel/Types/.gitignore create mode 100644 src/IsNsfw.ServiceModel/Types/LinkResponse.cs delete mode 100644 src/IsNsfw.Tests/CreateLinkCommandValidatorTests.cs delete mode 100644 src/IsNsfw.Tests/CreateLinkEventCommandTests.cs delete mode 100644 src/IsNsfw.Tests/CreateLinkEventCommandValidatorTests.cs create mode 100644 src/IsNsfw.Tests/CreateLinkEventRequestTests.cs rename src/IsNsfw.Tests/{CreateLinkCommandTests.cs => CreateLinkRequestTests.cs} (56%) create mode 100644 src/IsNsfw.Tests/CreateLinkRequestValidatorTests.cs delete mode 100644 src/IsNsfw.Tests/IntegrationTest.cs create mode 100644 src/IsNsfw.Tests/LinkServiceTests.cs diff --git a/src/IsNsfw.Command.Interface/IsNsfw.Command.Interface.csproj b/src/IsNsfw.Command.Interface/IsNsfw.Command.Interface.csproj index 9b5a06a..ba0a153 100644 --- a/src/IsNsfw.Command.Interface/IsNsfw.Command.Interface.csproj +++ b/src/IsNsfw.Command.Interface/IsNsfw.Command.Interface.csproj @@ -1,17 +1,17 @@ - netcoreapp2.0 + netstandard2.0 - - ..\..\..\..\Users\egassert\.nuget\packages\servicestack\5.1.1\lib\netstandard2.0\ServiceStack.dll - + + + - + diff --git a/src/IsNsfw.Command/CreateLinkCommand.cs b/src/IsNsfw.Command/CreateLinkCommand.cs index 02242f7..1034183 100644 --- a/src/IsNsfw.Command/CreateLinkCommand.cs +++ b/src/IsNsfw.Command/CreateLinkCommand.cs @@ -19,30 +19,4 @@ public class CreateLinkCommand : ICommandWithIntResult // out parameter public int Id { get; set; } } - - public class CreateLinkCommandValidator : CommandValidatorBase - { - private readonly ILinkRepository _linkRepo; - - public CreateLinkCommandValidator(ILinkRepository linkRepo) - { - _linkRepo = linkRepo; - RuleFor(m => m.SessionId).NotEmpty(); - RuleFor(m => m.Key).NotEmpty(); - RuleFor(m => m.Url).NotEmpty().MustBeAUrl(); - } - - public override ValidationResult Validate(ValidationContext context) - { - var ret = base.Validate(context); - - if(ret.IsValid) - { - if(_linkRepo.KeyExists(context.InstanceToValidate.Key)) - ret.Errors.Add(new ValidationFailure(nameof(context.InstanceToValidate.Key), $"Key '{context.InstanceToValidate.Key}' already exists.")); - } - - return ret; - } - } } diff --git a/src/IsNsfw.Command/Domains/LinkCommandHandlers.cs b/src/IsNsfw.Command/Domains/LinkCommandHandlers.cs index f7a8fa5..ae5bb75 100644 --- a/src/IsNsfw.Command/Domains/LinkCommandHandlers.cs +++ b/src/IsNsfw.Command/Domains/LinkCommandHandlers.cs @@ -48,7 +48,7 @@ public void Handle(CreateLinkEventCommand command) UnitOfWork(db => { var c = command.ConvertTo(); - c.Timestamp = DateTime.UtcNow; + c.CreatedAt = DateTime.UtcNow; db.Save(c); switch(c.LinkEventType) diff --git a/src/IsNsfw.Command/IsNsfw.Command.csproj b/src/IsNsfw.Command/IsNsfw.Command.csproj index e7cfac9..7531d44 100644 --- a/src/IsNsfw.Command/IsNsfw.Command.csproj +++ b/src/IsNsfw.Command/IsNsfw.Command.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + netstandard2.0 diff --git a/src/IsNsfw.Model/ICreatedAt.cs b/src/IsNsfw.Model/ICreatedAt.cs new file mode 100644 index 0000000..5d37bed --- /dev/null +++ b/src/IsNsfw.Model/ICreatedAt.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace IsNsfw.Model +{ + public interface ICreatedAt + { + DateTime CreatedAt { get; set; } + } +} diff --git a/src/IsNsfw.Model/IsNsfw.Model.csproj b/src/IsNsfw.Model/IsNsfw.Model.csproj index 001208a..005bc87 100644 --- a/src/IsNsfw.Model/IsNsfw.Model.csproj +++ b/src/IsNsfw.Model/IsNsfw.Model.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + netstandard2.0 diff --git a/src/IsNsfw.Model/Link.cs b/src/IsNsfw.Model/Link.cs index 035c16b..067d44e 100644 --- a/src/IsNsfw.Model/Link.cs +++ b/src/IsNsfw.Model/Link.cs @@ -4,11 +4,12 @@ using ServiceStack.Auth; using ServiceStack.DataAnnotations; using ServiceStack.Model; +using ServiceStack.OrmLite; namespace IsNsfw.Model { [Alias("Links")] - public class Link : IHasIntId, ISoftDelete + public class Link : IHasIntId, ISoftDelete, ICreatedAt { [AutoIncrement] [PrimaryKey] @@ -39,6 +40,9 @@ public class Link : IHasIntId, ISoftDelete [Reference] public List LinkTags { get; set; } + [Default(OrmLiteVariables.SystemUtc)] // Populated with UTC Date by RDBMS + public DateTime CreatedAt { get; set; } + //[Ignore] //public List Tags { get; set; } } diff --git a/src/IsNsfw.Model/LinkEvent.cs b/src/IsNsfw.Model/LinkEvent.cs index 9eee6ff..74fbb95 100644 --- a/src/IsNsfw.Model/LinkEvent.cs +++ b/src/IsNsfw.Model/LinkEvent.cs @@ -5,7 +5,7 @@ namespace IsNsfw.Model { - public class LinkEvent : IHasIntId + public class LinkEvent : IHasIntId, ICreatedAt { [AutoIncrement] [PrimaryKey] @@ -20,7 +20,7 @@ public class LinkEvent : IHasIntId [Index] [References(typeof(Link))] - public int? LinkId { get; set; } + public int LinkId { get; set; } [Reference] public User User { get; set; } @@ -31,6 +31,6 @@ public class LinkEvent : IHasIntId public LinkEventType LinkEventType { get; set; } [Default(OrmLiteVariables.SystemUtc)] // Populated with UTC Date by RDBMS - public DateTime Timestamp { get; set; } + public DateTime CreatedAt { get; set; } } } diff --git a/src/IsNsfw.Model/LinkTag.cs b/src/IsNsfw.Model/LinkTag.cs index e7a5d97..e1531e2 100644 --- a/src/IsNsfw.Model/LinkTag.cs +++ b/src/IsNsfw.Model/LinkTag.cs @@ -6,6 +6,10 @@ namespace IsNsfw.Model [CompositeIndex(nameof(LinkId), nameof(TagId))] public class LinkTag { + [PrimaryKey] + [Ignore] + public string Id => $"{this.LinkId}/{this.TagId}"; + [References(typeof(Link))] public int LinkId { get; set; } diff --git a/src/IsNsfw.Repository.Interface/ILinkRepository.cs b/src/IsNsfw.Repository.Interface/ILinkRepository.cs index bc0e037..3afaf7d 100644 --- a/src/IsNsfw.Repository.Interface/ILinkRepository.cs +++ b/src/IsNsfw.Repository.Interface/ILinkRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using IsNsfw.Model; +using IsNsfw.ServiceModel.Types; namespace IsNsfw.Repository.Interface { @@ -9,5 +10,12 @@ public interface ILinkRepository : IIntRepository { bool KeyExists(string key); Link GetByKey(string key); + void SetLinkTags(int linkId, IEnumerable linkTags); + LinkResponse GetLinkResponse(int linkId); + void CreateLinkEvent(LinkEvent linkEvent); + void IncrementTotalViews(int linkId); + void IncrementClickThroughs(int linkId); + void IncrementPreviews(int linkId); + void IncrementTurnBacks(int linkId); } } diff --git a/src/IsNsfw.Repository.Interface/IRepository.cs b/src/IsNsfw.Repository.Interface/IRepository.cs index d0167da..046bf6a 100644 --- a/src/IsNsfw.Repository.Interface/IRepository.cs +++ b/src/IsNsfw.Repository.Interface/IRepository.cs @@ -9,6 +9,8 @@ public interface IRepository where T : IHasId { T GetById(TIdType id); void DeleteById(TIdType id); + TIdType Create(T item); + void Update(T item); } public interface IIntRepository : IRepository where T : IHasId { } diff --git a/src/IsNsfw.Repository.Interface/ITagRepository.cs b/src/IsNsfw.Repository.Interface/ITagRepository.cs index d1e8164..97d6319 100644 --- a/src/IsNsfw.Repository.Interface/ITagRepository.cs +++ b/src/IsNsfw.Repository.Interface/ITagRepository.cs @@ -10,5 +10,6 @@ public interface ITagRepository : IIntRepository bool KeyExists(string key); Tag GetByKey(string key); List GetOrderedTags(); + Dictionary GetTagsDictionary(); } } diff --git a/src/IsNsfw.Repository.Interface/IsNsfw.Repository.Interface.csproj b/src/IsNsfw.Repository.Interface/IsNsfw.Repository.Interface.csproj index 8df7210..73d1b22 100644 --- a/src/IsNsfw.Repository.Interface/IsNsfw.Repository.Interface.csproj +++ b/src/IsNsfw.Repository.Interface/IsNsfw.Repository.Interface.csproj @@ -1,11 +1,16 @@ - netcoreapp2.0 + netstandard2.0 + + + + + diff --git a/src/IsNsfw.Repository/IsNsfw.Repository.csproj b/src/IsNsfw.Repository/IsNsfw.Repository.csproj index eefeddc..1b6646d 100644 --- a/src/IsNsfw.Repository/IsNsfw.Repository.csproj +++ b/src/IsNsfw.Repository/IsNsfw.Repository.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + netstandard2.0 diff --git a/src/IsNsfw.Repository/LinkRepository.cs b/src/IsNsfw.Repository/LinkRepository.cs index 851c82a..297b74b 100644 --- a/src/IsNsfw.Repository/LinkRepository.cs +++ b/src/IsNsfw.Repository/LinkRepository.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using IsNsfw.Model; using IsNsfw.Repository.Interface; +using IsNsfw.ServiceModel.Types; +using ServiceStack; using ServiceStack.Data; using ServiceStack.OrmLite; @@ -23,5 +26,57 @@ public Link GetByKey(string key) { return Execute(db => db.Single(m => m.Key == key)); } + + public void SetLinkTags(int linkId, IEnumerable linkTags) + { + Execute(db => + { + db.Delete(m => m.LinkId == linkId); + db.InsertAll(linkTags.Where(m => m.LinkId == linkId)); + }); + } + + public LinkResponse GetLinkResponse(int linkId) + { + return Execute(db => + { + var query = db.From() + .LeftJoin( (l, lt) => l.Id == lt.LinkId) + .LeftJoin( (lt, t) => lt.TagId == t.Id) + .Where(l => l.Id == linkId); + + var results = db.SelectMulti(query); + + var link = results[0].Item1.ConvertTo(); + link.Tags = results.Select(m => m.Item2.Key).ToHashSet(); + + return link; + }); + } + + public void CreateLinkEvent(LinkEvent linkEvent) + { + Execute(db => db.Save(linkEvent)); + } + + public void IncrementTotalViews(int linkId) + { + Execute(db => db.UpdateAdd(() => new Link { TotalViews = 1 }, where: m => m.Id == linkId)); + } + + public void IncrementClickThroughs(int linkId) + { + Execute(db => db.UpdateAdd(() => new Link { TotalClickThroughs = 1 }, where: m => m.Id == linkId)); + } + + public void IncrementPreviews(int linkId) + { + Execute(db => db.UpdateAdd(() => new Link { TotalPreviews = 1 }, where: m => m.Id == linkId)); + } + + public void IncrementTurnBacks(int linkId) + { + Execute(db => db.UpdateAdd(() => new Link { TotalTurnBacks = 1 }, where: m => m.Id == linkId)); + } } } diff --git a/src/IsNsfw.Repository/RepositoryBase.cs b/src/IsNsfw.Repository/RepositoryBase.cs index 70efdee..a14449d 100644 --- a/src/IsNsfw.Repository/RepositoryBase.cs +++ b/src/IsNsfw.Repository/RepositoryBase.cs @@ -26,7 +26,7 @@ public virtual T GetById(TIndex id) public virtual void DeleteById(TIndex id) { - if(typeof(ISoftDelete).IsAssignableFrom(typeof(T))) + if (typeof(ISoftDelete).IsAssignableFrom(typeof(T))) { Execute(db => db.Update(new { IsDeleted = true }, where: m => m.Id.Equals(id))); } @@ -36,15 +36,41 @@ public virtual void DeleteById(TIndex id) } } + public virtual TIndex Create(T item) + { + return Execute(db => + { + db.Save(item); + return item.Id; + }); + } + + public virtual void Update(T item) + { + Execute(db => + { + db.Update(item); + }); + } + public void ExecuteTransaction(Action a) { - using(var db = _factory.OpenDbConnection()) - using(var trans = db.OpenTransaction()) + using (var db = _factory.OpenDbConnection()) + using (var trans = db.OpenTransaction()) { a(db, trans); } } + public TResult ExecuteTransaction(Func a) + { + using (var db = _factory.OpenDbConnection()) + using (var trans = db.OpenTransaction()) + { + return a(db, trans); + } + } + public void UnitOfWork(Action a) { using (var db = _factory.OpenDbConnection()) @@ -56,9 +82,22 @@ public void UnitOfWork(Action a) } } + public TResult UnitOfWork(Func a) + { + using (var db = _factory.OpenDbConnection()) + using (var trans = db.OpenTransaction()) + { + var ret = a(db); + + trans.Commit(); + + return ret; + } + } + public virtual void Execute(Action a) { - using(var db = _factory.OpenDbConnection()) + using (var db = _factory.OpenDbConnection()) a(db); } diff --git a/src/IsNsfw.Repository/TagRepository.cs b/src/IsNsfw.Repository/TagRepository.cs index 3f7c977..016b157 100644 --- a/src/IsNsfw.Repository/TagRepository.cs +++ b/src/IsNsfw.Repository/TagRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using IsNsfw.Model; using IsNsfw.Repository.Interface; @@ -28,5 +29,10 @@ public List GetOrderedTags() { return Execute(db => db.Select(db.From().Where(m => !m.IsDeleted).OrderBy(m => m.SortOrder).ThenBy(m => m.Key))); } + + public Dictionary GetTagsDictionary() + { + return GetOrderedTags().ToDictionary(m => m.Key, m => m); + } } } diff --git a/src/IsNsfw.Service/AppHost.cs b/src/IsNsfw.Service/AppHost.cs new file mode 100644 index 0000000..70894d3 --- /dev/null +++ b/src/IsNsfw.Service/AppHost.cs @@ -0,0 +1,29 @@ +using IsNsfw.Repository; +using IsNsfw.Repository.Interface; +using ServiceStack; +using SimpleInjector; + +namespace IsNsfw.ServiceInterface +{ + public class AppHost : AppHostBase + { + public AppHost() : base("IsNSFW", typeof(MyServices).Assembly) { } + + // Configure your AppHost with the necessary configuration and dependencies your App needs + public override void Configure(Funq.Container container) + { + base.SetConfig(new HostConfig + { + DebugMode = AppSettings.Get(nameof(HostConfig.DebugMode), false) + }); + + Plugins.Add(new TemplatePagesFeature()); + + var simpleContainer = new Container(); + container.Adapter = new SimpleInjectorIocAdapter (simpleContainer); + + simpleContainer.Register(typeof(IRepository<,>), typeof(IRepository<,>).Assembly, typeof(LinkRepository).Assembly); + //simpleContainer.RegisterDecorator(...); // https://cuttingedge.it/blogs/steven/pivot/entry.php?id=93 + } + } +} \ No newline at end of file diff --git a/src/IsNsfw.Service/IsNsfw.Service.csproj b/src/IsNsfw.Service/IsNsfw.Service.csproj new file mode 100644 index 0000000..a5f8c6f --- /dev/null +++ b/src/IsNsfw.Service/IsNsfw.Service.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp2.0 + + + + + + + + + + + + + diff --git a/src/IsNsfw.ServiceInterface/AppHost.cs b/src/IsNsfw.ServiceInterface/AppHost.cs deleted file mode 100644 index 6d1d15b..0000000 --- a/src/IsNsfw.ServiceInterface/AppHost.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Funq; -using ServiceStack; - -namespace IsNsfw.ServiceInterface -{ - public class AppHost : AppHostBase - { - public AppHost() : base("IsNSFW", typeof(MyServices).Assembly) { } - - // Configure your AppHost with the necessary configuration and dependencies your App needs - public override void Configure(Container container) - { - base.SetConfig(new HostConfig - { - DebugMode = AppSettings.Get(nameof(HostConfig.DebugMode), false) - }); - - Plugins.Add(new TemplatePagesFeature()); - } - } -} \ No newline at end of file diff --git a/src/IsNsfw.ServiceInterface/IsNsfw.ServiceInterface.csproj b/src/IsNsfw.ServiceInterface/IsNsfw.ServiceInterface.csproj index 20a2fb3..9c6e51a 100644 --- a/src/IsNsfw.ServiceInterface/IsNsfw.ServiceInterface.csproj +++ b/src/IsNsfw.ServiceInterface/IsNsfw.ServiceInterface.csproj @@ -6,9 +6,11 @@ + + diff --git a/src/IsNsfw.ServiceInterface/LinkService.cs b/src/IsNsfw.ServiceInterface/LinkService.cs new file mode 100644 index 0000000..ee6c5c9 --- /dev/null +++ b/src/IsNsfw.ServiceInterface/LinkService.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Text; +using IsNsfw.Model; +using IsNsfw.ServiceModel; +using ServiceStack; +using IsNsfw.Repository.Interface; +using IsNsfw.ServiceModel.Types; + +namespace IsNsfw.ServiceInterface +{ + public class LinkService : ServiceBase + , IPost + , IPostVoid + { + private readonly ILinkRepository _linkRepo; + private readonly ITagRepository _tagRepo; + + public LinkService(ILinkRepository linkRepo + , ITagRepository tagRepo) + { + _linkRepo = linkRepo; + _tagRepo = tagRepo; + } + + public object Post(CreateLinkRequest request) + { + var link = request.ConvertTo(); + link.SessionId = Session.Id; + link.CreatedAt = DateTime.UtcNow; + + UnitOfWork(() => + { + _linkRepo.Create(link); + + if(request.Tags != null) + { + link.LinkTags = new List(); + + var tagsDict = _tagRepo.GetTagsDictionary(); + + foreach(var tag in request.Tags) + { + if(!tagsDict.ContainsKey(tag)) + throw HttpError.NotFound($"Tag '{tag}' not found."); + + var linkTag = new LinkTag() + { + LinkId = link.Id, + TagId = tagsDict[tag].Id + }; + + link.LinkTags.Add(linkTag); + } + + _linkRepo.SetLinkTags(link.Id, link.LinkTags); + } + }); + + var ret = _linkRepo.GetLinkResponse(link.Id); + + return ret; + } + + public void Post(CreateLinkEventRequest request) + { + UnitOfWork(() => + { + var c = request.ConvertTo(); + c.LinkId = request.Id; + c.CreatedAt = DateTime.UtcNow; + c.SessionId = Session.Id; + _linkRepo.CreateLinkEvent(c); + + switch(c.LinkEventType) + { + case LinkEventType.View: + _linkRepo.IncrementTotalViews(c.LinkId); + break; + + case LinkEventType.ClickThrough: + _linkRepo.IncrementClickThroughs(c.LinkId); + break; + + case LinkEventType.Preview: + _linkRepo.IncrementPreviews(c.LinkId); + break; + + case LinkEventType.TurnBack: + _linkRepo.IncrementTurnBacks(c.LinkId); + break; + + default: + throw new System.ArgumentException($"Unknown {nameof(LinkEventType)} value '{c.LinkEventType}'", nameof(c.LinkEventType)); + } + }); + } + } +} diff --git a/src/IsNsfw.ServiceInterface/ServiceBase.cs b/src/IsNsfw.ServiceInterface/ServiceBase.cs new file mode 100644 index 0000000..18cdcc6 --- /dev/null +++ b/src/IsNsfw.ServiceInterface/ServiceBase.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Text; +using System.Transactions; +using ServiceStack.Auth; + +namespace IsNsfw.ServiceInterface +{ + public class ServiceBase : ServiceStack.Service + { + private IAuthSession _session; + public IAuthSession Session => _session ?? (_session = this.GetSession()); + + public void ExecuteTransaction(Action a) + { + using (var trans = new TransactionScope(TransactionScopeOption.Required, TransactionScopeAsyncFlowOption.Enabled)) + { + a(trans); + } + } + + public TResult ExecuteTransaction(Func a) + { + using (var trans = new TransactionScope(TransactionScopeOption.Required, TransactionScopeAsyncFlowOption.Enabled)) + { + return a(trans); + } + } + + public void UnitOfWork(Action a) + { + using (var trans = new TransactionScope(TransactionScopeOption.Required, TransactionScopeAsyncFlowOption.Enabled)) + { + a(); + + trans.Complete(); + } + } + + public TResult UnitOfWork(Func a) + { + using (var trans = new TransactionScope(TransactionScopeOption.Required, TransactionScopeAsyncFlowOption.Enabled)) + { + var ret = a(); + + trans.Complete(); + + return ret; + } + } + } +} diff --git a/src/IsNsfw.ServiceInterface/SimpleInjectorIocAdapter.cs b/src/IsNsfw.ServiceInterface/SimpleInjectorIocAdapter.cs new file mode 100644 index 0000000..0f04d9d --- /dev/null +++ b/src/IsNsfw.ServiceInterface/SimpleInjectorIocAdapter.cs @@ -0,0 +1,26 @@ +using ServiceStack.Configuration; +using SimpleInjector; + +namespace IsNsfw.ServiceInterface +{ + public class SimpleInjectorIocAdapter : IContainerAdapter + { + private readonly Container _container; + + public SimpleInjectorIocAdapter(Container container) + { + this._container = container; + } + + public T Resolve() + { + return (T)this._container.GetInstance(typeof(T)); + } + + public T TryResolve() + { + var registration = this._container.GetRegistration(typeof(T)); + return (T) registration?.GetInstance(); + } + } +} diff --git a/src/IsNsfw.ServiceInterface/Validators/LinkValidators.cs b/src/IsNsfw.ServiceInterface/Validators/LinkValidators.cs new file mode 100644 index 0000000..7d98815 --- /dev/null +++ b/src/IsNsfw.ServiceInterface/Validators/LinkValidators.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; +using IsNsfw.Repository.Interface; +using IsNsfw.ServiceModel; +using ServiceStack.FluentValidation; +using ServiceStack.FluentValidation.Results; + +namespace IsNsfw.ServiceInterface.Validators +{ + public class CreateLinkRequestValidator : AbstractValidator + { + private readonly ILinkRepository _linkRepo; + + public CreateLinkRequestValidator(ILinkRepository linkRepo, ITagValidator tagValidator) + { + _linkRepo = linkRepo; + RuleFor(m => m.Key).NotEmpty(); + RuleFor(m => m.Url).NotEmpty().MustBeAUrl(); + RuleFor(m => m.Tags).NotEmpty(); + RuleForEach(m => m.Tags).Must(tagValidator.ValidateTagKey).WithMessage(m => $"Tag '{m}' not found."); + } + + public override ValidationResult Validate(ValidationContext context) + { + var ret = base.Validate(context); + + if(ret.IsValid) + { + if(_linkRepo.KeyExists(context.InstanceToValidate.Key)) + ret.Errors.Add(new ValidationFailure(nameof(context.InstanceToValidate.Key), $"Key '{context.InstanceToValidate.Key}' already exists.")); + } + + return ret; + } + } +} diff --git a/src/IsNsfw.ServiceInterface/Validators/TagValidator.cs b/src/IsNsfw.ServiceInterface/Validators/TagValidator.cs new file mode 100644 index 0000000..da54f78 --- /dev/null +++ b/src/IsNsfw.ServiceInterface/Validators/TagValidator.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; +using IsNsfw.Repository.Interface; +using ServiceStack.FluentValidation; + +namespace IsNsfw.ServiceInterface.Validators +{ + public interface ITagValidator + { + bool ValidateTagKey(string key); + } + + public class TagValidator : ITagValidator + { + private readonly ITagRepository _tagRepo; + + public TagValidator(ITagRepository tagRepo) + { + _tagRepo = tagRepo; + } + + public bool ValidateTagKey(string key) + { + return _tagRepo.KeyExists(key); + } + } +} diff --git a/src/IsNsfw.ServiceInterface/Validators/ValidationHelpers.cs b/src/IsNsfw.ServiceInterface/Validators/ValidationHelpers.cs new file mode 100644 index 0000000..cb7e901 --- /dev/null +++ b/src/IsNsfw.ServiceInterface/Validators/ValidationHelpers.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; +using ServiceStack.FluentValidation; + +namespace IsNsfw.ServiceInterface.Validators +{ + public static class ValidationHelpers + { + public static IRuleBuilderOptions MustBeAUrl(this IRuleBuilderOptions builder) + { + return builder.Must(ValidationHelpers.Url).WithMessage("Invalid URL"); + } + + public static bool Url(string arg) + { + return Uri.TryCreate(arg, UriKind.Absolute, out _); + } + } +} diff --git a/src/IsNsfw.ServiceModel/IsNsfw.ServiceModel.csproj b/src/IsNsfw.ServiceModel/IsNsfw.ServiceModel.csproj index d533286..f6f1460 100644 --- a/src/IsNsfw.ServiceModel/IsNsfw.ServiceModel.csproj +++ b/src/IsNsfw.ServiceModel/IsNsfw.ServiceModel.csproj @@ -5,11 +5,17 @@ - + - + + + + + + ..\..\..\..\Users\egassert\.nuget\packages\servicestack.text\5.1.1\lib\netstandard2.0\ServiceStack.Text.dll + diff --git a/src/IsNsfw.ServiceModel/LinkRequests.cs b/src/IsNsfw.ServiceModel/LinkRequests.cs new file mode 100644 index 0000000..36d9347 --- /dev/null +++ b/src/IsNsfw.ServiceModel/LinkRequests.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using IsNsfw.Model; +using IsNsfw.ServiceModel.Types; +using ServiceStack; + +namespace IsNsfw.ServiceModel +{ + [Route("/links", HttpMethods.Post)] + public class CreateLinkRequest : IPost, IReturn + { + public string Key { get; set; } + public string Url { get; set; } + public HashSet Tags { get; set; } + } + + [Route("/links/{Id}/event", HttpMethods.Post)] + public class CreateLinkEventRequest : IPost, IReturnVoid + { + public int Id { get; set; } + public LinkEventType LinkEventType { get; set; } + } +} diff --git a/src/IsNsfw.ServiceModel/Types/.gitignore b/src/IsNsfw.ServiceModel/Types/.gitignore deleted file mode 100644 index d6a6183..0000000 --- a/src/IsNsfw.ServiceModel/Types/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Keep Empty Directory -* -!.gitignore \ No newline at end of file diff --git a/src/IsNsfw.ServiceModel/Types/LinkResponse.cs b/src/IsNsfw.ServiceModel/Types/LinkResponse.cs new file mode 100644 index 0000000..67e89e5 --- /dev/null +++ b/src/IsNsfw.ServiceModel/Types/LinkResponse.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace IsNsfw.ServiceModel.Types +{ + public class LinkResponse + { + public int Id { get; set; } + + public string Key { get; set; } + + public string SessionId { get; set; } + + public string Url { get; set; } + + public bool IsDeleted { get; set; } + + public int TotalViews { get; set; } + public int TotalPreviews { get; set; } + public int TotalClickThroughs { get; set; } + public int TotalTurnBacks { get; set; } + + public int? UserId { get; set; } + + public HashSet Tags { get; set; } + } +} \ No newline at end of file diff --git a/src/IsNsfw.Tests/CreateLinkCommandValidatorTests.cs b/src/IsNsfw.Tests/CreateLinkCommandValidatorTests.cs deleted file mode 100644 index 8d8acf6..0000000 --- a/src/IsNsfw.Tests/CreateLinkCommandValidatorTests.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using IsNsfw.Command; -using IsNsfw.Repository.Interface; -using Moq; -using NUnit.Framework; - -namespace IsNsfw.Tests -{ - public class CreateLinkCommandValidatorTests - { - [Test] - public void ErrorIfNullUrl() - { - var sut = new CreateLinkCommandValidator(null); - var results = sut.Validate(new CreateLinkCommand() { }); - - Assert.IsFalse(results.IsValid); - Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkCommand.Url))); - } - - [Test] - public void ErrorIfNullKey() - { - var sut = new CreateLinkCommandValidator(null); - var results = sut.Validate(new CreateLinkCommand() { }); - - Assert.IsFalse(results.IsValid); - Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkCommand.Key))); - } - - [Test] - public void ErrorIfNullSessionId() - { - var sut = new CreateLinkCommandValidator(null); - var results = sut.Validate(new CreateLinkCommand() { }); - - Assert.IsFalse(results.IsValid); - Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkCommand.SessionId))); - } - - [Test] - public void ErrorIfBlankUrl() - { - var sut = new CreateLinkCommandValidator(null); - var results = sut.Validate(new CreateLinkCommand() { Url = " " }); - - Assert.IsFalse(results.IsValid); - Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkCommand.Url))); - } - - [Test] - public void ErrorIfBlankKey() - { - var sut = new CreateLinkCommandValidator(null); - var results = sut.Validate(new CreateLinkCommand() { Key = " " }); - - Assert.IsFalse(results.IsValid); - Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkCommand.Key))); - } - - [Test] - public void ErrorIfBlankSessionId() - { - var sut = new CreateLinkCommandValidator(null); - var results = sut.Validate(new CreateLinkCommand() { SessionId = " " }); - - Assert.IsFalse(results.IsValid); - Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkCommand.SessionId))); - } - - [Test] - public void RequiresValidUrl() - { - var linkRepo = new Mock(); - linkRepo.Setup(m => m.KeyExists(It.IsAny())).Returns(false); - - var sut = new CreateLinkCommandValidator(linkRepo.Object); - var results = sut.Validate(new CreateLinkCommand() { Key = "Test", Url = "bad url", SessionId = "123" }); - - Assert.IsFalse(results.IsValid); - Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkCommand.Url))); - } - - [Test] - public void RequiresUniqueKey() - { - var linkRepo = new Mock(); - linkRepo.Setup(m => m.KeyExists(It.IsAny())).Returns(true); - - var sut = new CreateLinkCommandValidator(linkRepo.Object); - var results = sut.Validate(new CreateLinkCommand() { Url = "http://www.google.com", Key = "werd", SessionId = "123" }); - - Assert.IsFalse(results.IsValid); - Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkCommand.Key))); - } - } -} diff --git a/src/IsNsfw.Tests/CreateLinkEventCommandTests.cs b/src/IsNsfw.Tests/CreateLinkEventCommandTests.cs deleted file mode 100644 index cc96827..0000000 --- a/src/IsNsfw.Tests/CreateLinkEventCommandTests.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Text; -using IsNsfw.Command; -using IsNsfw.Command.Domains; -using IsNsfw.Command.Interface; -using IsNsfw.Model; -using IsNsfw.Repository; -using NUnit.Framework; -using ServiceStack.OrmLite; - -namespace IsNsfw.Tests -{ - public class CreateLinkEventCommandTests - { - private readonly LinkRepository _linkRepo; - private readonly LinkCommandHandlers _linkDomain; - private readonly IDbConnection _db; - private readonly OrmLiteConnectionFactory _dbFactory; - private Link _link; - - public CreateLinkEventCommandTests() - { - _dbFactory = new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider); - - _db = _dbFactory.Open(); - - _db.CreateTableIfNotExists(); - _db.CreateTableIfNotExists(); - _db.CreateTableIfNotExists(); - - _linkDomain = new LinkCommandHandlers(_dbFactory); - } - - public ICommandHandler GetCommandHandler() - { - return new ValidationCommandHandlerDecorator(_linkDomain, new CreateLinkEventCommandValidator()); - } - - [SetUp] - public void SetUp() - { - _link = new Link() { Key = "test", Url = "http://www.google.com", SessionId = "test", }; - _db.Save(_link); - } - - [TearDown] - public void TearDown() - { - _db.DeleteAll(); - _db.DeleteAll(); - _db.DeleteAll(); - } - - [OneTimeTearDown] - public void OneTimeTearDown() - { - _db.DeleteAll(); - _db.DeleteAll(); - _db.DeleteAll(); - _db.Dispose(); - } - - [Test] - public void CreateLinkEventCommand_View_CreatesRecord() - { - var sut = GetCommandHandler(); - - var req = new CreateLinkEventCommand() - { - LinkId = _link.Id, - SessionId = "12345", - LinkEventType = LinkEventType.View - }; - - sut.Handle(req); - - Assert.IsTrue(_db.Exists(m => m.SessionId == req.SessionId && m.LinkEventType == req.LinkEventType)); - } - - [Test] - public void CreateLinkEventCommand_View_IncrementsTotal() - { - var sut = GetCommandHandler(); - - var req = new CreateLinkEventCommand() - { - LinkId = _link.Id, - SessionId = "12345", - LinkEventType = LinkEventType.View - }; - - sut.Handle(req); - - _link = _db.SingleById(_link.Id); - - Assert.AreEqual(1, _link.TotalViews); - } - - [Test] - public void CreateLinkEventCommand_Preview_CreatesRecord() - { - var sut = GetCommandHandler(); - - var req = new CreateLinkEventCommand() - { - LinkId = _link.Id, - SessionId = "12345", - LinkEventType = LinkEventType.Preview - }; - - sut.Handle(req); - - Assert.IsTrue(_db.Exists(m => m.SessionId == req.SessionId && m.LinkEventType == req.LinkEventType)); - } - - [Test] - public void CreateLinkEventCommand_Preview_IncrementsTotal() - { - var sut = GetCommandHandler(); - - var req = new CreateLinkEventCommand() - { - LinkId = _link.Id, - SessionId = "12345", - LinkEventType = LinkEventType.Preview - }; - - sut.Handle(req); - - _link = _db.SingleById(_link.Id); - - Assert.AreEqual(1, _link.TotalPreviews); - } - - [Test] - public void CreateLinkEventCommand_ClickThrough_CreatesRecord() - { - var sut = GetCommandHandler(); - - var req = new CreateLinkEventCommand() - { - LinkId = _link.Id, - SessionId = "12345", - LinkEventType = LinkEventType.ClickThrough - }; - - sut.Handle(req); - - Assert.IsTrue(_db.Exists(m => m.SessionId == req.SessionId && m.LinkEventType == req.LinkEventType)); - } - - [Test] - public void CreateLinkEventCommand_ClickThrough_IncrementsTotal() - { - var sut = GetCommandHandler(); - - var req = new CreateLinkEventCommand() - { - LinkId = _link.Id, - SessionId = "12345", - LinkEventType = LinkEventType.ClickThrough - }; - - sut.Handle(req); - - _link = _db.SingleById(_link.Id); - - Assert.AreEqual(1, _link.TotalClickThroughs); - } - - [Test] - public void CreateLinkEventCommand_TurnBack_CreatesRecord() - { - var sut = GetCommandHandler(); - - var req = new CreateLinkEventCommand() - { - LinkId = _link.Id, - SessionId = "12345", - LinkEventType = LinkEventType.TurnBack - }; - - sut.Handle(req); - - Assert.IsTrue(_db.Exists(m => m.SessionId == req.SessionId && m.LinkEventType == req.LinkEventType)); - } - - [Test] - public void CreateLinkEventCommand_TurnBack_IncrementsTotal() - { - var sut = GetCommandHandler(); - - var req = new CreateLinkEventCommand() - { - LinkId = _link.Id, - SessionId = "12345", - LinkEventType = LinkEventType.TurnBack - }; - - sut.Handle(req); - - _link = _db.SingleById(_link.Id); - - Assert.AreEqual(1, _link.TotalTurnBacks); - } - } -} diff --git a/src/IsNsfw.Tests/CreateLinkEventCommandValidatorTests.cs b/src/IsNsfw.Tests/CreateLinkEventCommandValidatorTests.cs deleted file mode 100644 index 16949be..0000000 --- a/src/IsNsfw.Tests/CreateLinkEventCommandValidatorTests.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using IsNsfw.Command; -using IsNsfw.Repository.Interface; -using Moq; -using NUnit.Framework; - -namespace IsNsfw.Tests -{ - public class CreateLinkEventCommandValidatorTests - { - [Test] - public void ErrorIfNullSessionId() - { - var sut = new CreateLinkEventCommandValidator(); - var results = sut.Validate(new CreateLinkEventCommand() { }); - - Assert.IsFalse(results.IsValid); - Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkEventCommand.SessionId))); - } - } -} diff --git a/src/IsNsfw.Tests/CreateLinkEventRequestTests.cs b/src/IsNsfw.Tests/CreateLinkEventRequestTests.cs new file mode 100644 index 0000000..5edb9b7 --- /dev/null +++ b/src/IsNsfw.Tests/CreateLinkEventRequestTests.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Diagnostics; +using System.Text; +using IsNsfw.Model; +using IsNsfw.Repository; +using IsNsfw.ServiceInterface; +using IsNsfw.ServiceModel; +using NUnit.Framework; +using ServiceStack; +using ServiceStack.Host; +using ServiceStack.Logging; +using ServiceStack.OrmLite; +using ServiceStack.Testing; + +namespace IsNsfw.Tests +{ + public class CreateLinkEventRequestTests + { + const string SessionId = "12345"; + + private readonly LinkRepository _linkRepo; + private readonly TagRepository _tagRepo; + private readonly IDbConnection _db; + private readonly OrmLiteConnectionFactory _dbFactory; + private ServiceStackHost _appHost; + private Link _link; + + public CreateLinkEventRequestTests() + { + LogManager.LogFactory = new DebugLogFactory(debugEnabled:true); + + _appHost = new BasicAppHost().Init(); + var container = _appHost.Container; + + _dbFactory = new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider); + OrmLiteConfig.BeforeExecFilter = cmd => Debug.WriteLine(cmd.GetDebugString()); + + _db = _dbFactory.Open(); + + _db.CreateTableIfNotExists(); + _db.CreateTableIfNotExists(); + _db.CreateTableIfNotExists(); + _db.CreateTableIfNotExists(); + _db.CreateTableIfNotExists(); + + _linkRepo = new LinkRepository(_dbFactory); + _tagRepo = new TagRepository(_dbFactory); + } + + [SetUp] + public void SetUp() + { + _link = new Link() { Key = "test", Url = "http://www.google.com", SessionId = "test", }; + _db.Save(_link); + } + + [TearDown] + public void TearDown() + { + _db.DeleteAll(); + _db.DeleteAll(); + _db.DeleteAll(); + _db.DeleteAll(); + _db.DeleteAll(); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _db.DeleteAll(); + _db.DeleteAll(); + _db.DeleteAll(); + _db.DeleteAll(); + _db.DeleteAll(); + _db.Dispose(); + + _appHost.Dispose(); + } + + public LinkService GetService(string sessionId = SessionId) + { + var ret = new LinkService(_linkRepo, _tagRepo); + + ret.Request = new BasicHttpRequest() + { + Items = + { + [Keywords.Session] = new AuthUserSession() { Id = sessionId } + } + }; + + return ret; + } + + [Test] + public void CreateLinkEventRequest_View_CreatesRecord() + { + var sut = GetService(); + + var req = new CreateLinkEventRequest() + { + Id = _link.Id, + LinkEventType = LinkEventType.View + }; + + sut.Post(req); + + Assert.IsTrue(_db.Exists(m => m.SessionId == SessionId && m.LinkEventType == req.LinkEventType)); + } + + [Test] + public void CreateLinkEventRequest_View_IncrementsTotal() + { + var sut = GetService(); + + var req = new CreateLinkEventRequest() + { + Id = _link.Id, + LinkEventType = LinkEventType.View + }; + + sut.Post(req); + + _link = _db.SingleById(_link.Id); + + Assert.AreEqual(1, _link.TotalViews); + } + + [Test] + public void CreateLinkEventRequest_Preview_CreatesRecord() + { + var sut = GetService(); + + var req = new CreateLinkEventRequest() + { + Id = _link.Id, + LinkEventType = LinkEventType.Preview + }; + + sut.Post(req); + + Assert.IsTrue(_db.Exists(m => m.SessionId == SessionId && m.LinkEventType == req.LinkEventType)); + } + + [Test] + public void CreateLinkEventRequest_Preview_IncrementsTotal() + { + var sut = GetService(); + + var req = new CreateLinkEventRequest() + { + Id = _link.Id, + LinkEventType = LinkEventType.Preview + }; + + sut.Post(req); + + _link = _db.SingleById(_link.Id); + + Assert.AreEqual(1, _link.TotalPreviews); + } + + [Test] + public void CreateLinkEventRequest_ClickThrough_CreatesRecord() + { + var sut = GetService(); + + var req = new CreateLinkEventRequest() + { + Id = _link.Id, + LinkEventType = LinkEventType.ClickThrough + }; + + sut.Post(req); + + Assert.IsTrue(_db.Exists(m => m.SessionId == SessionId && m.LinkEventType == req.LinkEventType)); + } + + [Test] + public void CreateLinkEventRequest_ClickThrough_IncrementsTotal() + { + var sut = GetService(); + + var req = new CreateLinkEventRequest() + { + Id = _link.Id, + LinkEventType = LinkEventType.ClickThrough + }; + + sut.Post(req); + + _link = _db.SingleById(_link.Id); + + Assert.AreEqual(1, _link.TotalClickThroughs); + } + + [Test] + public void CreateLinkEventRequest_TurnBack_CreatesRecord() + { + var sut = GetService(); + + var req = new CreateLinkEventRequest() + { + Id = _link.Id, + LinkEventType = LinkEventType.TurnBack + }; + + sut.Post(req); + + Assert.IsTrue(_db.Exists(m => m.SessionId == SessionId && m.LinkEventType == req.LinkEventType)); + } + + [Test] + public void CreateLinkEventRequest_TurnBack_IncrementsTotal() + { + var sut = GetService(); + + var req = new CreateLinkEventRequest() + { + Id = _link.Id, + LinkEventType = LinkEventType.TurnBack + }; + + sut.Post(req); + + _link = _db.SingleById(_link.Id); + + Assert.AreEqual(1, _link.TotalTurnBacks); + } + } +} diff --git a/src/IsNsfw.Tests/CreateLinkCommandTests.cs b/src/IsNsfw.Tests/CreateLinkRequestTests.cs similarity index 56% rename from src/IsNsfw.Tests/CreateLinkCommandTests.cs rename to src/IsNsfw.Tests/CreateLinkRequestTests.cs index 94eaad4..dc74503 100644 --- a/src/IsNsfw.Tests/CreateLinkCommandTests.cs +++ b/src/IsNsfw.Tests/CreateLinkRequestTests.cs @@ -1,34 +1,44 @@ using System; using System.Collections.Generic; using System.Data; +using System.Diagnostics; +using System.Linq; using System.Text; -using IsNsfw.Command; -using IsNsfw.Command.Domains; -using IsNsfw.Command.Interface; using IsNsfw.Model; using IsNsfw.Repository; using IsNsfw.Repository.Interface; using IsNsfw.ServiceInterface; +using IsNsfw.ServiceModel; +using IsNsfw.ServiceModel.Types; using Moq; using NUnit.Framework; using ServiceStack; using ServiceStack.Data; using ServiceStack.FluentValidation; +using ServiceStack.Host; +using ServiceStack.Logging; using ServiceStack.OrmLite; using ServiceStack.Testing; namespace IsNsfw.Tests { - public class CreateLinkCommandTests + public class CreateLinkRequestTests { private readonly LinkRepository _linkRepo; - private readonly LinkCommandHandlers _linkDomain; + private readonly TagRepository _tagRepo; private readonly IDbConnection _db; private readonly OrmLiteConnectionFactory _dbFactory; + private ServiceStackHost _appHost; - public CreateLinkCommandTests() + public CreateLinkRequestTests() { + LogManager.LogFactory = new DebugLogFactory(debugEnabled:true); + + _appHost = new BasicAppHost().Init(); + var container = _appHost.Container; + _dbFactory = new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider); + OrmLiteConfig.BeforeExecFilter = cmd => Debug.WriteLine(cmd.GetDebugString()); _db = _dbFactory.Open(); @@ -38,7 +48,7 @@ public CreateLinkCommandTests() _db.CreateTableIfNotExists(); _linkRepo = new LinkRepository(_dbFactory); - _linkDomain = new LinkCommandHandlers(_dbFactory); + _tagRepo = new TagRepository(_dbFactory); } [TearDown] @@ -58,34 +68,45 @@ public void OneTimeTearDown() _db.DeleteAll(); _db.DeleteAll(); _db.Dispose(); + + _appHost.Dispose(); } - public ICommandHandler GetCommandHandler() + public LinkService GetService(string sessionId = "12345") { - return new ValidationCommandHandlerDecorator(_linkDomain, new CreateLinkCommandValidator(_linkRepo)); + var ret = new LinkService(_linkRepo, _tagRepo); + + ret.Request = new BasicHttpRequest() + { + Items = + { + [Keywords.Session] = new AuthUserSession() { Id = sessionId } + } + }; + + return ret; } [Test] public void CanCreateLink() { - var sut = GetCommandHandler(); + var sut = GetService(); - var req = new CreateLinkCommand() + var req = new CreateLinkRequest() { Key = "Hello", Url = "http://www.google.com", - SessionId = "test", }; - sut.Handle(req); + var res = (LinkResponse)sut.Post(req); - Assert.AreNotEqual(0, req.Id); + Assert.AreNotEqual(0, res.Id); } [Test] public void CanCreateLinkWithTags() { - var sut = GetCommandHandler(); + var sut = GetService(); var tags = new [] { @@ -93,42 +114,44 @@ public void CanCreateLinkWithTags() new Tag() { Key = "T2" }, new Tag() { Key = "T3" }, }; + _db.SaveAll(tags); - var req = new CreateLinkCommand() + var req = new CreateLinkRequest() { Key = "Hello", Url = "http://www.google.com", - SessionId = "test", - TagIds = new HashSet() { tags[0].Id, tags[1].Id } + Tags = new HashSet() { tags[0].Key, tags[1].Key } }; - sut.Handle(req); + var res = (LinkResponse)sut.Post(req); - Assert.AreNotEqual(0, req.Id); + Assert.AreNotEqual(0, res.Id); + Assert.AreEqual(2, res.Tags.Count); + Assert.Contains(tags[0].Key, res.Tags.ToList()); + Assert.Contains(tags[1].Key, res.Tags.ToList()); } [Test] public void LinkPersistedInDatabase() { - var sut = GetCommandHandler(); + var sut = GetService(); - var req = new CreateLinkCommand() + var req = new CreateLinkRequest() { Key = "Hello", Url = "http://www.google.com", - SessionId = "test", }; - sut.Handle(req); + var res = (LinkResponse)sut.Post(req); - Assert.IsNotNull(_db.Single(m => m.Id == req.Id)); + Assert.IsNotNull(_db.Single(m => m.Id == res.Id)); } [Test] public void CreateLinkWithTagsPersistedInDatabase() { - var sut = GetCommandHandler(); + var sut = GetService(); var tags = new [] { @@ -138,67 +161,48 @@ public void CreateLinkWithTagsPersistedInDatabase() }; _db.SaveAll(tags); - var req = new CreateLinkCommand() + var req = new CreateLinkRequest() { Key = "Hello", Url = "http://www.google.com", - SessionId = "test", - TagIds = new HashSet() { tags[0].Id, tags[1].Id } + Tags = new HashSet() { tags[0].Key, tags[1].Key } }; - sut.Handle(req); + var res = (LinkResponse)sut.Post(req); - Assert.AreNotEqual(2, _db.Count(m => m.LinkId == req.Id)); + Assert.AreEqual(2, _db.Count(m => m.LinkId == res.Id)); } [Test] public void CreatedLinkContainsKey() { - var sut = GetCommandHandler(); + var sut = GetService(); - var req = new CreateLinkCommand() + var req = new CreateLinkRequest() { Key = "Hello", - Url = "http://www.google.com", - SessionId = "test", + Url = "http://www.google.com" }; - sut.Handle(req); + var res = (LinkResponse)sut.Post(req); - Assert.AreEqual(req.Key, _db.Single(m => m.Id == req.Id).Key); + Assert.AreEqual(req.Key, _db.Single(m => m.Id == res.Id).Key); } [Test] public void CreatedLinkContainsUrl() { - var sut = GetCommandHandler(); + var sut = GetService(); - var req = new CreateLinkCommand() - { - Key = "Hello", - Url = "http://www.google.com", - SessionId = "test", - }; - - sut.Handle(req); - - Assert.AreEqual(req.Url, _db.Single(m => m.Id == req.Id).Url); - } - - [Test] - public void CreateLinkThrowsIfDuplicateKey() - { - var sut = GetCommandHandler(); - - _db.Insert(new Link() { Key = "Hello", Url = "http://www.test.com", SessionId = "test" }); - - var req = new CreateLinkCommand() + var req = new CreateLinkRequest() { Key = "Hello", Url = "http://www.google.com" }; - Assert.Throws(() => sut.Handle(req)); + var res = (LinkResponse)sut.Post(req); + + Assert.AreEqual(req.Url, _db.Single(m => m.Id == res.Id).Url); } } } diff --git a/src/IsNsfw.Tests/CreateLinkRequestValidatorTests.cs b/src/IsNsfw.Tests/CreateLinkRequestValidatorTests.cs new file mode 100644 index 0000000..6fefae5 --- /dev/null +++ b/src/IsNsfw.Tests/CreateLinkRequestValidatorTests.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using IsNsfw.Model; +using IsNsfw.Repository; +using IsNsfw.Repository.Interface; +using IsNsfw.ServiceInterface.Validators; +using IsNsfw.ServiceModel; +using Moq; +using NUnit.Framework; +using ServiceStack.OrmLite; + +namespace IsNsfw.Tests +{ + public class CreateLinkRequestValidatorTests + { + private readonly TagRepository _tagRepo; + private readonly IDbConnection _db; + private readonly OrmLiteConnectionFactory _dbFactory; + + public CreateLinkRequestValidatorTests() + { + _dbFactory = new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider); + + _db = _dbFactory.Open(); + + _db.CreateTableIfNotExists(); + + _tagRepo = new TagRepository(_dbFactory); + } + + [TearDown] + public void TearDown() + { + _db.DeleteAll(); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _db.DeleteAll(); + _db.Dispose(); + } + + [Test] + public void ErrorIfNullUrl() + { + var sut = new CreateLinkRequestValidator(null, new TagValidator(_tagRepo)); + var results = sut.Validate(new CreateLinkRequest() { }); + + Assert.IsFalse(results.IsValid); + Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkRequest.Url))); + } + + [Test] + public void ErrorIfNullKey() + { + var sut = new CreateLinkRequestValidator(null, new TagValidator(_tagRepo)); + var results = sut.Validate(new CreateLinkRequest() { }); + + Assert.IsFalse(results.IsValid); + Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkRequest.Key))); + } + + //[Test] + //public void ErrorIfNullSessionId() + //{ + // var sut = new CreateLinkRequestValidator(null, new TagValidator(_tagRepo)); + // var results = sut.Validate(new CreateLinkRequest() { }); + + // Assert.IsFalse(results.IsValid); + // Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkRequest.SessionId))); + //} + + [Test] + public void ErrorIfBlankUrl() + { + var sut = new CreateLinkRequestValidator(null, new TagValidator(_tagRepo)); + var results = sut.Validate(new CreateLinkRequest() { Url = " " }); + + Assert.IsFalse(results.IsValid); + Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkRequest.Url))); + } + + [Test] + public void ErrorIfBlankKey() + { + var sut = new CreateLinkRequestValidator(null, new TagValidator(_tagRepo)); + var results = sut.Validate(new CreateLinkRequest() { Key = " " }); + + Assert.IsFalse(results.IsValid); + Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkRequest.Key))); + } + + //[Test] + //public void ErrorIfBlankSessionId() + //{ + // var sut = new CreateLinkRequestValidator(null, new TagValidator(_tagRepo)); + // var results = sut.Validate(new CreateLinkRequest() { SessionId = " " }); + + // Assert.IsFalse(results.IsValid); + // Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkRequest.SessionId))); + //} + + [Test] + public void RequiresValidUrl() + { + var linkRepo = new Mock(); + linkRepo.Setup(m => m.KeyExists(It.IsAny())).Returns(false); + + var sut = new CreateLinkRequestValidator(linkRepo.Object, new TagValidator(_tagRepo)); + var results = sut.Validate(new CreateLinkRequest() { Key = "Test", Url = "bad url" }); + + Assert.IsFalse(results.IsValid); + Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkRequest.Url))); + } + + [Test] + public void RequiresAtLeastOneTag() + { + var linkRepo = new Mock(); + linkRepo.Setup(m => m.KeyExists(It.IsAny())).Returns(false); + + var sut = new CreateLinkRequestValidator(linkRepo.Object, new TagValidator(_tagRepo)); + var results = sut.Validate(new CreateLinkRequest() { Key = "Test", Url = "bad url", Tags = new HashSet() { } }); + + Assert.IsFalse(results.IsValid); + Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkRequest.Tags))); + } + + [Test] + public void RequiresTagsThatExist() + { + var linkRepo = new Mock(); + linkRepo.Setup(m => m.KeyExists(It.IsAny())).Returns(false); + + var sut = new CreateLinkRequestValidator(linkRepo.Object, new TagValidator(_tagRepo)); + var results = sut.Validate(new CreateLinkRequest() { Key = "Test", Url = "bad url", Tags = new HashSet() { "T1" } }); + + Assert.IsFalse(results.IsValid); + Assert.IsTrue(results.Errors.Any(m => m.PropertyName.Contains(nameof(CreateLinkRequest.Tags)))); + } + + [Test] + public void RequiresUniqueKey() + { + var linkRepo = new Mock(); + linkRepo.Setup(m => m.KeyExists(It.IsAny())).Returns(true); + + _db.Insert(new Tag() { Key = "T1", Name = "Tag 1" }); + + var sut = new CreateLinkRequestValidator(linkRepo.Object, new TagValidator(_tagRepo)); + var results = sut.Validate(new CreateLinkRequest() { Url = "http://www.google.com", Key = "werd", Tags = new HashSet() { "T1" } }); + + Assert.IsFalse(results.IsValid); + Assert.IsTrue(results.Errors.Any(m => m.PropertyName == nameof(CreateLinkRequest.Key))); + } + } +} diff --git a/src/IsNsfw.Tests/IntegrationTest.cs b/src/IsNsfw.Tests/IntegrationTest.cs deleted file mode 100644 index b59ed28..0000000 --- a/src/IsNsfw.Tests/IntegrationTest.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Funq; -using ServiceStack; -using NUnit.Framework; -using IsNsfw.ServiceInterface; -using IsNsfw.ServiceModel; - -namespace IsNsfw.Tests -{ - public class IntegrationTest - { - const string BaseUri = "http://localhost:2000/"; - private readonly ServiceStackHost appHost; - - class AppHost : AppSelfHostBase - { - public AppHost() : base(nameof(IntegrationTest), typeof(MyServices).Assembly) { } - - public override void Configure(Container container) - { - } - } - - public IntegrationTest() - { - appHost = new AppHost() - .Init() - .Start(BaseUri); - } - - [OneTimeTearDown] - public void OneTimeTearDown() => appHost.Dispose(); - - public IServiceClient CreateClient() => new JsonServiceClient(BaseUri); - - [Test] - public void Can_call_Hello_Service() - { - var client = CreateClient(); - - var response = client.Get(new Hello { Name = "World" }); - - Assert.That(response.Result, Is.EqualTo("Hello, World!")); - } - } -} \ No newline at end of file diff --git a/src/IsNsfw.Tests/IsNsfw.Tests.csproj b/src/IsNsfw.Tests/IsNsfw.Tests.csproj index 9f6540c..930f927 100644 --- a/src/IsNsfw.Tests/IsNsfw.Tests.csproj +++ b/src/IsNsfw.Tests/IsNsfw.Tests.csproj @@ -7,7 +7,10 @@ - + + + + diff --git a/src/IsNsfw.Tests/LinkServiceTests.cs b/src/IsNsfw.Tests/LinkServiceTests.cs new file mode 100644 index 0000000..8289f9b --- /dev/null +++ b/src/IsNsfw.Tests/LinkServiceTests.cs @@ -0,0 +1,109 @@ +using System.Data; +using System.Diagnostics; +using Funq; +using IsNsfw.Model; +using IsNsfw.Repository; +using IsNsfw.Repository.Interface; +using ServiceStack; +using NUnit.Framework; +using IsNsfw.ServiceInterface; +using IsNsfw.ServiceInterface.Validators; +using IsNsfw.ServiceModel; +using ServiceStack.Data; +using ServiceStack.Host; +using ServiceStack.FluentValidation; +using ServiceStack.Logging; +using ServiceStack.OrmLite; +using ServiceStack.Testing; +using ServiceStack.Validation; + +namespace IsNsfw.Tests +{ + public class LinkServiceTests + { + const string BaseUri = "http://localhost:2000/"; + private readonly AppHost _appHost; + + public LinkServiceTests() + { + _appHost = new AppHost(); + + _appHost + .Init() + .Start(BaseUri); + } + + class AppHost : AppSelfHostBase + { + public IDbConnection Db; + private OrmLiteConnectionFactory _dbFactory; + + public AppHost() : base(nameof(LinkServiceTests), typeof(MyServices).Assembly) { } + + public override void Configure(Container container) + { + SetConfig(new HostConfig { DebugMode = true }); + + Plugins.Add(new ValidationFeature()); + + LogManager.LogFactory = new DebugLogFactory(debugEnabled:true); + + _dbFactory = new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider); + OrmLiteConfig.BeforeExecFilter = cmd => Debug.WriteLine(cmd.GetDebugString()); + + Db = _dbFactory.Open(); + + Db.CreateTableIfNotExists(); + Db.CreateTableIfNotExists(); + Db.CreateTableIfNotExists(); + Db.CreateTableIfNotExists(); + + container.Register(_dbFactory); + container.RegisterAs(); + container.RegisterAs(); + container.RegisterAs(); + container.RegisterValidators(typeof(CreateLinkRequestValidator).Assembly); + } + } + + + [TearDown] + public void TearDown() + { + _appHost.Db.DeleteAll(); + _appHost.Db.DeleteAll(); + _appHost.Db.DeleteAll(); + _appHost.Db.DeleteAll(); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _appHost.Db.DeleteAll(); + _appHost.Db.DeleteAll(); + _appHost.Db.DeleteAll(); + _appHost.Db.DeleteAll(); + _appHost.Db.Dispose(); + + _appHost.Dispose(); + } + + public IServiceClient CreateClient() => new JsonServiceClient(BaseUri); + + [Test] + public void CreateLinkThrowsIfDuplicateKey() + { + var client = CreateClient(); + + _appHost.Db.Insert(new Link() { Key = "Hello", Url = "http://www.test.com", SessionId = "test" }); + + var req = new CreateLinkRequest() + { + Key = "Hello", + Url = "http://www.google.com" + }; + + Assert.Throws(() => client.Post(req)); + } + } +} \ No newline at end of file diff --git a/src/IsNsfw.sln b/src/IsNsfw.sln index 0b611f4..2cb16a2 100644 --- a/src/IsNsfw.sln +++ b/src/IsNsfw.sln @@ -11,17 +11,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsNsfw.ServiceModel", "IsNs EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsNsfw.Tests", "IsNsfw.Tests\IsNsfw.Tests.csproj", "{455EC1EF-134F-4CD4-9C78-E813E4E6D8F6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IsNsfw.Model", "IsNsfw.Model\IsNsfw.Model.csproj", "{31C3317B-F9DE-4069-A483-100B3E492511}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsNsfw.Model", "IsNsfw.Model\IsNsfw.Model.csproj", "{31C3317B-F9DE-4069-A483-100B3E492511}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IsNsfw.Migration", "IsNsfw.Migration\IsNsfw.Migration.csproj", "{6EB5465A-5A1C-419D-A89F-B7C1DD104AEA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsNsfw.Migration", "IsNsfw.Migration\IsNsfw.Migration.csproj", "{6EB5465A-5A1C-419D-A89F-B7C1DD104AEA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IsNsfw.Command", "IsNsfw.Command\IsNsfw.Command.csproj", "{CA6EEDE5-E092-4CDB-A832-0B3CE3D4C95C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsNsfw.Repository", "IsNsfw.Repository\IsNsfw.Repository.csproj", "{9767D3F8-2EA0-4F8A-AF2A-16E944A56E87}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IsNsfw.Repository", "IsNsfw.Repository\IsNsfw.Repository.csproj", "{9767D3F8-2EA0-4F8A-AF2A-16E944A56E87}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsNsfw.Repository.Interface", "IsNsfw.Repository.Interface\IsNsfw.Repository.Interface.csproj", "{C8FB8EBF-0CBD-4A75-90EC-540739278B6E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IsNsfw.Repository.Interface", "IsNsfw.Repository.Interface\IsNsfw.Repository.Interface.csproj", "{C8FB8EBF-0CBD-4A75-90EC-540739278B6E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IsNsfw.Command.Interface", "IsNsfw.Command.Interface\IsNsfw.Command.Interface.csproj", "{0234C1C8-6697-4DCC-AE53-3EFD02A23A08}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IsNsfw.Service", "IsNsfw.Service\IsNsfw.Service.csproj", "{DD64874C-464F-40B3-9965-C1A99F4816D4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -53,10 +51,6 @@ Global {6EB5465A-5A1C-419D-A89F-B7C1DD104AEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {6EB5465A-5A1C-419D-A89F-B7C1DD104AEA}.Release|Any CPU.ActiveCfg = Release|Any CPU {6EB5465A-5A1C-419D-A89F-B7C1DD104AEA}.Release|Any CPU.Build.0 = Release|Any CPU - {CA6EEDE5-E092-4CDB-A832-0B3CE3D4C95C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CA6EEDE5-E092-4CDB-A832-0B3CE3D4C95C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CA6EEDE5-E092-4CDB-A832-0B3CE3D4C95C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CA6EEDE5-E092-4CDB-A832-0B3CE3D4C95C}.Release|Any CPU.Build.0 = Release|Any CPU {9767D3F8-2EA0-4F8A-AF2A-16E944A56E87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9767D3F8-2EA0-4F8A-AF2A-16E944A56E87}.Debug|Any CPU.Build.0 = Debug|Any CPU {9767D3F8-2EA0-4F8A-AF2A-16E944A56E87}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -65,10 +59,10 @@ Global {C8FB8EBF-0CBD-4A75-90EC-540739278B6E}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8FB8EBF-0CBD-4A75-90EC-540739278B6E}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8FB8EBF-0CBD-4A75-90EC-540739278B6E}.Release|Any CPU.Build.0 = Release|Any CPU - {0234C1C8-6697-4DCC-AE53-3EFD02A23A08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0234C1C8-6697-4DCC-AE53-3EFD02A23A08}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0234C1C8-6697-4DCC-AE53-3EFD02A23A08}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0234C1C8-6697-4DCC-AE53-3EFD02A23A08}.Release|Any CPU.Build.0 = Release|Any CPU + {DD64874C-464F-40B3-9965-C1A99F4816D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD64874C-464F-40B3-9965-C1A99F4816D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD64874C-464F-40B3-9965-C1A99F4816D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD64874C-464F-40B3-9965-C1A99F4816D4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/IsNsfw/IsNsfw.csproj b/src/IsNsfw/IsNsfw.csproj index f55f816..14c3132 100644 --- a/src/IsNsfw/IsNsfw.csproj +++ b/src/IsNsfw/IsNsfw.csproj @@ -16,6 +16,7 @@ +