diff --git a/BotwinMediator/BotwinMediator.csproj b/BotwinMediator/BotwinMediator.csproj index ba38131..acc63fe 100644 --- a/BotwinMediator/BotwinMediator.csproj +++ b/BotwinMediator/BotwinMediator.csproj @@ -9,6 +9,7 @@ + diff --git a/BotwinMediator/Features/Films/FilmsModule.cs b/BotwinMediator/Features/Films/FilmsModule.cs index 4e7fdfa..ac2a85c 100644 --- a/BotwinMediator/Features/Films/FilmsModule.cs +++ b/BotwinMediator/Features/Films/FilmsModule.cs @@ -59,7 +59,7 @@ public FilmsModule(Handler handler) : base("/api/films") { var command = new CreateFilmCommand(result.Data); this.handler.Handle(command); - res.StatusCode = 204; + res.StatusCode = 201; } catch (Exception) { diff --git a/BotwinMediator/Features/Films/ListFilmById/ListFilmsByIdCommandHandler.cs b/BotwinMediator/Features/Films/ListFilmById/ListFilmsByIdCommandHandler.cs index 6e99df5..e7ee6b3 100644 --- a/BotwinMediator/Features/Films/ListFilmById/ListFilmsByIdCommandHandler.cs +++ b/BotwinMediator/Features/Films/ListFilmById/ListFilmsByIdCommandHandler.cs @@ -27,6 +27,11 @@ protected override object Execute(ListFilmsByIdCommand command) //Use shared query to get film var film = this.listFilmByIdQuery.Execute(command.Id); + + if (film == null) + { + return null; + } var director = this.getDirectorByIdQuery.Execute(film.DirectorId); film.Director = director; diff --git a/BotwinMediator/Program.cs b/BotwinMediator/Program.cs index 892c9bb..b386cd6 100644 --- a/BotwinMediator/Program.cs +++ b/BotwinMediator/Program.cs @@ -1,19 +1,16 @@ namespace BotwinMediator { - using System.IO; + using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; public class Program { public static void Main(string[] args) { - var host = new WebHostBuilder() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseKestrel() + WebHost.CreateDefaultBuilder(args) .UseStartup() - .Build(); - - host.Run(); + .Build() + .Run(); } } } diff --git a/BotwinMediator/Startup.cs b/BotwinMediator/Startup.cs index d88b60c..6514268 100644 --- a/BotwinMediator/Startup.cs +++ b/BotwinMediator/Startup.cs @@ -33,7 +33,7 @@ public void ConfigureServices(IServiceCollection services) new UpdateFilmCommandHandler(s.GetRequiredService(),s.GetRequiredService()) })); - services.AddBotwin(Assembly.GetEntryAssembly(), typeof(FilmValidator).Assembly); + services.AddBotwin(Assembly.GetCallingAssembly(), typeof(FilmValidator).Assembly); } public void Configure(IApplicationBuilder app) diff --git a/DDDScot.sln b/DDDScot.sln index 51bc8e0..61bdf09 100644 --- a/DDDScot.sln +++ b/DDDScot.sln @@ -31,6 +31,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2. MediatR Project Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediatRWebAPI.Tests", "MediatRWebAPI.Tests\MediatRWebAPI.Tests.csproj", "{E9EA372B-B08D-4C2E-B78A-948810D79F02}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3. Functional Project Tests", "3. Functional Project Tests", "{F03EA84D-15EF-4E1D-8FBE-79FFC0FDC277}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionalProjectTests", "FunctionalProjectTests\FunctionalProjectTests.csproj", "{0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -101,6 +105,18 @@ Global {E9EA372B-B08D-4C2E-B78A-948810D79F02}.Release|x64.Build.0 = Release|Any CPU {E9EA372B-B08D-4C2E-B78A-948810D79F02}.Release|x86.ActiveCfg = Release|Any CPU {E9EA372B-B08D-4C2E-B78A-948810D79F02}.Release|x86.Build.0 = Release|Any CPU + {0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A}.Release|Any CPU.Build.0 = Release|Any CPU + {0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A}.Debug|x64.ActiveCfg = Debug|Any CPU + {0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A}.Debug|x64.Build.0 = Debug|Any CPU + {0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A}.Debug|x86.ActiveCfg = Debug|Any CPU + {0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A}.Debug|x86.Build.0 = Debug|Any CPU + {0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A}.Release|x64.ActiveCfg = Release|Any CPU + {0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A}.Release|x64.Build.0 = Release|Any CPU + {0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A}.Release|x86.ActiveCfg = Release|Any CPU + {0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {626AD192-F5D0-4AE1-AEEC-7B8FE40E927F} = {66D3DCED-78BD-4DB8-8C8D-3A5FA09543C6} @@ -115,5 +131,7 @@ Global {2974A21D-76F8-4248-BF01-E7B5BDA6A6FF} = {DD7F5385-6594-4AF5-ADE9-ABC65C2A29BC} {694570A7-D60F-4058-A86F-B146204891B7} = {44C1DD68-1371-456C-9541-BCC3CEE8A2BE} {E9EA372B-B08D-4C2E-B78A-948810D79F02} = {694570A7-D60F-4058-A86F-B146204891B7} + {F03EA84D-15EF-4E1D-8FBE-79FFC0FDC277} = {44C1DD68-1371-456C-9541-BCC3CEE8A2BE} + {0A4CAD93-00B1-4ED6-9C92-3DB4C4F8509A} = {F03EA84D-15EF-4E1D-8FBE-79FFC0FDC277} EndGlobalSection EndGlobal diff --git a/FunctionalProject/Features/Films/FilmsModule.cs b/FunctionalProject/Features/Films/FilmsModule.cs deleted file mode 100644 index cbaa150..0000000 --- a/FunctionalProject/Features/Films/FilmsModule.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace FunctionalProject.Features.Films -{ - using System.Threading.Tasks; - using Botwin; - using Botwin.Response; - using Microsoft.AspNetCore.Http; - - public class FilmsModule : BotwinModule - { - public FilmsModule() : base("/api/films") - { - this.Get("/", GetFilms); - } - - private async Task GetFilms(HttpContext context) - { - - } - } -} diff --git a/FunctionalProject/Features/Films/RouteHandlers.cs b/FunctionalProject/Features/Films/RouteHandlers.cs deleted file mode 100644 index 7a6d379..0000000 --- a/FunctionalProject/Features/Films/RouteHandlers.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FunctionalProject.Features.Films -{ - public static class RouteHandlers - { - - } -} diff --git a/FunctionalProject/Features/FuncFilms/CreateFilm/CreateFilmRoute.cs b/FunctionalProject/Features/FuncFilms/CreateFilm/CreateFilmRoute.cs new file mode 100644 index 0000000..43e4146 --- /dev/null +++ b/FunctionalProject/Features/FuncFilms/CreateFilm/CreateFilmRoute.cs @@ -0,0 +1,20 @@ +namespace FunctionalProject.Features.FuncFilms.CreateFilm +{ + using System; + using Models; + + public static class CreateFilmRoute + { + public static void Handle(Film film, Func validUserQuery) + { + if (!validUserQuery()) + { + throw new InvalidOperationException(); + } + + //Do some special MEGA CORP business validation + + //Save to database by writing SQL here + } + } +} diff --git a/FunctionalProject/Features/FuncFilms/DeleteFilm/DeleteFilmRoute.cs b/FunctionalProject/Features/FuncFilms/DeleteFilm/DeleteFilmRoute.cs new file mode 100644 index 0000000..a942bd4 --- /dev/null +++ b/FunctionalProject/Features/FuncFilms/DeleteFilm/DeleteFilmRoute.cs @@ -0,0 +1,17 @@ +namespace FunctionalProject.Features.FuncFilms.DeleteFilm +{ + using System; + + public static class DeleteFilmRoute + { + public static void Handle(int id, Func validUserQuery) + { + if (!validUserQuery()) + { + throw new InvalidOperationException(); + } + + //Write some SQL to delete from DB + } + } +} diff --git a/FunctionalProject/Features/DelegateFilms/FilmsModule.cs b/FunctionalProject/Features/FuncFilms/FilmsModule.cs similarity index 57% rename from FunctionalProject/Features/DelegateFilms/FilmsModule.cs rename to FunctionalProject/Features/FuncFilms/FilmsModule.cs index 4591b2b..261b5b6 100644 --- a/FunctionalProject/Features/DelegateFilms/FilmsModule.cs +++ b/FunctionalProject/Features/FuncFilms/FilmsModule.cs @@ -1,4 +1,4 @@ -namespace FunctionalProject.Features.DelegateFilms +namespace FunctionalProject.Features.FuncFilms { using System; using System.Threading.Tasks; @@ -12,11 +12,38 @@ public class FilmsModule : BotwinModule { - public FilmsModule() : base("/api/films") + public FilmsModule() : base("/api/funcy/films") { this.Get("/", this.GetFilms); this.Get("/{id:int}", this.GetFilmById); this.Put("/{id:int}", this.UpdateFilm); + this.Post("/", this.CreateFilm); + this.Delete("/{id:int}", this.DeleteFilm); + } + + private async Task CreateFilm(HttpContext context) + { + var result = context.Request.BindAndValidate(); + + if (!result.ValidationResult.IsValid) + { + context.Response.StatusCode = 422; + await context.Response.Negotiate(result.ValidationResult.GetFormattedErrors()); + return; + } + + try + { + var handler = RouteHandlers.CreateFilmHandler; + + handler(result.Data); + + context.Response.StatusCode = 201; + } + catch (Exception) + { + context.Response.StatusCode = 403; + } } private async Task UpdateFilm(HttpContext context) @@ -35,7 +62,7 @@ private async Task UpdateFilm(HttpContext context) var handler = RouteHandlers.UpdateFilmHandler; handler(context.GetRouteData().As("id"), result.Data); - + context.Response.StatusCode = 204; } catch (Exception) @@ -67,5 +94,23 @@ private async Task GetFilmById(HttpContext context) await context.Response.AsJson(film); } + + private Task DeleteFilm(HttpContext context) + { + try + { + var handler = RouteHandlers.DeleteFilmHandler; + + handler(context.GetRouteData().As("id")); + + context.Response.StatusCode = 204; + } + catch (Exception) + { + context.Response.StatusCode = 403; + } + + return Task.CompletedTask; + } } } diff --git a/FunctionalProject/Features/DelegateFilms/ListFilmById/ListFilmByIdRoute.cs b/FunctionalProject/Features/FuncFilms/ListFilmById/ListFilmByIdRoute.cs similarity index 77% rename from FunctionalProject/Features/DelegateFilms/ListFilmById/ListFilmByIdRoute.cs rename to FunctionalProject/Features/FuncFilms/ListFilmById/ListFilmByIdRoute.cs index 5a660d7..2b30938 100644 --- a/FunctionalProject/Features/DelegateFilms/ListFilmById/ListFilmByIdRoute.cs +++ b/FunctionalProject/Features/FuncFilms/ListFilmById/ListFilmByIdRoute.cs @@ -1,4 +1,4 @@ -namespace FunctionalProject.Features.DelegateFilms.ListFilmById +namespace FunctionalProject.Features.FuncFilms.ListFilmById { using System; using System.Collections.Generic; @@ -9,6 +9,11 @@ public static class ListFilmByIdRoute public static Film Handle(int id, Func listFilmById, Func getDirectorById, Func> getCastByFilmIdQuery) { var film = listFilmById(id); + + if (film == null) + { + return null; + } var director = getDirectorById(film.DirectorId); film.Director = director; diff --git a/FunctionalProject/Features/DelegateFilms/ListFilms/ListFilmsRoute.cs b/FunctionalProject/Features/FuncFilms/ListFilms/ListFilmsRoute.cs similarity index 82% rename from FunctionalProject/Features/DelegateFilms/ListFilms/ListFilmsRoute.cs rename to FunctionalProject/Features/FuncFilms/ListFilms/ListFilmsRoute.cs index 7e0197e..2783be7 100644 --- a/FunctionalProject/Features/DelegateFilms/ListFilms/ListFilmsRoute.cs +++ b/FunctionalProject/Features/FuncFilms/ListFilms/ListFilmsRoute.cs @@ -1,4 +1,4 @@ -namespace FunctionalProject.Features.DelegateFilms.ListFilms +namespace FunctionalProject.Features.FuncFilms.ListFilms { using System.Collections.Generic; using Models; diff --git a/FunctionalProject/Features/DelegateFilms/RouteHandlers.cs b/FunctionalProject/Features/FuncFilms/RouteHandlers.cs similarity index 66% rename from FunctionalProject/Features/DelegateFilms/RouteHandlers.cs rename to FunctionalProject/Features/FuncFilms/RouteHandlers.cs index da3444e..72af230 100644 --- a/FunctionalProject/Features/DelegateFilms/RouteHandlers.cs +++ b/FunctionalProject/Features/FuncFilms/RouteHandlers.cs @@ -1,10 +1,12 @@ -namespace FunctionalProject.Features.DelegateFilms +namespace FunctionalProject.Features.FuncFilms { using System; using System.Collections.Generic; - using FunctionalProject.Features.DelegateFilms.ListFilmById; - using FunctionalProject.Features.DelegateFilms.ListFilms; - using FunctionalProject.Features.DelegateFilms.UpdateFilm; + using FunctionalProject.Features.FuncFilms.CreateFilm; + using FunctionalProject.Features.FuncFilms.DeleteFilm; + using FunctionalProject.Features.FuncFilms.ListFilmById; + using FunctionalProject.Features.FuncFilms.ListFilms; + using FunctionalProject.Features.FuncFilms.UpdateFilm; using Models; public static class RouteHandlers @@ -15,6 +17,10 @@ public static class RouteHandlers private static Action updateFilm; + private static Action createFilm; + + private static Action deleteFilm; + //private static Func getFilmyById = i => new Film { Id = 1, Name = "Pulp Fiction", DirectorId = 1 }; public static Func> ListFilmsHandler @@ -51,5 +57,17 @@ public static Action UpdateFilmHandler set => updateFilm = value; } + + public static Action CreateFilmHandler + { + get => createFilm ?? (film => CreateFilmRoute.Handle(film, () => new Random().Next() % 2 == 0)); + set => createFilm = value; + } + + public static Action DeleteFilmHandler + { + get => deleteFilm ?? (id => DeleteFilmRoute.Handle(id, () => new Random().Next() % 2 == 0)); + set => deleteFilm = value; + } } } diff --git a/FunctionalProject/Features/DelegateFilms/UpdateFilm/UpdateFilmRoute.cs b/FunctionalProject/Features/FuncFilms/UpdateFilm/UpdateFilmRoute.cs similarity index 91% rename from FunctionalProject/Features/DelegateFilms/UpdateFilm/UpdateFilmRoute.cs rename to FunctionalProject/Features/FuncFilms/UpdateFilm/UpdateFilmRoute.cs index 9ed63e3..2fb5b8b 100644 --- a/FunctionalProject/Features/DelegateFilms/UpdateFilm/UpdateFilmRoute.cs +++ b/FunctionalProject/Features/FuncFilms/UpdateFilm/UpdateFilmRoute.cs @@ -1,4 +1,4 @@ -namespace FunctionalProject.Features.DelegateFilms.UpdateFilm +namespace FunctionalProject.Features.FuncFilms.UpdateFilm { using System; using Models; diff --git a/FunctionalProject/Features/NamedDelegatesFilms/CastMembers/GetCastByFilmIdQuery.cs b/FunctionalProject/Features/NamedDelegatesFilms/CastMembers/GetCastByFilmIdQuery.cs new file mode 100644 index 0000000..2a5a1bd --- /dev/null +++ b/FunctionalProject/Features/NamedDelegatesFilms/CastMembers/GetCastByFilmIdQuery.cs @@ -0,0 +1,15 @@ +namespace FunctionalProject.Features.NamedDelegatesFilms.CastMembers +{ + using System.Collections.Generic; + using Models; + + public static class GetCastByFilmIdQuery + { + public static IEnumerable Execute(int filmId) + { + //Do some SQL + + return new[] { new CastMember { Name = "John Travolta" }, new CastMember { Name = "Samuel L Jackson" } }; + } + } +} diff --git a/FunctionalProject/Features/NamedDelegatesFilms/Directors/GetDirectorByIdQuery.cs b/FunctionalProject/Features/NamedDelegatesFilms/Directors/GetDirectorByIdQuery.cs new file mode 100644 index 0000000..6e44a88 --- /dev/null +++ b/FunctionalProject/Features/NamedDelegatesFilms/Directors/GetDirectorByIdQuery.cs @@ -0,0 +1,14 @@ +namespace FunctionalProject.Features.NamedDelegatesFilms.Directors +{ + using Models; + + public static class GetDirectorByIdQuery + { + public static Director Execute(int id) + { + //Do some SQL + + return new Director { Name = "Steven Spielberg" }; + } + } +} diff --git a/FunctionalProject/Features/NamedDelegatesFilms/Films/CreateFilm/CreateFilmRoute.cs b/FunctionalProject/Features/NamedDelegatesFilms/Films/CreateFilm/CreateFilmRoute.cs new file mode 100644 index 0000000..3a7d1aa --- /dev/null +++ b/FunctionalProject/Features/NamedDelegatesFilms/Films/CreateFilm/CreateFilmRoute.cs @@ -0,0 +1,20 @@ +namespace FunctionalProject.Features.NamedDelegatesFilms.Films.CreateFilm +{ + using System; + using Models; + + public static class CreateFilmRoute + { + public static void Handle(Film film, ValidUserDelegate validUserQuery) + { + if (!validUserQuery()) + { + throw new InvalidOperationException(); + } + + //Do some special MEGA CORP business validation + + //Save to database by writing SQL here + } + } +} diff --git a/FunctionalProject/Features/NamedDelegatesFilms/Films/Delegates.cs b/FunctionalProject/Features/NamedDelegatesFilms/Films/Delegates.cs new file mode 100644 index 0000000..a05566f --- /dev/null +++ b/FunctionalProject/Features/NamedDelegatesFilms/Films/Delegates.cs @@ -0,0 +1,21 @@ +namespace FunctionalProject.Features.NamedDelegatesFilms.Films +{ + using System.Collections.Generic; + using Models; + + public delegate Film ListFilmByIdDelegate(int id); + + public delegate void CreateFilmDelegate(Film film); + + public delegate void DeleteFilmDelegate(int id); + + public delegate IEnumerable ListFilmsDelegate(); + + public delegate void UpdateFilmDelegate(int id, Film film); + + public delegate bool ValidUserDelegate(); + + public delegate Director GetDirectorByIdDelegate(int id); + + public delegate IEnumerable GetCastByFilmIdDelegate(int filmId); +} diff --git a/FunctionalProject/Features/NamedDelegatesFilms/Films/DeleteFilm/DeleteFilmRoute.cs b/FunctionalProject/Features/NamedDelegatesFilms/Films/DeleteFilm/DeleteFilmRoute.cs new file mode 100644 index 0000000..d5ab6f3 --- /dev/null +++ b/FunctionalProject/Features/NamedDelegatesFilms/Films/DeleteFilm/DeleteFilmRoute.cs @@ -0,0 +1,17 @@ +namespace FunctionalProject.Features.NamedDelegatesFilms.Films.DeleteFilm +{ + using System; + + public static class DeleteFilmRoute + { + public static void Handle(int id, ValidUserDelegate validUserQuery) + { + if (!validUserQuery()) + { + throw new InvalidOperationException(); + } + + //Write some SQL to delete from DB + } + } +} diff --git a/FunctionalProject/Features/NamedDelegatesFilms/Films/FilmsModule.cs b/FunctionalProject/Features/NamedDelegatesFilms/Films/FilmsModule.cs new file mode 100644 index 0000000..508f666 --- /dev/null +++ b/FunctionalProject/Features/NamedDelegatesFilms/Films/FilmsModule.cs @@ -0,0 +1,116 @@ +namespace FunctionalProject.Features.NamedDelegatesFilms.Films +{ + using System; + using System.Threading.Tasks; + using Botwin; + using Botwin.ModelBinding; + using Botwin.Request; + using Botwin.Response; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Routing; + using Models; + + public class FilmsModule : BotwinModule + { + public FilmsModule() : base("/api/delegate/films") + { + this.Get("/", this.GetFilms); + this.Get("/{id:int}", this.GetFilmById); + this.Put("/{id:int}", this.UpdateFilm); + this.Post("/", this.CreateFilm); + this.Delete("/{id:int}", this.DeleteFilm); + } + + private async Task CreateFilm(HttpContext context) + { + var result = context.Request.BindAndValidate(); + + if (!result.ValidationResult.IsValid) + { + context.Response.StatusCode = 422; + await context.Response.Negotiate(result.ValidationResult.GetFormattedErrors()); + return; + } + + try + { + var handler = RouteHandlers.CreateFilmHandler; + + handler(result.Data); + + context.Response.StatusCode = 201; + } + catch (Exception) + { + context.Response.StatusCode = 403; + } + } + + private async Task UpdateFilm(HttpContext context) + { + var result = context.Request.BindAndValidate(); + + if (!result.ValidationResult.IsValid) + { + context.Response.StatusCode = 422; + await context.Response.Negotiate(result.ValidationResult.GetFormattedErrors()); + return; + } + + try + { + var handler = RouteHandlers.UpdateFilmHandler; + + handler(context.GetRouteData().As("id"), result.Data); + + context.Response.StatusCode = 204; + } + catch (Exception) + { + context.Response.StatusCode = 403; + } + } + + private async Task GetFilms(HttpContext context) + { + var handler = RouteHandlers.ListFilmsHandler; + + var films = handler(); + + await context.Response.AsJson(films); + } + + private async Task GetFilmById(HttpContext context) + { + var handler = RouteHandlers.ListFilmByIdHandler; + + var film = handler(context.GetRouteData().As("id")); + + if (film == null) + { + context.Response.StatusCode = 404; + return; + } + + await context.Response.AsJson(film); + } + + private Task DeleteFilm(HttpContext context) + { + try + { + var handler = RouteHandlers.DeleteFilmHandler; + + handler(context.GetRouteData().As("id")); + + context.Response.StatusCode = 204; + } + catch (Exception) + { + context.Response.StatusCode = 403; + } + + return Task.CompletedTask; + } + } +} diff --git a/FunctionalProject/Features/NamedDelegatesFilms/Films/ListFilmById/ListFilmByIdRoute.cs b/FunctionalProject/Features/NamedDelegatesFilms/Films/ListFilmById/ListFilmByIdRoute.cs new file mode 100644 index 0000000..5d2f35d --- /dev/null +++ b/FunctionalProject/Features/NamedDelegatesFilms/Films/ListFilmById/ListFilmByIdRoute.cs @@ -0,0 +1,25 @@ +namespace FunctionalProject.Features.NamedDelegatesFilms.Films.ListFilmById +{ + using Models; + + public static class ListFilmByIdRoute + { + public static Film Handle(int id, ListFilmByIdDelegate listFilmById, GetDirectorByIdDelegate getDirectorByIdDelegate, GetCastByFilmIdDelegate getCastByFilmIdDelegateQuery) + { + var film = listFilmById(id); + + if (film == null) + { + return null; + } + + var director = getDirectorByIdDelegate(film.DirectorId); + film.Director = director; + + var cast = getCastByFilmIdDelegateQuery(id); + film.Cast = cast; + + return film; + } + } +} diff --git a/FunctionalProject/Features/NamedDelegatesFilms/Films/ListFilms/ListFilmsRoute.cs b/FunctionalProject/Features/NamedDelegatesFilms/Films/ListFilms/ListFilmsRoute.cs new file mode 100644 index 0000000..2cb1b9d --- /dev/null +++ b/FunctionalProject/Features/NamedDelegatesFilms/Films/ListFilms/ListFilmsRoute.cs @@ -0,0 +1,13 @@ +namespace FunctionalProject.Features.NamedDelegatesFilms.Films.ListFilms +{ + using System.Collections.Generic; + using Models; + + public static class ListFilmsRoute + { + public static IEnumerable Handle() + { + return new[] { new Film { Id = 1, Name = "Pulp Fiction" }, new Film { Id = 2, Name = "Trainspotting" } }; + } + } +} diff --git a/FunctionalProject/Features/NamedDelegatesFilms/Films/ListFilmsByIdQuery/ListFilmsByIdQuery.cs b/FunctionalProject/Features/NamedDelegatesFilms/Films/ListFilmsByIdQuery/ListFilmsByIdQuery.cs new file mode 100644 index 0000000..3eec6a6 --- /dev/null +++ b/FunctionalProject/Features/NamedDelegatesFilms/Films/ListFilmsByIdQuery/ListFilmsByIdQuery.cs @@ -0,0 +1,12 @@ +namespace FunctionalProject.Features.NamedDelegatesFilms.Films.ListFilmsByIdQuery +{ + using Models; + + public static class ListFilmsByIdQuery + { + public static Film Execute(int id) + { + return new Film { Id = 1, Name = "Pulp Fiction", DirectorId = 1 }; + } + } +} diff --git a/FunctionalProject/Features/NamedDelegatesFilms/Films/Permissions/ValidUserQuery.cs b/FunctionalProject/Features/NamedDelegatesFilms/Films/Permissions/ValidUserQuery.cs new file mode 100644 index 0000000..9ea76bc --- /dev/null +++ b/FunctionalProject/Features/NamedDelegatesFilms/Films/Permissions/ValidUserQuery.cs @@ -0,0 +1,14 @@ +namespace FunctionalProject.Features.NamedDelegatesFilms.Films.Permissions +{ + using System; + + + + public static class ValidUserQuery + { + public static bool Execute() + { + return new Random().Next() % 2 == 0; + } + } +} diff --git a/FunctionalProject/Features/NamedDelegatesFilms/Films/RouteHandlers.cs b/FunctionalProject/Features/NamedDelegatesFilms/Films/RouteHandlers.cs new file mode 100644 index 0000000..41d30bc --- /dev/null +++ b/FunctionalProject/Features/NamedDelegatesFilms/Films/RouteHandlers.cs @@ -0,0 +1,45 @@ +namespace FunctionalProject.Features.NamedDelegatesFilms.Films +{ + using FunctionalProject.Features.FuncFilms.UpdateFilm; + using FunctionalProject.Features.NamedDelegatesFilms.CastMembers; + using FunctionalProject.Features.NamedDelegatesFilms.Directors; + using FunctionalProject.Features.NamedDelegatesFilms.Films.CreateFilm; + using FunctionalProject.Features.NamedDelegatesFilms.Films.DeleteFilm; + using FunctionalProject.Features.NamedDelegatesFilms.Films.ListFilmById; + using FunctionalProject.Features.NamedDelegatesFilms.Films.ListFilms; + using FunctionalProject.Features.NamedDelegatesFilms.Films.Permissions; + + public static class RouteHandlers + { + static RouteHandlers() + { + CreateFilmHandler = film => CreateFilmRoute.Handle(film, () => ValidUserQuery.Execute()); + + DeleteFilmHandler = id => DeleteFilmRoute.Handle(id, () => ValidUserQuery.Execute()); + + ListFilmByIdHandler = id => ListFilmByIdRoute.Handle( + id, + filmId => ListFilmsByIdQuery.ListFilmsByIdQuery.Execute(id), + dirId => GetDirectorByIdQuery.Execute(dirId), + filmId => GetCastByFilmIdQuery.Execute(id) + ); + + ListFilmsHandler = () => ListFilmsRoute.Handle(); + + UpdateFilmHandler = (id, film) => UpdateFilmRoute.Handle( + id, + film, () => ValidUserQuery.Execute(), + filmId => ListFilmsByIdQuery.ListFilmsByIdQuery.Execute(filmId)); + } + + public static CreateFilmDelegate CreateFilmHandler; + + public static ListFilmByIdDelegate ListFilmByIdHandler; + + public static DeleteFilmDelegate DeleteFilmHandler; + + public static ListFilmsDelegate ListFilmsHandler; + + public static UpdateFilmDelegate UpdateFilmHandler; + } +} diff --git a/FunctionalProject/Features/NamedDelegatesFilms/Films/UpdateFilm/UpdateFilmRoute.cs b/FunctionalProject/Features/NamedDelegatesFilms/Films/UpdateFilm/UpdateFilmRoute.cs new file mode 100644 index 0000000..d7640a7 --- /dev/null +++ b/FunctionalProject/Features/NamedDelegatesFilms/Films/UpdateFilm/UpdateFilmRoute.cs @@ -0,0 +1,26 @@ +namespace FunctionalProject.Features.NamedDelegatesFilms.Films.UpdateFilm +{ + using System; + using Models; + + public static class UpdateFilmRoute + { + public static void Handle(int id, Film film, ValidUserDelegate validUserQuery, ListFilmByIdDelegate listFilmById) + { + if (!validUserQuery()) + { + throw new InvalidOperationException(); + } + + //Do some special MEGA CORP business validation + + var existingFilm = listFilmById(id); + + existingFilm.Name = film.Name; + existingFilm.Budget = film.Budget; + existingFilm.Language = film.Language; + + //Write some SQL to store in db + } + } +} diff --git a/FunctionalProject/FunctionalProject.csproj b/FunctionalProject/FunctionalProject.csproj index d1e6948..70e42d1 100644 --- a/FunctionalProject/FunctionalProject.csproj +++ b/FunctionalProject/FunctionalProject.csproj @@ -8,9 +8,7 @@ - - - + diff --git a/FunctionalProject/Program.cs b/FunctionalProject/Program.cs index 7eae806..7827fdb 100644 --- a/FunctionalProject/Program.cs +++ b/FunctionalProject/Program.cs @@ -1,19 +1,16 @@ namespace FunctionalProject { - using System.IO; + using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; public class Program { public static void Main(string[] args) { - var host = new WebHostBuilder() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseKestrel() + WebHost.CreateDefaultBuilder(args) .UseStartup() - .Build(); - - host.Run(); + .Build() + .Run(); } } } diff --git a/FunctionalProject/Startup.cs b/FunctionalProject/Startup.cs index 26f4203..d652cfd 100644 --- a/FunctionalProject/Startup.cs +++ b/FunctionalProject/Startup.cs @@ -10,7 +10,7 @@ public class Startup { public void ConfigureServices(IServiceCollection services) { - services.AddBotwin(Assembly.GetEntryAssembly(), typeof(FilmValidator).Assembly); + services.AddBotwin(Assembly.GetCallingAssembly(), typeof(FilmValidator).Assembly); } public void Configure(IApplicationBuilder app) diff --git a/FunctionalProjectTests/Features/Films/FilmTests.cs b/FunctionalProjectTests/Features/Films/FilmTests.cs new file mode 100644 index 0000000..192903a --- /dev/null +++ b/FunctionalProjectTests/Features/Films/FilmTests.cs @@ -0,0 +1,115 @@ +namespace FunctionalProjectTests.Features.Films +{ + using System; + using System.Net.Http; + using System.Text; + using System.Threading.Tasks; + using Botwin; + using FunctionalProject.Features.NamedDelegatesFilms.Films; + using FunctionalProject.Features.NamedDelegatesFilms.Films.CreateFilm; + using FunctionalProject.Features.NamedDelegatesFilms.Films.ListFilmById; + using Microsoft.AspNetCore; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.TestHost; + using Models; + using Newtonsoft.Json; + using Xunit; + + public class FilmTests + { + private TestServer server; + + private HttpClient client; + + public FilmTests() + { + server = new TestServer(WebHost.CreateDefaultBuilder() + .ConfigureServices(services => services.AddBotwin(typeof(RouteHandlers).Assembly, typeof(FilmValidator).Assembly)) + .Configure(app => app.UseBotwin()) + ); + + client = server.CreateClient(); + } + + [Fact] + public async Task Should_return_422_on_invalid_data_when_creating_film() + { + //Given + + //No mock library required to fake the UserValidQuery we just invoke a func to return true/false + + RouteHandlers.CreateFilmHandler = film => CreateFilmRoute.Handle(film, () => true); + + var newFilm = new Film { Name = "" }; + + //When + var response = await client.PostAsync("/api/delegate/films", new StringContent(JsonConvert.SerializeObject(newFilm), Encoding.UTF8, "application/json")); + + //Then + Assert.Equal(422, (int)response.StatusCode); + } + + [Fact] + public async Task Should_return_403_on_invalid_user_when_creating_film() + { + //Given + RouteHandlers.CreateFilmHandler = film => CreateFilmRoute.Handle(film, () => false); + + var newFilm = new Film { Name = "Shrek" }; + + //When + var response = await client.PostAsync("/api/delegate/films", new StringContent(JsonConvert.SerializeObject(newFilm), Encoding.UTF8, "application/json")); + + //Then + Assert.Equal(403, (int)response.StatusCode); + } + + [Fact] + public async Task Should_return_201_when_creating_film() + { + //Given + RouteHandlers.CreateFilmHandler = film => CreateFilmRoute.Handle(film, () => true); + + var newFilm = new Film { Name = "Shrek" }; + + //When + var response = await client.PostAsync("/api/delegate/films", new StringContent(JsonConvert.SerializeObject(newFilm), Encoding.UTF8, "application/json")); + + //Then + Assert.Equal(201, (int)response.StatusCode); + } + + [Fact] + public async Task Should_get_film_by_id() + { + //Given + + //No mock library required to fake the GetFilmBtIdDelegate, GetDirectorById, GetCastMembersByFilmId we just invoke a func + + RouteHandlers.ListFilmByIdHandler = id => ListFilmByIdRoute.Handle(id, filmid => new Film { Name = "Blade Runner" }, i => new Director(), filmId => new[] { new CastMember() }); + + //When + var response = await client.GetAsync("/api/delegate/films/1"); + var contents = await response.Content.ReadAsStringAsync(); + + //Then + Assert.Contains("Blade Runner", contents, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task Should_return_404_when_no_film_found_via_get_film_by_id() + { + //Given + + //No mock library required to fake the GetFilmBtIdDelegate, GetDirectorById, GetCastMembersByFilmId we just invoke a func + + RouteHandlers.ListFilmByIdHandler = id => ListFilmByIdRoute.Handle(id, filmid => null, i => new Director(), filmId => new[] { new CastMember() }); + + //When + var response = await client.GetAsync("/api/delegate/films/1"); + + //Then + Assert.Equal(404, (int)response.StatusCode); + } + } +} diff --git a/FunctionalProjectTests/FunctionalProjectTests.csproj b/FunctionalProjectTests/FunctionalProjectTests.csproj new file mode 100644 index 0000000..268f38e --- /dev/null +++ b/FunctionalProjectTests/FunctionalProjectTests.csproj @@ -0,0 +1,15 @@ + + + Exe + netcoreapp2.0 + + + + + + + + + + + \ No newline at end of file diff --git a/MediatRWebAPI.Tests/Features/Films/CreateFilm/CreateFilmMessageHandlerTests.cs b/MediatRWebAPI.Tests/Features/Films/CreateFilm/CreateFilmMessageHandlerTests.cs new file mode 100644 index 0000000..3464e92 --- /dev/null +++ b/MediatRWebAPI.Tests/Features/Films/CreateFilm/CreateFilmMessageHandlerTests.cs @@ -0,0 +1,28 @@ +namespace MediatRWebAPI.Tests.Features.Films.CreateFilm +{ + using System; + using FakeItEasy; + using MediatRWebAPI.Features.Films.CreateFilm; + using MediatRWebAPI.Features.Permissions; + using Models; + using Xunit; + + public class CreateFilmMessageHandlerTests + { + //This class can be unit test and/or integration test as our handler class is responsible for doing whatever it takes + + [Fact] + public void Should_throw_exception_if_user_invalid() + { + //Given + var fakeValidUserQuery = A.Fake(); + A.CallTo(() => fakeValidUserQuery.Execute()).Returns(false); + var handler = new CreateFilmMessageHandler(fakeValidUserQuery); + + //When & Then + Assert.Throws(() => handler.Handle(new CreateFilmMessage(new Film()))); + } + + //Could write an integration test to see if the database has a new film in it or use the API tests to POST then GET the film it created + } +} diff --git a/MediatRWebAPI.Tests/Features/Films/DeleteFilm/DeleteFilmMessageHandlerTests.cs b/MediatRWebAPI.Tests/Features/Films/DeleteFilm/DeleteFilmMessageHandlerTests.cs new file mode 100644 index 0000000..6da07fb --- /dev/null +++ b/MediatRWebAPI.Tests/Features/Films/DeleteFilm/DeleteFilmMessageHandlerTests.cs @@ -0,0 +1,27 @@ +namespace MediatRWebAPI.Tests.Features.Films.DeleteFilm +{ + using System; + using FakeItEasy; + using MediatRWebAPI.Features.Films.DeleteFilm; + using MediatRWebAPI.Features.Permissions; + using Xunit; + + public class DeleteFilmMessageHandlerTests + { + //This class can be unit test and/or integration test as our handler class is responsible for doing whatever it takes + + [Fact] + public void Should_throw_exception_if_user_invalid() + { + //Given + var fakeValidUserQuery = A.Fake(); + A.CallTo(() => fakeValidUserQuery.Execute()).Returns(false); + var handler = new DeleteFilmMessageHandler(fakeValidUserQuery); + + //When & Then + Assert.Throws(() => handler.Handle(new DeleteFilmMessage(1))); + } + + //Could write an integration test to see if the database has a new film in it or use the API tests to POST then GET the film it created + } +} diff --git a/MediatRWebAPI.Tests/FilmControllerTests.cs b/MediatRWebAPI.Tests/Features/Films/FilmControllerTests.cs similarity index 99% rename from MediatRWebAPI.Tests/FilmControllerTests.cs rename to MediatRWebAPI.Tests/Features/Films/FilmControllerTests.cs index 6482730..30635ce 100644 --- a/MediatRWebAPI.Tests/FilmControllerTests.cs +++ b/MediatRWebAPI.Tests/Features/Films/FilmControllerTests.cs @@ -1,4 +1,4 @@ -namespace MediatRWebAPI.Tests +namespace MediatRWebAPI.Tests.Features.Films { using System; using System.Net.Http; diff --git a/MediatRWebAPI.Tests/Features/Films/ListFilmById/ListFilmsByIdMessageHandlerTests.cs b/MediatRWebAPI.Tests/Features/Films/ListFilmById/ListFilmsByIdMessageHandlerTests.cs new file mode 100644 index 0000000..5b484a5 --- /dev/null +++ b/MediatRWebAPI.Tests/Features/Films/ListFilmById/ListFilmsByIdMessageHandlerTests.cs @@ -0,0 +1,36 @@ +namespace MediatRWebAPI.Tests.Features.Films.ListFilmById +{ + using FakeItEasy; + using MediatRWebAPI.Features.CastMembers.GetCastByFilmIdQuery; + using MediatRWebAPI.Features.Directors.GetDirectorByIdQuery; + using MediatRWebAPI.Features.Films.ListFilmById; + using MediatRWebAPI.Features.Films.ListFilmByIdQuery; + using Xunit; + + public class ListFilmsByIdMessageHandlerTests + { + //This class can be unit test and/or integration test as our handler class is responsible for doing whatever it takes + + [Fact] + public void Should_return_film() + { + //Given + var fakeListFilmByIdQuery = A.Fake(); + var fakeGetDirectorByIdQuery = A.Fake(); + var fakeGetCastByIdDirectory = A.Fake(); + var handler = new ListFilmsByIdMessageHandler(fakeListFilmByIdQuery, fakeGetDirectorByIdQuery, fakeGetCastByIdDirectory); + + //When + var film = handler.Handle(new ListFilmsByIdMessage(1)); + + //Then + Assert.NotNull(film); + + //The issue here, like the service tests are that we are testing the fakes are called which doesn't offer much value + + //If there is a bit more logic in the function then a unit test can be valuable, we can see that in the Create/Update/Delete handlers that do a valid user check + + //You are better off having an integration test at this point possibly with an in-memory sql database or Before/After xUnit attribute to setup db state + } + } +} diff --git a/MediatRWebAPI.Tests/Features/Films/ListFilms/ListFilmsMessageHandlerTests.cs b/MediatRWebAPI.Tests/Features/Films/ListFilms/ListFilmsMessageHandlerTests.cs new file mode 100644 index 0000000..57316c1 --- /dev/null +++ b/MediatRWebAPI.Tests/Features/Films/ListFilms/ListFilmsMessageHandlerTests.cs @@ -0,0 +1,24 @@ +namespace MediatRWebAPI.Tests.Features.Films.ListFilms +{ + using System.Linq; + using MediatRWebAPI.Features.Films.ListFilms; + using Xunit; + + public class ListFilmsMessageHandlerTests + { + //This class can be unit test and/or integration test as our handler class is responsible for doing whatever it takes + + [Fact] + public void Should_return_list_of_films() + { + //Given + var handler = new ListFilmsMessageHandler(); + + //When + var films = handler.Handle(new ListFilmsMessage()); + + //Then + Assert.True(films.Any()); + } + } +} diff --git a/MediatRWebAPI.Tests/Features/Films/UpdateFilm/UpdateFilmMessageHandlerTests.cs b/MediatRWebAPI.Tests/Features/Films/UpdateFilm/UpdateFilmMessageHandlerTests.cs new file mode 100644 index 0000000..e7ca548 --- /dev/null +++ b/MediatRWebAPI.Tests/Features/Films/UpdateFilm/UpdateFilmMessageHandlerTests.cs @@ -0,0 +1,31 @@ +namespace MediatRWebAPI.Tests.Features.Films.UpdateFilm +{ + using System; + using FakeItEasy; + using MediatRWebAPI.Features.Films.ListFilmByIdQuery; + using MediatRWebAPI.Features.Films.UpdateFilm; + using MediatRWebAPI.Features.Permissions; + using Models; + using Xunit; + + public class UpdateFilmMessageHandlerTests + { + //This class can be unit test and/or integration test as our handler class is responsible for doing whatever it takes + + [Fact] + public void Should_throw_exception_if_user_invalid() + { + //Given + var fakeValidUserQuery = A.Fake(); + A.CallTo(() => fakeValidUserQuery.Execute()).Returns(false); + var fakeListFilmByIdQuery = A.Fake(); + + var handler = new UpdateFilmMessageHandler(fakeListFilmByIdQuery, fakeValidUserQuery); + + //When & Then + Assert.Throws(() => handler.Handle(new UpdateFilmMessage(1, new Film()))); + } + + //Could write an integration test to see if the database has a new film in it or use the API tests to POST then GET the film it created + } +} diff --git a/MediatRWebAPI/Features/Films/ListFilmById/ListFilmsByIdMessageHandler.cs b/MediatRWebAPI/Features/Films/ListFilmById/ListFilmsByIdMessageHandler.cs index 9dd40fd..f1b868e 100644 --- a/MediatRWebAPI/Features/Films/ListFilmById/ListFilmsByIdMessageHandler.cs +++ b/MediatRWebAPI/Features/Films/ListFilmById/ListFilmsByIdMessageHandler.cs @@ -25,8 +25,6 @@ public ListFilmsByIdMessageHandler(IListFilmByIdQuery listFilmByIdQuery, IGetDir public Film Handle(ListFilmsByIdMessage message) { - //TODO Unit testing? - //Use shared query to get film var film = this.listFilmByIdQuery.Execute(message.Id); diff --git a/TradtionalWebAPI.Tests/FilmControllerTests.cs b/TradtionalWebAPI.Tests/Controllers/FilmControllerTests.cs similarity index 99% rename from TradtionalWebAPI.Tests/FilmControllerTests.cs rename to TradtionalWebAPI.Tests/Controllers/FilmControllerTests.cs index 7ed11ed..f578f2d 100644 --- a/TradtionalWebAPI.Tests/FilmControllerTests.cs +++ b/TradtionalWebAPI.Tests/Controllers/FilmControllerTests.cs @@ -1,4 +1,4 @@ -namespace TradtionalWebAPI.Tests +namespace TradtionalWebAPI.Tests.Controllers { using System; using System.Net.Http; diff --git a/TradtionalWebAPI.Tests/FilmServiceTests.cs b/TradtionalWebAPI.Tests/Services/FilmServiceTests.cs similarity index 99% rename from TradtionalWebAPI.Tests/FilmServiceTests.cs rename to TradtionalWebAPI.Tests/Services/FilmServiceTests.cs index e547c10..b244387 100644 --- a/TradtionalWebAPI.Tests/FilmServiceTests.cs +++ b/TradtionalWebAPI.Tests/Services/FilmServiceTests.cs @@ -1,4 +1,4 @@ -namespace TradtionalWebAPI.Tests +namespace TradtionalWebAPI.Tests.Services { using System; using FakeItEasy;