diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..222b21679 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +bin\ +obj\ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index a512c8b38..46d5173b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,362 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore -# Test binary, built with `go test -c` -*.test +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates -# Output of the go coverage tool, specifically when used with LiteIDE -*.out +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs -# Dependency directories (remove the comment below to include it) -# vendor/ +# Mono auto generated files +mono_crash.* -out/ +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ -.idea \ No newline at end of file +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*[.json, .xml, .info] + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +.vscode/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..985ef54e9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Builds +FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build +WORKDIR /build +COPY . . +RUN dotnet restore --disable-parallel +WORKDIR /build/TogoService.API +RUN dotnet build "TogoService.API.csproj" -c Release -o /app +RUN dotnet publish -c release -r debian-x64 -o /app + +# Runs +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS final +WORKDIR /app +ENV ASPNETCORE_URLS http://+:80 +EXPOSE 80 + +COPY --from=build /app . +ENTRYPOINT ["dotnet", "TogoService.API.dll"] \ No newline at end of file diff --git a/README.md b/README.md index 8df9d4d3a..2a2438448 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,73 @@ -### Requirements + # Togo Service +There are 3 projects as below: + - **TogoService.API**: It's main application for WEB API. It's a lightweight web API. Business logic is in Controller class as well. + - **TogoService.UnitTest**: Unit test for API. I only care about the method AddTasksForUser in UserController class. It contains all business logic. + - **TogoService.IntegrationTest**: Integration Test for API. -- Implement one single API which accepts a todo task and records it - - There is a maximum **limit of N tasks per user** that can be added **per day**. - - Different users can have **different** maximum daily limit. -- Write integration (functional) tests -- Write unit tests -- Choose a suitable architecture to make your code simple, organizable, and maintainable -- Write a concise README - - How to run your code locally? - - A sample “curl” command to call your API - - How to run your unit tests locally? - - What do you love about your solution? - - What else do you want us to know about however you do not have enough time to complete? +**Language**: C#. +**Framework**: .NET Core 3.1 -### Notes +## Prerequisite +- .NET Core 3.1 SDK ([download](https://dotnet.microsoft.com/en-us/download/dotnet/3.1 "download")) +- Editor: any editor you familiar with. +- [ SQLite Studio](https://sqlitestudio.pl/ " SQLite Studio") for viewing data -- We're using Golang at Manabie. **However**, we encourage you to use the programming language that you are most comfortable with because we want you to **shine** with all your skills and knowledge. +## Run locally +**PLEASE UNINSTALL ALL .NET SDK IN YOUR LOCAL MACHINE IF YOU ALREADY INSTALLED ANY .NET SDK VERSION > 3.1** -### How to submit your solution? +**INSTAL CORRECT VERSION OF .NET SDK. .NET Core SDK 3.1 WITH THE LINK ABOVE.** -- Fork this repo and show us your development progress via a PR +**YOU CAN INSTALL ANY .NET SDK VERSIONS HIGHER THAN 3.1 AFTER YOU INSTALL .NET SDK VERSION 3.1** -### Interesting facts about Manabie +**DO NOT install any .NET SDK versions higher than 3.1 before install version 3.1.** -- Monthly there are about 2 million lines of code changes (inserted/updated/deleted) committed into our GitHub repositories. To avoid **regression bugs**, we write different kinds of **automated tests** (unit/integration (functionality)/end2end) as parts of the definition of done of our assigned tasks. -- We nurture the cultural values: **knowledge sharing** and **good communication**, therefore good written documents and readable, organizable, and maintainable code are in our blood when we build any features to grow our products. -- We have **collaborative** culture at Manabie. Feel free to ask trieu@manabie.com any questions. We are very happy to answer all of them. +- Clone code base. +- Navigate to root code base folder. Run `dotnet build`. It will restore and build solution (3 projects). +- Navigate to TogoService.API folder. Run `dotnet run` to run it. Open browser at https://localhost:5001/swagger for API document and try it. + - You can also use postman to try it. + - Using CURL: +> curl -X 'POST' \ + 'https://localhost:5001/api/users/{userId}/tasks' \ + -H 'accept: text/plain' \ + -H 'Content-Type: application/json' \ + -d '{ + "Date": "2022-07-05T03:54:32.843Z", + "Tasks": [ + { + "Name": "task 1", + "Description": "task thứ nhất" + } + ] +}' -Thank you for spending time to read and attempt our take-home assessment. We are looking forward to your submission. +## Run locally with Docker +- Clone code base +- Navigate to root code base folder. Run `docker build -t togoservice .` for building image. +- Run `docker run -d -p 5001:80 --name togoapi togoservice` to start container. +- Open browser at http://localhost:5001/swagger for API document and try it or you can use CURL with above information. + +## Note: 2 users id for run locally testing: +- id =BA4688FB-B389-40A7-A94C-FE43F61A2BCE - max daily tasks = 10 +- id =437A4196-7ADF-441F-8352-05F126C469B2 - max daily tasks = 0 + +## Unit Test - Run locally +- Clone code base. +- Navigate to root code base folder. Run `dotnet build`. It will restore and build solution (3 projects). +- Navigate to TogoService.UnitTest. Run `dotnet test`. +You can do the same with integration test. + +## My solution +- It is lightweight web API project. +- I already dockerized but did not test it. +- Unit test coverage is 100% and integration test coverage is 80%. +![test coverage_unit test](https://user-images.githubusercontent.com/4899325/177812275-8d8f2b01-78e7-4fa5-9450-cfbc2f120129.PNG) + +![test coverage_integration test](https://user-images.githubusercontent.com/4899325/177812394-e5b6d475-aa96-4563-952a-2a452f3b351a.PNG) + + +## More +If I have more time, I think there are some things we can improve: +- Dockerization with careful testing. +- Using environment variables for various environments (dev, qa, staging, production...). +- Authentication and authorization for endpoints. +- I am using SQLite without any security (credentials...). Need to pay attention here. diff --git a/TogoService.API/Controller/UserController.cs b/TogoService.API/Controller/UserController.cs new file mode 100644 index 000000000..b3e9ab5e7 --- /dev/null +++ b/TogoService.API/Controller/UserController.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading.Tasks; +using AutoMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using TogoService.API.Dto; +using TogoService.API.Filter; +using TogoService.API.Infrastructure.Helper; +using TogoService.API.Infrastructure.Helper.MessageUtil; +using TogoService.API.Model; +using TogoService.API.Model.Interface; + +namespace TogoService.API.Controller +{ + [ApiController] + [ApiConventionType(typeof(CustomApiConventions))] + [Route("api/users")] + [ServiceFilter(typeof(ValidationActionFilter))] + public class UserController : ControllerBase + { + private readonly ILogger _logger; + private readonly IMapper _mapper; + private readonly IUnitOfWork _iUnitOfWork; + private readonly ITodoTaskRepository _iTodoTaskRepository; + public UserController(ILogger logger, IMapper mapper, IUnitOfWork iUnitOfWork, ITodoTaskRepository iTodoTaskRepository) + { + _logger = logger; + _mapper = mapper; + _iUnitOfWork = iUnitOfWork; + _iTodoTaskRepository = iTodoTaskRepository; + _iTodoTaskRepository.SetUnitOfWork(iUnitOfWork); + } + + [HttpPost("{userId}/tasks")] + [ProducesResponseType(typeof(CommonResponse), StatusCodes.Status201Created)] + public async Task AddTasksForUser(Guid userId, [FromBody] NewTaskRequest requestData) + { + CommonResponse response; + if (requestData.Date.Equals(new DateTime())) + { + response = new CommonResponse(StatusCodes.Status400BadRequest, UserControllerErrMsg.MissingTodoDay, null); + return StatusCode(response.StatusCode, response); + } + + if (Guid.Empty.Equals(userId)) + { + response = GenerateCannotFindUserRes(userId); + return StatusCode(response.StatusCode, response); + } + + try + { + Model.User user = await _iUnitOfWork.GenericRepository().GetById(userId); + if (user == null) + { + response = GenerateCannotFindUserRes(userId); + return StatusCode(response.StatusCode, response); + } + else + { + uint addedTasks = ((uint)(await _iTodoTaskRepository.GetAddedTasks(userId, requestData.Date)).Length); + uint canAddTasks = user.MaxDailyTasks - addedTasks; + if (requestData.Tasks.Length > canAddTasks) + { + response = new CommonResponse(StatusCodes.Status422UnprocessableEntity, UserControllerErrMsg.ReachoutMaxTaskPerDay, null); + return StatusCode(response.StatusCode, response); + } + + TodoTask[] newTasks = new TodoTask[requestData.Tasks.Length]; + for (int i = 0; i < requestData.Tasks.Length; i++) + { + newTasks[i] = _mapper.Map(requestData.Tasks[i]); + newTasks[i].UserId = userId; + newTasks[i].TodoDay = requestData.Date; + } + await _iTodoTaskRepository.AddRange(newTasks); + await _iUnitOfWork.Save(); + response = new CommonResponse(StatusCodes.Status201Created, + CommonMessages.Ok, + CommonMessages.GetSuccessfulAddedItemsMsg(typeof(Model.TodoTask).Name, newTasks.Length)); + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, ex.Message); + response = new CommonResponse(StatusCodes.Status500InternalServerError, ex.Message, null); + } + + return StatusCode(response.StatusCode, response); + } + + private CommonResponse GenerateCannotFindUserRes(Guid userId) + { + return new CommonResponse(StatusCodes.Status404NotFound, CommonMessages.GetCannotFindMsg(typeof(Model.User).Name, userId), null); + } + } +} \ No newline at end of file diff --git a/TogoService.API/Dto/CommonResponse.cs b/TogoService.API/Dto/CommonResponse.cs new file mode 100644 index 000000000..9e7829722 --- /dev/null +++ b/TogoService.API/Dto/CommonResponse.cs @@ -0,0 +1,39 @@ +using TogoService.API.Infrastructure.Helper.Constant; +using System.Text.Json.Serialization; + +namespace TogoService.API.Dto +{ + + public class CommonResponse where T : class + { + public CommonResponse() { } + + public CommonResponse(int statusCode) + { + StatusCode = statusCode; + Message = null; + Result = null; + } + + public CommonResponse(int statusCode, T result) : this(statusCode) + { + Message = "Ok"; + Result = result; + } + + public CommonResponse(int statusCode, string message, T result) : this(statusCode) + { + Message = message; + Result = result; + } + + [JsonPropertyName(JsonPropertyNames.StatusCode)] + public int StatusCode { get; set; } + + [JsonPropertyName(JsonPropertyNames.Message)] + public string Message { get; set; } + + [JsonPropertyName(JsonPropertyNames.Result)] + public T Result { get; set; } + } +} \ No newline at end of file diff --git a/TogoService.API/Dto/NewTasksRequest.cs b/TogoService.API/Dto/NewTasksRequest.cs new file mode 100644 index 000000000..3fa4c85b1 --- /dev/null +++ b/TogoService.API/Dto/NewTasksRequest.cs @@ -0,0 +1,33 @@ +using System; +using System.Text.Json.Serialization; + +namespace TogoService.API.Dto +{ + public class NewTaskRequest + { + public NewTaskRequest() { } + + [JsonPropertyName("Date")] + public DateTime Date { get; set; } + + [JsonPropertyName("Tasks")] + public TaskRequest[] Tasks { get; set; } + } + + public class TaskRequest + { + public TaskRequest() { } + + public TaskRequest(string name, string description) + { + Name = name; + Description = description; + } + + [JsonPropertyName("Name")] + public string Name { get; set; } + + [JsonPropertyName("Description")] + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/TogoService.API/Filter/ValidationActionFilter.cs b/TogoService.API/Filter/ValidationActionFilter.cs new file mode 100644 index 000000000..4c8044adc --- /dev/null +++ b/TogoService.API/Filter/ValidationActionFilter.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using TogoService.API.Dto; +using TogoService.API.Infrastructure.Helper.MessageUtil; + +namespace TogoService.API.Filter +{ + public class ValidationActionFilter : IActionFilter + { + public void OnActionExecuted(ActionExecutedContext context) + { + // Not doing anything after the action has executed + } + public void OnActionExecuting(ActionExecutingContext context) + { + var logger = context.HttpContext.RequestServices.GetService>(); + var modelState = context.ModelState; + if (!modelState.IsValid) + { + var errors = new List(); + foreach (var state in modelState) + { + foreach (var error in state.Value.Errors) + { + errors.Add(error.ErrorMessage); + } + } + if (logger != null) + { + logger.LogError(string.Join(", ", errors)); + } + + + context.Result = new BadRequestObjectResult( + new CommonResponse( + StatusCodes.Status400BadRequest, + CommonMessages.InvalidObject)); + } + } + } +} diff --git a/TogoService.API/Infrastructure/Database/Migrations/20220707042155_InitDatabase.Designer.cs b/TogoService.API/Infrastructure/Database/Migrations/20220707042155_InitDatabase.Designer.cs new file mode 100644 index 000000000..496e16852 --- /dev/null +++ b/TogoService.API/Infrastructure/Database/Migrations/20220707042155_InitDatabase.Designer.cs @@ -0,0 +1,122 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TogoService.API.Infrastructure.Database; + +namespace TogoService.API.Infrastructure.Database.Migrations +{ + [DbContext(typeof(TogoDbContext))] + [Migration("20220707042155_InitDatabase")] + partial class InitDatabase + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.8"); + + modelBuilder.Entity("TogoService.API.Model.TodoTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("createdAt"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER") + .HasColumnName("isDeleted"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("TodoDay") + .HasColumnType("TEXT") + .HasColumnName("todoDay"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updatedAt"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("UserId"); + + b.ToTable("TodoTask"); + }); + + modelBuilder.Entity("TogoService.API.Model.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("createdAt"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER") + .HasColumnName("isDeleted"); + + b.Property("MaxDailyTasks") + .HasColumnType("INTEGER") + .HasColumnName("maxDailyTasks"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updatedAt"); + + b.HasKey("Id"); + + b.HasIndex("IsDeleted"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("TogoService.API.Model.TodoTask", b => + { + b.HasOne("TogoService.API.Model.User", "User") + .WithMany("Tasks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TogoService.API.Model.User", b => + { + b.Navigation("Tasks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TogoService.API/Infrastructure/Database/Migrations/20220707042155_InitDatabase.cs b/TogoService.API/Infrastructure/Database/Migrations/20220707042155_InitDatabase.cs new file mode 100644 index 000000000..150461dbc --- /dev/null +++ b/TogoService.API/Infrastructure/Database/Migrations/20220707042155_InitDatabase.cs @@ -0,0 +1,75 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace TogoService.API.Infrastructure.Database.Migrations +{ + public partial class InitDatabase : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "User", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + name = table.Column(type: "TEXT", maxLength: 500, nullable: false), + maxDailyTasks = table.Column(type: "INTEGER", nullable: false), + isDeleted = table.Column(type: "INTEGER", nullable: false), + updatedAt = table.Column(type: "TEXT", nullable: false), + createdAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_User", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "TodoTask", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + name = table.Column(type: "TEXT", maxLength: 500, nullable: false), + description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + todoDay = table.Column(type: "TEXT", nullable: false), + userId = table.Column(type: "TEXT", nullable: false), + isDeleted = table.Column(type: "INTEGER", nullable: false), + updatedAt = table.Column(type: "TEXT", nullable: false), + createdAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TodoTask", x => x.id); + table.ForeignKey( + name: "FK_TodoTask_User_userId", + column: x => x.userId, + principalTable: "User", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_TodoTask_isDeleted", + table: "TodoTask", + column: "isDeleted"); + + migrationBuilder.CreateIndex( + name: "IX_TodoTask_userId", + table: "TodoTask", + column: "userId"); + + migrationBuilder.CreateIndex( + name: "IX_User_isDeleted", + table: "User", + column: "isDeleted"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TodoTask"); + + migrationBuilder.DropTable( + name: "User"); + } + } +} diff --git a/TogoService.API/Infrastructure/Database/Migrations/TogoDbContextModelSnapshot.cs b/TogoService.API/Infrastructure/Database/Migrations/TogoDbContextModelSnapshot.cs new file mode 100644 index 000000000..88a81f5f1 --- /dev/null +++ b/TogoService.API/Infrastructure/Database/Migrations/TogoDbContextModelSnapshot.cs @@ -0,0 +1,120 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TogoService.API.Infrastructure.Database; + +namespace TogoService.API.Infrastructure.Database.Migrations +{ + [DbContext(typeof(TogoDbContext))] + partial class TogoDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.8"); + + modelBuilder.Entity("TogoService.API.Model.TodoTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("createdAt"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER") + .HasColumnName("isDeleted"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("TodoDay") + .HasColumnType("TEXT") + .HasColumnName("todoDay"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updatedAt"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("userId"); + + b.HasKey("Id"); + + b.HasIndex("IsDeleted"); + + b.HasIndex("UserId"); + + b.ToTable("TodoTask"); + }); + + modelBuilder.Entity("TogoService.API.Model.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("createdAt"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER") + .HasColumnName("isDeleted"); + + b.Property("MaxDailyTasks") + .HasColumnType("INTEGER") + .HasColumnName("maxDailyTasks"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updatedAt"); + + b.HasKey("Id"); + + b.HasIndex("IsDeleted"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("TogoService.API.Model.TodoTask", b => + { + b.HasOne("TogoService.API.Model.User", "User") + .WithMany("Tasks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TogoService.API.Model.User", b => + { + b.Navigation("Tasks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TogoService.API/Infrastructure/Database/TogoDbContext.cs b/TogoService.API/Infrastructure/Database/TogoDbContext.cs new file mode 100644 index 000000000..0ab54255f --- /dev/null +++ b/TogoService.API/Infrastructure/Database/TogoDbContext.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore; +using TogoService.API.Model; + +namespace TogoService.API.Infrastructure.Database +{ + public class TogoDbContext : DbContext + { + public TogoDbContext(DbContextOptions options) : base(options) { } + + public TogoDbContext() { } + + public DbSet Users { get; set; } + public DbSet TodoTasks { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Model + modelBuilder.Entity() + .HasOne(task => task.User) + .WithMany(user => user.Tasks) + .HasForeignKey(task => task.UserId); + } + } +} diff --git a/TogoService.API/Infrastructure/Helper/Constant/EntityConstantsCollection.cs b/TogoService.API/Infrastructure/Helper/Constant/EntityConstantsCollection.cs new file mode 100644 index 000000000..b3123f594 --- /dev/null +++ b/TogoService.API/Infrastructure/Helper/Constant/EntityConstantsCollection.cs @@ -0,0 +1,7 @@ +namespace TogoService.API.Infrastructure.Helper.Constant +{ + public class EntityConstantsCollection + { + public const int MaxLengthDataTypeVarchar = 500; + } +} diff --git a/TogoService.API/Infrastructure/Helper/Constant/JsonPropertyNames.cs b/TogoService.API/Infrastructure/Helper/Constant/JsonPropertyNames.cs new file mode 100644 index 000000000..7978d5669 --- /dev/null +++ b/TogoService.API/Infrastructure/Helper/Constant/JsonPropertyNames.cs @@ -0,0 +1,9 @@ +namespace TogoService.API.Infrastructure.Helper.Constant +{ + public static class JsonPropertyNames + { + public const string StatusCode = "statusCode"; + public const string Message = "message"; + public const string Result = "result"; + } +} diff --git a/TogoService.API/Infrastructure/Helper/CustomApiConventions.cs b/TogoService.API/Infrastructure/Helper/CustomApiConventions.cs new file mode 100644 index 000000000..552dac9f8 --- /dev/null +++ b/TogoService.API/Infrastructure/Helper/CustomApiConventions.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; + +namespace TogoService.API.Infrastructure.Helper +{ + public static class CustomApiConventions + { + + [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static void Show() { } + + [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + + public static void Add( + [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)] + [ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)] params object[] args) + { } + + [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + + public static void Create( + [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)] + [ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)] params object[] args) + { } + } +} \ No newline at end of file diff --git a/TogoService.API/Infrastructure/Helper/MessageUtil/CommonMessages.cs b/TogoService.API/Infrastructure/Helper/MessageUtil/CommonMessages.cs new file mode 100644 index 000000000..5c82c270b --- /dev/null +++ b/TogoService.API/Infrastructure/Helper/MessageUtil/CommonMessages.cs @@ -0,0 +1,20 @@ +using System; + +namespace TogoService.API.Infrastructure.Helper.MessageUtil +{ + public static class CommonMessages + { + public static string Ok { get { return "Ok"; } } + public static string InvalidObject { get { return "Error occured: invalid object"; } } + + public static string GetCannotFindMsg(string entityName, Guid id) + { + return $"Cannot find entity {entityName} with id = {id.ToString()}"; + } + + public static string GetSuccessfulAddedItemsMsg(string entityName, int addedItems) + { + return $"Successfully added {addedItems} {entityName}."; + } + } +} diff --git a/TogoService.API/Infrastructure/Helper/MessageUtil/UserControllerErrMsg.cs b/TogoService.API/Infrastructure/Helper/MessageUtil/UserControllerErrMsg.cs new file mode 100644 index 000000000..16111fd9e --- /dev/null +++ b/TogoService.API/Infrastructure/Helper/MessageUtil/UserControllerErrMsg.cs @@ -0,0 +1,8 @@ +namespace TogoService.API.Infrastructure.Helper.MessageUtil +{ + public static class UserControllerErrMsg + { + public static string MissingTodoDay { get { return "Missing day for to do tasks."; } } + public static string ReachoutMaxTaskPerDay { get { return "User reaches limitation of number of tasks per day."; } } + } +} diff --git a/TogoService.API/Infrastructure/Mapper/AutoMapperProfile.cs b/TogoService.API/Infrastructure/Mapper/AutoMapperProfile.cs new file mode 100644 index 000000000..de490995f --- /dev/null +++ b/TogoService.API/Infrastructure/Mapper/AutoMapperProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using TogoService.API.Model; +using TogoService.API.Dto; + +namespace TogoService.API.Infrastructure.Mapper +{ + public partial class AutoMapperProfile : Profile + { + public AutoMapperProfile() + { + CreateMap() + .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description)) + .ForAllOtherMembers(opt => opt.Ignore()); + } + } +} diff --git a/TogoService.API/Infrastructure/Repository/BaseRepository.cs b/TogoService.API/Infrastructure/Repository/BaseRepository.cs new file mode 100644 index 000000000..714d9de13 --- /dev/null +++ b/TogoService.API/Infrastructure/Repository/BaseRepository.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using TogoService.API.Infrastructure.Database; +using TogoService.API.Model.Interface; + +namespace TogoService.API.Infrastructure.Repository +{ + public class BaseRepository : IRepository, IDisposable where T : class + { + private DbSet _entities; + protected TogoDbContext _dbContext; + + public BaseRepository() { } + + public BaseRepository(TogoDbContext dbContext) + { + _dbContext = dbContext; + } + + public BaseRepository(IUnitOfWork unitOfWork) + : this(unitOfWork.Context) { } + + protected virtual DbSet Entities + { + get { return _entities ?? (_entities = _dbContext.Set()); } + } + + public void Dispose() + { + if (_dbContext != null) + _dbContext.Dispose(); + } + + public virtual async Task GetById(Guid id) + { + try + { + return await Entities.FindAsync(id); + } + catch (Exception ex) + { + throw ex; + } + } + + public virtual async Task Add(T entity) + { + try + { + if (entity == null) + throw new ArgumentNullException(typeof(T).Name); + return (await Entities.AddAsync(entity)).Entity; + } + catch (Exception ex) + { + throw ex; + } + } + + public virtual async Task AddRange(T[] entities) + { + try + { + await _dbContext.AddRangeAsync(entities); + } + catch (Exception ex) + { + throw ex; + } + } + + public void HardDelete(T entity) + { + throw new NotImplementedException(); + } + + public void HardDeleteRange(T[] entities) + { + throw new NotImplementedException(); + } + + public void Update(T entity) + { + throw new NotImplementedException(); + } + + public void UpdateRange(T[] entities) + { + throw new NotImplementedException(); + } + + public Task> GetAll() + { + throw new NotImplementedException(); + } + } +} diff --git a/TogoService.API/Infrastructure/Repository/TodoTaskRepository.cs b/TogoService.API/Infrastructure/Repository/TodoTaskRepository.cs new file mode 100644 index 000000000..7ca5f712e --- /dev/null +++ b/TogoService.API/Infrastructure/Repository/TodoTaskRepository.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using TogoService.API.Model; +using TogoService.API.Model.Interface; + +namespace TogoService.API.Infrastructure.Repository +{ + public class TodoTaskRepository : BaseRepository, ITodoTaskRepository + { + public TodoTaskRepository() { } + + public async Task GetAddedTasks(Guid userId, DateTime todoDate) + { + try + { + return await (_dbContext.TodoTasks.Where(x => x.IsDeleted == false + && x.UserId.Equals(userId) + && x.TodoDay.Date.Equals(todoDate.Date)).ToArrayAsync()); + } + catch (Exception ex) + { + throw ex; + } + } + + public void SetUnitOfWork(IUnitOfWork unitOfWork) + { + this._dbContext = unitOfWork.Context; + } + } +} diff --git a/TogoService.API/Infrastructure/Repository/UnitOfWork.cs b/TogoService.API/Infrastructure/Repository/UnitOfWork.cs new file mode 100644 index 000000000..d3aefc854 --- /dev/null +++ b/TogoService.API/Infrastructure/Repository/UnitOfWork.cs @@ -0,0 +1,88 @@ +using TogoService.API.Model.Interface; +using System.Collections.Generic; +using System.Threading.Tasks; +using System; +using Microsoft.EntityFrameworkCore.Storage; +using TogoService.API.Infrastructure.Database; + +namespace TogoService.API.Infrastructure.Repository +{ + public class UnitOfWork : IUnitOfWork, IDisposable + { + private readonly TogoDbContext _dbContext; + private IDbContextTransaction _objTran; + private bool _disposed; + private Dictionary _repositories; + + public UnitOfWork(TogoDbContext dbContext) + { + _dbContext = dbContext; + } + + public TogoDbContext Context + { + get { return _dbContext; } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public BaseRepository GenericRepository() where T : class + { + if (_repositories == null) + { + _repositories = new Dictionary(); + } + + var type = typeof(T).Name; + + if (!_repositories.ContainsKey(type)) + { + var repositoryInstance = new BaseRepository(_dbContext); + _repositories.Add(type, repositoryInstance); + } + + return (BaseRepository)_repositories[type]; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + if (disposing) + _dbContext.Dispose(); + _disposed = true; + } + + + public async Task CreateTransactionAsync() + { + _objTran = await _dbContext.Database.BeginTransactionAsync(); + } + + public async Task CommitAsync() + { + await _objTran.CommitAsync(); + } + + public async Task RollbackAsync() + { + await _objTran.RollbackAsync(); + await _objTran.DisposeAsync(); + } + + public async Task Save() + { + try + { + return await _dbContext.SaveChangesAsync(); + } + catch (Exception ex) + { + throw ex; + } + } + } +} diff --git a/TogoService.API/Model/BaseEntity.cs b/TogoService.API/Model/BaseEntity.cs new file mode 100644 index 000000000..341254192 --- /dev/null +++ b/TogoService.API/Model/BaseEntity.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace TogoService.API.Model +{ + [Index(nameof(IsDeleted))] + public abstract class BaseEntity + { + public BaseEntity() + { + IsDeleted = false; + UpdatedAt = DateTime.UtcNow; + CreatedAt = DateTime.UtcNow; + } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Column("id")] + public Guid Id { get; set; } + + [Required] + [Column("isDeleted")] + public bool IsDeleted { get; set; } + + [Required] + [Column("updatedAt")] + public DateTime UpdatedAt { get; set; } + + [Required] + [Column("createdAt")] + public DateTime CreatedAt { get; set; } + } +} diff --git a/TogoService.API/Model/Interface/IRepository.cs b/TogoService.API/Model/Interface/IRepository.cs new file mode 100644 index 000000000..a13b34252 --- /dev/null +++ b/TogoService.API/Model/Interface/IRepository.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace TogoService.API.Model.Interface +{ + public interface IRepository where T : class + { + /// + /// Add an entity + /// + /// Entity + /// Entity after added + Task Add(T entity); + + /// + /// Add a list of entity + /// + /// List of entity + Task AddRange(T[] entities); + + /// + /// Hard delete an entity + /// + /// Entity + void HardDelete(T entity); + + /// + /// Hard delete a list of entity + /// + /// List of entity + void HardDeleteRange(T[] entities); + + /// + /// Change state of entity to modified + /// + /// Entity + void Update(T entity); + + /// + /// Change state of entities to modified + /// + /// entities + void UpdateRange(T[] entities); + + /// + /// Get entity by its id. + /// + /// Guid + /// Entity + Task GetById(Guid id); + + /// + /// Get all entities. + /// + /// List of entitties + Task> GetAll(); + } +} diff --git a/TogoService.API/Model/Interface/ITodoTaskRepository.cs b/TogoService.API/Model/Interface/ITodoTaskRepository.cs new file mode 100644 index 000000000..5459b4dd9 --- /dev/null +++ b/TogoService.API/Model/Interface/ITodoTaskRepository.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; + +namespace TogoService.API.Model.Interface +{ + public interface ITodoTaskRepository : IRepository + { + void SetUnitOfWork(IUnitOfWork unitOfWork); + + /// + /// Get tasks already added for one user on specific date. + /// + /// + /// + /// List tasks already added. + Task GetAddedTasks(Guid userId, DateTime todoDate); + } +} \ No newline at end of file diff --git a/TogoService.API/Model/Interface/IUnitOfWork.cs b/TogoService.API/Model/Interface/IUnitOfWork.cs new file mode 100644 index 000000000..64b0ad112 --- /dev/null +++ b/TogoService.API/Model/Interface/IUnitOfWork.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using TogoService.API.Infrastructure.Database; +using TogoService.API.Infrastructure.Repository; + +namespace TogoService.API.Model.Interface +{ + public interface IUnitOfWork + { + BaseRepository GenericRepository() where T : class; + TogoDbContext Context { get; } + Task CreateTransactionAsync(); + Task CommitAsync(); + Task RollbackAsync(); + Task Save(); + } +} \ No newline at end of file diff --git a/TogoService.API/Model/TodoTask.cs b/TogoService.API/Model/TodoTask.cs new file mode 100644 index 000000000..8fd3e432b --- /dev/null +++ b/TogoService.API/Model/TodoTask.cs @@ -0,0 +1,39 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using TogoService.API.Infrastructure.Helper.Constant; + +namespace TogoService.API.Model +{ + [Table("TodoTask")] + public class TodoTask : BaseEntity + { + public TodoTask() : base() { } + + public TodoTask(string name, string description, DateTime todoDay, Guid userId, User user) : base() + { + Name = name; + Description = description; + TodoDay = todoDay; + UserId = userId; + User = user; + } + + [Required] + [Column("name")] + [MaxLength(EntityConstantsCollection.MaxLengthDataTypeVarchar)] + public string Name { get; set; } + + [Column("description")] + [MaxLength(EntityConstantsCollection.MaxLengthDataTypeVarchar)] + public string Description { get; set; } + + [Required] + [Column("todoDay")] + public DateTime TodoDay { get; set; } + + [Column("userId")] + public Guid UserId { get; set; } + public virtual User User { get; set; } + } +} \ No newline at end of file diff --git a/TogoService.API/Model/User.cs b/TogoService.API/Model/User.cs new file mode 100644 index 000000000..ee7de4411 --- /dev/null +++ b/TogoService.API/Model/User.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using TogoService.API.Infrastructure.Helper.Constant; + +namespace TogoService.API.Model +{ + [Table("User")] + public class User : BaseEntity + { + public User() : base() { } + + public User(string name, uint maxDailyTasks, IList tasks) : base() + { + Name = name; + MaxDailyTasks = maxDailyTasks; + Tasks = tasks; + } + + [Required] + [Column("name")] + [MaxLength(EntityConstantsCollection.MaxLengthDataTypeVarchar)] + public string Name { get; set; } + + [Column("maxDailyTasks")] + public uint MaxDailyTasks { get; set; } + + public virtual IList Tasks { get; set; } + } +} \ No newline at end of file diff --git a/TogoService.API/Program.cs b/TogoService.API/Program.cs new file mode 100644 index 000000000..ac16af6c8 --- /dev/null +++ b/TogoService.API/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace TogoService.API +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} \ No newline at end of file diff --git a/TogoService.API/Properties/launchSettings.json b/TogoService.API/Properties/launchSettings.json new file mode 100644 index 000000000..d9f3eeaf7 --- /dev/null +++ b/TogoService.API/Properties/launchSettings.json @@ -0,0 +1,43 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:51900", + "sslPort": 44369, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "ProductService.API": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/weatherforecast", + "publishAllPorts": true, + "useSSL": true + } + }, + "AWS" : { + "Region": "eu-central-1" + } +} diff --git a/TogoService.API/Startup.cs b/TogoService.API/Startup.cs new file mode 100644 index 000000000..c116bbbf7 --- /dev/null +++ b/TogoService.API/Startup.cs @@ -0,0 +1,128 @@ +using AutoMapper; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; +using TogoService.API.Filter; +using TogoService.API.Infrastructure.Database; +using TogoService.API.Infrastructure.Repository; +using TogoService.API.Model.Interface; +using System; +using System.IO; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace TogoService.API +{ + public class Startup + { + string swaggerDocVersion = "v1"; + string swaggerDocTitle = "Togo Service Api"; + ILoggerFactory _loggerFactory = new LoggerFactory(); + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // var writerConnectionStr = Environment.GetEnvironmentVariable(EnvironmentVariableNames.EnvNameConnStringWriter); + // var readerConnectionStr = Environment.GetEnvironmentVariable(EnvironmentVariableNames.EnvNameConnStringReader); + + services.AddDbContext(options => options.UseLazyLoadingProxies() + .ConfigureWarnings(b => b.Log((RelationalEventId.CommandCreated, LogLevel.Information), + (RelationalEventId.CommandExecuted, LogLevel.Information), + (RelationalEventId.TransactionCommitted, LogLevel.Information))) + .UseSqlite("Data Source=TogoService.db;") + ); + + // Repository - Unit of Work + services.AddScoped(); + services.AddScoped(); + + services.AddControllers(); + + services.AddAutoMapper(typeof(Startup)); + + services.AddLogging(logging => + { + logging.AddConsole(); + logging.SetMinimumLevel(LogLevel.Information); + }); + + services.AddCors(c => c.AddPolicy("AllowOrigin", o => o.AllowAnyOrigin())); + services.AddControllers() + .AddJsonOptions(options => + options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter())); + services.AddScoped(); + + services.AddSwaggerGen(options => + { + options.SwaggerDoc(swaggerDocVersion, new OpenApiInfo { Title = swaggerDocTitle, Version = swaggerDocVersion }); + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + options.IncludeXmlComments(xmlPath); + options.CustomSchemaIds(c => c.FullName); + + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Description = "Standard Authorization header using the Bearer scheme. Example: \"bearer {token}\"", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer" + }); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // Apply EF migration to database + using (var scope = app.ApplicationServices.CreateScope()) + { + using var context = scope.ServiceProvider.GetService(); + + for (int i = 1; i <= 5; i++) + { + try + { + Console.WriteLine($"Try execute migrate command time: {i}"); + context.Database.Migrate(); + break; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + } + } + + app.UseExceptionHandler("/api/error"); + + app.UseHttpsRedirection(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", swaggerDocTitle); + }); + + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + app.UseCors(o => o.AllowAnyOrigin()); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} \ No newline at end of file diff --git a/TogoService.API/TogoService.API.csproj b/TogoService.API/TogoService.API.csproj new file mode 100644 index 000000000..89514b647 --- /dev/null +++ b/TogoService.API/TogoService.API.csproj @@ -0,0 +1,34 @@ + + + netcoreapp3.1 + b93f848a-0079-4d63-9925-8b05443f2611 + Exe + true + $(NoWarn);1591 + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + Always + + + \ No newline at end of file diff --git a/TogoService.API/TogoService.db b/TogoService.API/TogoService.db new file mode 100644 index 000000000..97121420b Binary files /dev/null and b/TogoService.API/TogoService.db differ diff --git a/TogoService.API/TogoService.db-shm b/TogoService.API/TogoService.db-shm new file mode 100644 index 000000000..d171e4a29 Binary files /dev/null and b/TogoService.API/TogoService.db-shm differ diff --git a/TogoService.API/TogoService.db-wal b/TogoService.API/TogoService.db-wal new file mode 100644 index 000000000..a5dc80f53 Binary files /dev/null and b/TogoService.API/TogoService.db-wal differ diff --git a/TogoService.IntegrationTest/Helper/FakeData.cs b/TogoService.IntegrationTest/Helper/FakeData.cs new file mode 100644 index 000000000..71d26863c --- /dev/null +++ b/TogoService.IntegrationTest/Helper/FakeData.cs @@ -0,0 +1,49 @@ +using System; +using Bogus; +using TogoService.API.Dto; +using TogoService.API.Model; + +namespace TogoService.IntegrationTest.Helper +{ + public class FakeData + { + private static int GenerateRandomArrLength() + { + Random random = new Random(); + return random.Next(1, 11); + } + + public static TaskRequest[] GenerateTaskRequests(int numberOfItems = 0) + { + if (numberOfItems == 0) + { + numberOfItems = GenerateRandomArrLength(); + } + + var fakerTaskRequest = new Faker() + .RuleFor(s => s.Name, f => f.Name.JobTitle()) + .RuleFor(s => s.Description, f => f.Name.JobDescriptor()); + + return fakerTaskRequest.Generate(numberOfItems).ToArray(); + } + + public static NewTaskRequest GenerateNewTaskRequest(int numberOfTasks = 0) + { + var fakerNewTaskRequest = new Faker() + .RuleFor(s => s.Date, f => DateTime.UtcNow) + .RuleFor(s => s.Tasks, _ => GenerateTaskRequests(numberOfTasks)); + + return fakerNewTaskRequest.Generate(); + } + + public static User GenerateUser(uint maxDailyTask) + { + var fakerUser = new Faker() + .RuleFor(s => s.Id, _ => Guid.NewGuid()) + .RuleFor(s => s.Name, f => f.Name.FullName()) + .RuleFor(s => s.MaxDailyTasks, _ => maxDailyTask); + + return fakerUser.Generate(); + } + } +} \ No newline at end of file diff --git a/TogoService.IntegrationTest/TestFixture/UserControllerFixture.cs b/TogoService.IntegrationTest/TestFixture/UserControllerFixture.cs new file mode 100644 index 000000000..1c65d5351 --- /dev/null +++ b/TogoService.IntegrationTest/TestFixture/UserControllerFixture.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading.Tasks; +using Autofac; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Logging; +using Moq; +using TogoService.API.Controller; +using TogoService.API.Infrastructure.Database; +using TogoService.API.Infrastructure.Mapper; +using TogoService.API.Infrastructure.Repository; +using TogoService.API.Model; +using TogoService.API.Model.Interface; +using TogoService.IntegrationTest.Helper; + +namespace TogoService.IntegrationTest.TestFixture +{ + public class UserControllerFixture : IDisposable + { + public IContainer Container { get; private set; } + public User UserWith0MaxDailyTasks { get; set; } + public User UserWith10MaxDailyTasks { get; set; } + + public UserControllerFixture() + { + // Register all DI need for unit test. + var builder = new ContainerBuilder(); + + // Mapper + builder.Register(context => new MapperConfiguration(cfg => + { + cfg.AddProfile(new AutoMapperProfile()); + })); + builder.Register(c => + { + var context = c.Resolve(); + var config = context.Resolve(); + return config.CreateMapper(context.Resolve); + }).As().SingleInstance(); + + // DbContext + var contextOptions = new DbContextOptionsBuilder() + .UseLazyLoadingProxies() + .ConfigureWarnings(b => b.Log((RelationalEventId.CommandCreated, LogLevel.Debug), + (RelationalEventId.CommandExecuted, LogLevel.Debug), + (RelationalEventId.TransactionCommitted, LogLevel.Debug))) + .UseSqlite(@"Data Source=TogoService.db;") + .Options; + builder.RegisterType() + .WithParameter(new TypedParameter(typeof(DbContextOptions), contextOptions)); + + builder.RegisterType().As(); + builder.RegisterType().As(); + + var logger = new Mock>(); + + builder.RegisterType().As() + .WithParameter(new TypedParameter(typeof(ILogger), logger.Object)) + .SingleInstance(); + + //Build container + Container = builder.Build(); + + Task task = InitData(new TogoDbContext(contextOptions)); + task.Wait(); + } + + public void Dispose() + { + Container.Dispose(); + } + + private async Task InitData(TogoDbContext dbContext) + { + IUnitOfWork unitOfWork = new UnitOfWork(dbContext); + UserWith0MaxDailyTasks = FakeData.GenerateUser(0); + UserWith10MaxDailyTasks = FakeData.GenerateUser(10); + await unitOfWork.GenericRepository().Add(UserWith0MaxDailyTasks); + await unitOfWork.GenericRepository().Add(UserWith10MaxDailyTasks); + await unitOfWork.Save(); + dbContext.Dispose(); + } + } +} diff --git a/TogoService.IntegrationTest/Tests/UserController/AddTaskForUser.cs b/TogoService.IntegrationTest/Tests/UserController/AddTaskForUser.cs new file mode 100644 index 000000000..bf57f3abf --- /dev/null +++ b/TogoService.IntegrationTest/Tests/UserController/AddTaskForUser.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TogoService.API.Dto; +using TogoService.API.Infrastructure.Helper.MessageUtil; +using TogoService.API.Model; +using TogoService.IntegrationTest.Helper; +using Xunit; + +namespace TogoService.IntegrationTest.Tests +{ + public partial class UserControllerTest + { + [Fact] + public async Task Successful_201AddedSuccessfully() + { + // Arrange + NewTaskRequest request = FakeData.GenerateNewTaskRequest(2); + var expectedResponse = new CommonResponse(StatusCodes.Status201Created, + CommonMessages.Ok, + CommonMessages.GetSuccessfulAddedItemsMsg(typeof(TodoTask).Name, request.Tasks.Length)); + + // Act + var result = (ObjectResult)await _userController.AddTasksForUser(_userWith10MaxDailyTasks.Id, request); + + // Assert + Assert.NotNull(result); + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + CommonResponse actualResponse = (CommonResponse)result.Value; + Assert.Equal(expectedResponse.StatusCode, actualResponse.StatusCode); + Assert.Equal(expectedResponse.Message, actualResponse.Message); + Assert.Equal(expectedResponse.Result, actualResponse.Result); + } + + [Fact] + public async Task UserIdNotFound_404NotFound() + { + // Arrange + Guid notFoundUserId = Guid.NewGuid(); + var expectedResponse = new CommonResponse(StatusCodes.Status404NotFound, + CommonMessages.GetCannotFindMsg(typeof(API.Model.User).Name, notFoundUserId), + null); + + // Act + var result = (ObjectResult)await _userController.AddTasksForUser(notFoundUserId, FakeData.GenerateNewTaskRequest(1)); + + // Assert + Assert.NotNull(result); + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + CommonResponse actualResponse = (CommonResponse)result.Value; + Assert.Equal(expectedResponse.StatusCode, actualResponse.StatusCode); + Assert.Equal(expectedResponse.Message, actualResponse.Message); + } + + [Fact] + public async Task AddMoreThanMaxTasksPerDay_422ReachoutMaxTaskPerDay() + { + // Arrange + NewTaskRequest request = FakeData.GenerateNewTaskRequest(2); + var expectedResponse = new CommonResponse(StatusCodes.Status422UnprocessableEntity, + UserControllerErrMsg.ReachoutMaxTaskPerDay, + null); + + // Act + var result = (ObjectResult)await _userController.AddTasksForUser(_userWith0MaxDailyTasks.Id, request); + + // Assert + Assert.NotNull(result); + Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode); + CommonResponse actualResponse = (CommonResponse)result.Value; + Assert.Equal(expectedResponse.StatusCode, actualResponse.StatusCode); + Assert.Equal(expectedResponse.Message, actualResponse.Message); + } + } +} \ No newline at end of file diff --git a/TogoService.IntegrationTest/Tests/UserController/UserControllerTest.cs b/TogoService.IntegrationTest/Tests/UserController/UserControllerTest.cs new file mode 100644 index 000000000..951b14f47 --- /dev/null +++ b/TogoService.IntegrationTest/Tests/UserController/UserControllerTest.cs @@ -0,0 +1,30 @@ +using Autofac; +using AutoMapper; +using TogoService.API.Controller; +using TogoService.API.Model; +using TogoService.IntegrationTest.TestFixture; +using Xunit; + +namespace TogoService.IntegrationTest.Tests +{ + + public partial class UserControllerTest : IClassFixture + { + private UserController _userController; + private IMapper _mapper; + private User _userWith0MaxDailyTasks; + private User _userWith10MaxDailyTasks; + + public UserControllerTest(UserControllerFixture fixture) + { + using (var scope = fixture.Container.BeginLifetimeScope()) + { + _userController = scope.Resolve(); + _mapper = scope.Resolve(); + } + + _userWith0MaxDailyTasks = fixture.UserWith0MaxDailyTasks; + _userWith10MaxDailyTasks = fixture.UserWith10MaxDailyTasks; + } + } +} \ No newline at end of file diff --git a/TogoService.IntegrationTest/TogoService.IntegrationTest.csproj b/TogoService.IntegrationTest/TogoService.IntegrationTest.csproj new file mode 100644 index 000000000..59449bbab --- /dev/null +++ b/TogoService.IntegrationTest/TogoService.IntegrationTest.csproj @@ -0,0 +1,25 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + + Always + + + diff --git a/TogoService.IntegrationTest/TogoService.db b/TogoService.IntegrationTest/TogoService.db new file mode 100644 index 000000000..934e5105b Binary files /dev/null and b/TogoService.IntegrationTest/TogoService.db differ diff --git a/TogoService.UnitTest/Helper/FakeData.cs b/TogoService.UnitTest/Helper/FakeData.cs new file mode 100644 index 000000000..6ac7ca34d --- /dev/null +++ b/TogoService.UnitTest/Helper/FakeData.cs @@ -0,0 +1,62 @@ +using System; +using Bogus; +using TogoService.API.Dto; +using TogoService.API.Model; + +namespace TogoService.UnitTest.Helper +{ + public class FakeData + { + private static int GenerateRandomArrLength() + { + Random random = new Random(); + return random.Next(1, 11); + } + + public static TaskRequest[] GenerateTaskRequests(int numberOfItems = 0) + { + if (numberOfItems == 0) + { + numberOfItems = GenerateRandomArrLength(); + } + + var fakerTaskRequest = new Faker() + .RuleFor(s => s.Name, f => f.Name.JobTitle()) + .RuleFor(s => s.Description, f => f.Name.JobDescriptor()); + + return fakerTaskRequest.Generate(numberOfItems).ToArray(); + } + + public static NewTaskRequest GenerateNewTaskRequest(int numberOfTasks = 0) + { + var fakerNewTaskRequest = new Faker() + .RuleFor(s => s.Date, f => DateTime.UtcNow) + .RuleFor(s => s.Tasks, _ => GenerateTaskRequests(numberOfTasks)); + + return fakerNewTaskRequest.Generate(); + } + + public static User GenerateUser(uint maxDailyTask) + { + var fakerUser = new Faker() + .RuleFor(s => s.Id, _ => Guid.NewGuid()) + .RuleFor(s => s.MaxDailyTasks, _ => maxDailyTask); + + return fakerUser.Generate(); + } + + public static TodoTask[] GenerateTodoTasks(int numberOfItems = 0) + { + if (numberOfItems == 0) + { + numberOfItems = GenerateRandomArrLength(); + } + + var fakerTodoTask = new Faker() + .RuleFor(s => s.Name, f => f.Name.JobTitle()) + .RuleFor(s => s.Description, f => f.Name.JobDescriptor()); + + return fakerTodoTask.Generate(numberOfItems).ToArray(); + } + } +} \ No newline at end of file diff --git a/TogoService.UnitTest/TestFixture/UserControllerFixture.cs b/TogoService.UnitTest/TestFixture/UserControllerFixture.cs new file mode 100644 index 000000000..0efa3895d --- /dev/null +++ b/TogoService.UnitTest/TestFixture/UserControllerFixture.cs @@ -0,0 +1,56 @@ +using System; +using Autofac; +using AutoMapper; +using Microsoft.Extensions.Logging; +using Moq; +using TogoService.API.Controller; +using TogoService.API.Infrastructure.Mapper; +using TogoService.API.Infrastructure.Repository; +using TogoService.API.Model.Interface; + +namespace TogoService.UnitTest.TestFixture +{ + public class UserControllerFixture : IDisposable + { + public IContainer Container { get; private set; } + public Mock MockUnitOfWork { get; set; } + public Mock> MockGenericUserRepository { get; set; } + public Mock MockTodoTaskRepository { get; set; } + + public UserControllerFixture() + { + // Register all DI need for unit test. + var builder = new ContainerBuilder(); + + // Mapper + builder.Register(context => new MapperConfiguration(cfg => + { + cfg.AddProfile(new AutoMapperProfile()); + })); + builder.Register(c => + { + var context = c.Resolve(); + var config = context.Resolve(); + return config.CreateMapper(context.Resolve); + }).As().SingleInstance(); + + var logger = new Mock>(); + MockUnitOfWork = new Mock(); + MockGenericUserRepository = new Mock>(); + MockTodoTaskRepository = new Mock(); + + builder.RegisterType().As() + .WithParameter(new TypedParameter(typeof(ILogger), logger.Object)) + .WithParameter(new TypedParameter(typeof(IUnitOfWork), MockUnitOfWork.Object)) + .WithParameter(new TypedParameter(typeof(ITodoTaskRepository), MockTodoTaskRepository.Object)); + + //Build container + Container = builder.Build(); + } + + public void Dispose() + { + Container.Dispose(); + } + } +} diff --git a/TogoService.UnitTest/Tests/UserController/AddTaskForUser.cs b/TogoService.UnitTest/Tests/UserController/AddTaskForUser.cs new file mode 100644 index 000000000..75ed10ebd --- /dev/null +++ b/TogoService.UnitTest/Tests/UserController/AddTaskForUser.cs @@ -0,0 +1,161 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using TogoService.API.Dto; +using TogoService.API.Infrastructure.Helper.MessageUtil; +using TogoService.API.Model; +using TogoService.UnitTest.Helper; +using Xunit; + +namespace TogoService.UnitTest.Tests +{ + public partial class UserControllerTest + { + private Guid _emptyGuid = Guid.Empty; + + [Fact] + public async Task NullRequest_ThrowException() + { + // Act + var exception = await Record.ExceptionAsync(async () => await _userController.AddTasksForUser(_emptyGuid, null)); + + // Assert + Assert.NotNull(exception); + } + + [Fact] + public async Task MissingTodoDate_404NotFound() + { + // Arrange + Guid userId = Guid.NewGuid(); + var expectedResponse = new CommonResponse(StatusCodes.Status400BadRequest, + UserControllerErrMsg.MissingTodoDay, + null); + NewTaskRequest request = FakeData.GenerateNewTaskRequest(); + request.Date = new DateTime(); + + // Act + var result = (ObjectResult)await _userController.AddTasksForUser(userId, request); + + // Assert + Assert.NotNull(result); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + CommonResponse actualResponse = (CommonResponse)result.Value; + Assert.Equal(expectedResponse.StatusCode, actualResponse.StatusCode); + Assert.Equal(expectedResponse.Message, actualResponse.Message); + } + + [Fact] + public async Task EmptyUserId_404NotFound() + { + // Arrange + var expectedResponse = new CommonResponse(StatusCodes.Status404NotFound, + CommonMessages.GetCannotFindMsg(typeof(API.Model.User).Name, _emptyGuid), + null); + + // Act + var result = (ObjectResult)await _userController.AddTasksForUser(_emptyGuid, FakeData.GenerateNewTaskRequest()); + + // Assert + Assert.NotNull(result); + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + CommonResponse actualResponse = (CommonResponse)result.Value; + Assert.Equal(expectedResponse.StatusCode, actualResponse.StatusCode); + Assert.Equal(expectedResponse.Message, actualResponse.Message); + } + + [Fact] + public async Task UserIdNotFound_404NotFound() + { + // Arrange + Guid userId = Guid.NewGuid(); + _mockGenericUserRepository.Setup(q => q.GetById(userId)).ReturnsAsync((API.Model.User)null); + var expectedResponse = new CommonResponse(StatusCodes.Status404NotFound, + CommonMessages.GetCannotFindMsg(typeof(API.Model.User).Name, userId), + null); + + // Act + var result = (ObjectResult)await _userController.AddTasksForUser(userId, FakeData.GenerateNewTaskRequest()); + + // Assert + Assert.NotNull(result); + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + CommonResponse actualResponse = (CommonResponse)result.Value; + Assert.Equal(expectedResponse.StatusCode, actualResponse.StatusCode); + Assert.Equal(expectedResponse.Message, actualResponse.Message); + } + + [Fact] + public async Task UnexpectedException_500InternalServerError() + { + // Arrange + Guid userId = Guid.NewGuid(); + string expectedExceptionMsg = "Exception"; + _mockGenericUserRepository.Setup(q => q.GetById(userId)).ThrowsAsync(new Exception(expectedExceptionMsg)); + var expectedResponse = new CommonResponse(StatusCodes.Status500InternalServerError, + expectedExceptionMsg, + null); + + // Act + var result = (ObjectResult)await _userController.AddTasksForUser(userId, FakeData.GenerateNewTaskRequest()); + + // Assert + Assert.NotNull(result); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + CommonResponse actualResponse = (CommonResponse)result.Value; + Assert.Equal(expectedResponse.StatusCode, actualResponse.StatusCode); + Assert.Equal(expectedResponse.Message, actualResponse.Message); + } + + [Fact] + public async Task AddMoreThanMaxTasksPerDay_422ReachoutMaxTaskPerDay() + { + // Arrange + User user = FakeData.GenerateUser(10); + NewTaskRequest request = FakeData.GenerateNewTaskRequest(); + TodoTask[] addedTasks = FakeData.GenerateTodoTasks((int)user.MaxDailyTasks); + _mockGenericUserRepository.Setup(q => q.GetById(user.Id)).ReturnsAsync(user); + _mockTodoTaskRepository.Setup(q => q.GetAddedTasks(user.Id, request.Date)).ReturnsAsync(addedTasks); + var expectedResponse = new CommonResponse(StatusCodes.Status422UnprocessableEntity, + UserControllerErrMsg.ReachoutMaxTaskPerDay, + null); + + // Act + var result = (ObjectResult)await _userController.AddTasksForUser(user.Id, request); + + // Assert + Assert.NotNull(result); + Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode); + CommonResponse actualResponse = (CommonResponse)result.Value; + Assert.Equal(expectedResponse.StatusCode, actualResponse.StatusCode); + Assert.Equal(expectedResponse.Message, actualResponse.Message); + } + + [Fact] + public async Task Successful_201AddedSuccessfully() + { + // Arrange + User user = FakeData.GenerateUser(10); + NewTaskRequest request = FakeData.GenerateNewTaskRequest(4); + TodoTask[] addedTasks = FakeData.GenerateTodoTasks(1); + _mockGenericUserRepository.Setup(q => q.GetById(user.Id)).ReturnsAsync(user); + _mockTodoTaskRepository.Setup(q => q.GetAddedTasks(user.Id, request.Date)).ReturnsAsync(addedTasks); + var expectedResponse = new CommonResponse(StatusCodes.Status201Created, + CommonMessages.Ok, + CommonMessages.GetSuccessfulAddedItemsMsg(typeof(TodoTask).Name, request.Tasks.Length)); + + // Act + var result = (ObjectResult)await _userController.AddTasksForUser(user.Id, request); + + // Assert + Assert.NotNull(result); + Assert.Equal(StatusCodes.Status201Created, result.StatusCode); + CommonResponse actualResponse = (CommonResponse)result.Value; + Assert.Equal(expectedResponse.StatusCode, actualResponse.StatusCode); + Assert.Equal(expectedResponse.Message, actualResponse.Message); + Assert.Equal(expectedResponse.Result, actualResponse.Result); + } + } +} \ No newline at end of file diff --git a/TogoService.UnitTest/Tests/UserController/UserControllerTest.cs b/TogoService.UnitTest/Tests/UserController/UserControllerTest.cs new file mode 100644 index 000000000..fada9ee62 --- /dev/null +++ b/TogoService.UnitTest/Tests/UserController/UserControllerTest.cs @@ -0,0 +1,35 @@ +using Autofac; +using AutoMapper; +using Moq; +using TogoService.API.Controller; +using TogoService.API.Infrastructure.Repository; +using TogoService.API.Model.Interface; +using TogoService.UnitTest.TestFixture; +using Xunit; + +namespace TogoService.UnitTest.Tests +{ + + public partial class UserControllerTest : IClassFixture + { + private UserController _userController; + private IMapper _mapper; + private Mock _mockUnitOfWork { get; set; } + private Mock> _mockGenericUserRepository { get; set; } + private Mock _mockTodoTaskRepository { get; set; } + + public UserControllerTest(UserControllerFixture fixture) + { + using (var scope = fixture.Container.BeginLifetimeScope()) + { + _userController = scope.Resolve(); + _mapper = scope.Resolve(); + } + + _mockUnitOfWork = fixture.MockUnitOfWork; + _mockGenericUserRepository = fixture.MockGenericUserRepository; + _mockUnitOfWork.Setup(q => q.GenericRepository()).Returns(_mockGenericUserRepository.Object); + _mockTodoTaskRepository = fixture.MockTodoTaskRepository; + } + } +} \ No newline at end of file diff --git a/TogoService.UnitTest/TogoService.UnitTest.csproj b/TogoService.UnitTest/TogoService.UnitTest.csproj new file mode 100644 index 000000000..a44af2b5f --- /dev/null +++ b/TogoService.UnitTest/TogoService.UnitTest.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + diff --git a/TogoService.sln b/TogoService.sln new file mode 100644 index 000000000..da7bd69b1 --- /dev/null +++ b/TogoService.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30104.148 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TogoService.API", "TogoService.API\TogoService.API.csproj", "{42ADD663-01C5-4859-BDF5-261557975D0B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TogoService.UnitTest", "TogoService.UnitTest\TogoService.UnitTest.csproj", "{411AEBA1-A8E9-4F2B-A5D6-51FDD789ADE9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TogoService.IntegrationTest", "TogoService.IntegrationTest\TogoService.IntegrationTest.csproj", "{32666885-34F9-424A-976A-1A40B1CF291A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {42ADD663-01C5-4859-BDF5-261557975D0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42ADD663-01C5-4859-BDF5-261557975D0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42ADD663-01C5-4859-BDF5-261557975D0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42ADD663-01C5-4859-BDF5-261557975D0B}.Release|Any CPU.Build.0 = Release|Any CPU + {411AEBA1-A8E9-4F2B-A5D6-51FDD789ADE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {411AEBA1-A8E9-4F2B-A5D6-51FDD789ADE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {411AEBA1-A8E9-4F2B-A5D6-51FDD789ADE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {411AEBA1-A8E9-4F2B-A5D6-51FDD789ADE9}.Release|Any CPU.Build.0 = Release|Any CPU + {32666885-34F9-424A-976A-1A40B1CF291A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32666885-34F9-424A-976A-1A40B1CF291A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32666885-34F9-424A-976A-1A40B1CF291A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32666885-34F9-424A-976A-1A40B1CF291A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9ABC0DC7-E4BD-4350-A283-C90665CCCAC9} + EndGlobalSection +EndGlobal