From 6695033723400baf333e2fc11e0eabf81398fa4c Mon Sep 17 00:00:00 2001 From: Kirill Taran Date: Wed, 21 Jun 2023 20:46:57 +0400 Subject: [PATCH 1/8] Framework version updated --- README.md | 7 ++++++- TodoApiDTO.csproj | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 466e41fd..551b2102 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ -# TodoService \ No newline at end of file +# TodoService + +1. Äîáàâèòü ñâàããåð +2. Õðàíèòü ñïèñîê çàäà÷ â SQL Server (ñåé÷àñ èñïîëüçóåòñÿ InMemoryDB). Ñòðîêà ñîåäèíåíèÿ äîëæíà õðàíèòüñÿ â ôàéëå appsettings. +3. Ñäåëàòü ðåôàêòîðèíã - âûäåëèòü ñëîè Data Access Layer, Business Layer +4. Íàïèñàòü þíèò-òåñòû íà áèçíåñ îïåðàöèè diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index bba6f6af..98db0615 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -1,17 +1,17 @@ - netcoreapp3.1 + net7.0 - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + From d64aabd41a91a37536883db4b0f09dc2b5f4c3e8 Mon Sep 17 00:00:00 2001 From: Kirill Taran Date: Wed, 21 Jun 2023 20:52:21 +0400 Subject: [PATCH 2/8] Swagger added --- Startup.cs | 5 +++++ TodoApiDTO.csproj | 1 + 2 files changed, 6 insertions(+) diff --git a/Startup.cs b/Startup.cs index bbfbc83d..56bbb2c7 100644 --- a/Startup.cs +++ b/Startup.cs @@ -30,6 +30,8 @@ public void ConfigureServices(IServiceCollection services) services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); services.AddControllers(); + + services.AddSwaggerGen(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -40,6 +42,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseDeveloperExceptionPage(); } + app.UseSwagger(); + app.UseSwaggerUI(); + app.UseHttpsRedirection(); app.UseRouting(); diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index 98db0615..9df97f85 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -12,6 +12,7 @@ + From 6134d4c07533e0cdd20ed0c9e3e41d4c2157c736 Mon Sep 17 00:00:00 2001 From: Kirill Taran Date: Wed, 21 Jun 2023 21:08:04 +0400 Subject: [PATCH 3/8] API method descriptions added --- Controllers/TodoItemsController.cs | 26 ++++++++++++++++++++++++++ Properties/launchSettings.json | 17 ++++++++--------- Startup.cs | 8 +++++++- TodoApiDTO.csproj | 1 + 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index 0ef138e7..f5207ecd 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -8,6 +8,7 @@ namespace TodoApi.Controllers { [Route("api/[controller]")] + [Produces("application/json")] [ApiController] public class TodoItemsController : ControllerBase { @@ -18,6 +19,10 @@ public TodoItemsController(TodoContext context) _context = context; } + /// + /// Gets ToDoItems. + /// + /// [HttpGet] public async Task>> GetTodoItems() { @@ -26,6 +31,11 @@ public async Task>> GetTodoItems() .ToListAsync(); } + /// + /// Gets a specific ToDoItem. + /// + /// Item ID + /// [HttpGet("{id}")] public async Task> GetTodoItem(long id) { @@ -39,6 +49,12 @@ public async Task> GetTodoItem(long id) return ItemToDTO(todoItem); } + /// + /// Updates a specific ToDoItem. + /// + /// Item ID + /// Modified item + /// [HttpPut("{id}")] public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO) { @@ -68,6 +84,11 @@ public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO return NoContent(); } + /// + /// Creates a new ToDoItem. + /// + /// New item + /// [HttpPost] public async Task> CreateTodoItem(TodoItemDTO todoItemDTO) { @@ -86,6 +107,11 @@ public async Task> CreateTodoItem(TodoItemDTO todoItem ItemToDTO(todoItem)); } + /// + /// Deletes a spefific ToDoItem. + /// + /// Item ID + /// [HttpDelete("{id}")] public async Task DeleteTodoItem(long id) { diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 6766196a..b9bf0083 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -1,12 +1,4 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:56416/", - "sslPort": 44331 - } - }, "profiles": { "IIS Express": { "commandName": "IISExpress", @@ -17,11 +9,18 @@ }, "TodoApiDTO": { "commandName": "Project", - "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5001;http://localhost:5000" } + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:56416/", + "sslPort": 44331 + } } } \ No newline at end of file diff --git a/Startup.cs b/Startup.cs index 56bbb2c7..6958ef8f 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -31,7 +33,11 @@ public void ConfigureServices(IServiceCollection services) opt.UseInMemoryDatabase("TodoList")); services.AddControllers(); - services.AddSwaggerGen(); + services.AddSwaggerGen(options => + { + var xmlFileName = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFileName)); + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index 9df97f85..5194dd98 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -2,6 +2,7 @@ net7.0 + True From f459f272c072e633cf1cc1fd0e117627c88407b6 Mon Sep 17 00:00:00 2001 From: Kirill Taran Date: Wed, 21 Jun 2023 22:32:23 +0400 Subject: [PATCH 4/8] An InMemory DB instance replaced with a Sql Server ainstance --- Controllers/TodoItemsController.cs | 3 +- .../20230621182622_InitialCreate.Designer.cs | 51 +++++++++++++++++++ Migrations/20230621182622_InitialCreate.cs | 36 +++++++++++++ Migrations/TodoContextModelSnapshot.cs | 48 +++++++++++++++++ Startup.cs | 3 +- TodoApiDTO.csproj | 12 +++++ appsettings.json | 3 ++ 7 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 Migrations/20230621182622_InitialCreate.Designer.cs create mode 100644 Migrations/20230621182622_InitialCreate.cs create mode 100644 Migrations/TodoContextModelSnapshot.cs diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index f5207ecd..044b5b87 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Linq; +using System.Net.Sockets; using System.Threading.Tasks; using TodoApi.Models; @@ -137,6 +138,6 @@ private static TodoItemDTO ItemToDTO(TodoItem todoItem) => Id = todoItem.Id, Name = todoItem.Name, IsComplete = todoItem.IsComplete - }; + }; } } diff --git a/Migrations/20230621182622_InitialCreate.Designer.cs b/Migrations/20230621182622_InitialCreate.Designer.cs new file mode 100644 index 00000000..a68bf30e --- /dev/null +++ b/Migrations/20230621182622_InitialCreate.Designer.cs @@ -0,0 +1,51 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TodoApi.Models; + +#nullable disable + +namespace TodoApiDTO.Migrations +{ + [DbContext(typeof(TodoContext))] + [Migration("20230621182622_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("TodoApi.Models.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20230621182622_InitialCreate.cs b/Migrations/20230621182622_InitialCreate.cs new file mode 100644 index 00000000..7d1c20ca --- /dev/null +++ b/Migrations/20230621182622_InitialCreate.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TodoApiDTO.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TodoItems", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: true), + IsComplete = table.Column(type: "bit", nullable: false), + Secret = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TodoItems", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TodoItems"); + } + } +} diff --git a/Migrations/TodoContextModelSnapshot.cs b/Migrations/TodoContextModelSnapshot.cs new file mode 100644 index 00000000..027d39f1 --- /dev/null +++ b/Migrations/TodoContextModelSnapshot.cs @@ -0,0 +1,48 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TodoApi.Models; + +#nullable disable + +namespace TodoApiDTO.Migrations +{ + [DbContext(typeof(TodoContext))] + partial class TodoContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("TodoApi.Models.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Startup.cs b/Startup.cs index 6958ef8f..c2cde98e 100644 --- a/Startup.cs +++ b/Startup.cs @@ -30,7 +30,8 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { services.AddDbContext(opt => - opt.UseInMemoryDatabase("TodoList")); + opt.UseSqlServer(Configuration.GetConnectionString("VelvetechDatabase"))); + services.AddControllers(); services.AddSwaggerGen(options => diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index 5194dd98..5605b3b4 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -5,6 +5,14 @@ True + + 1701;1702;CS1591; + + + + 1701;1702;CS1591; + + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -12,6 +20,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/appsettings.json b/appsettings.json index d9d9a9bf..222d0e99 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "VelvetechDatabase": "Server=localhost; Database=default; User Id=sa; Password=DiwhAWmXFZ@b3jXA8T.A; TrustServerCertificate=True" + }, "Logging": { "LogLevel": { "Default": "Information", From 4d996015476a3a9581e795e8a76df6697f1fc179 Mon Sep 17 00:00:00 2001 From: Kirill Taran Date: Wed, 21 Jun 2023 22:32:23 +0400 Subject: [PATCH 5/8] An InMemory DB instance replaced with a Sql Server ainstance --- Controllers/TodoItemsController.cs | 3 +- .../20230621182622_InitialCreate.Designer.cs | 51 +++++++++++++++++++ Migrations/20230621182622_InitialCreate.cs | 36 +++++++++++++ Migrations/TodoContextModelSnapshot.cs | 48 +++++++++++++++++ Startup.cs | 3 +- TodoApiDTO.csproj | 12 +++++ appsettings.json | 3 ++ 7 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 Migrations/20230621182622_InitialCreate.Designer.cs create mode 100644 Migrations/20230621182622_InitialCreate.cs create mode 100644 Migrations/TodoContextModelSnapshot.cs diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index f5207ecd..044b5b87 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Linq; +using System.Net.Sockets; using System.Threading.Tasks; using TodoApi.Models; @@ -137,6 +138,6 @@ private static TodoItemDTO ItemToDTO(TodoItem todoItem) => Id = todoItem.Id, Name = todoItem.Name, IsComplete = todoItem.IsComplete - }; + }; } } diff --git a/Migrations/20230621182622_InitialCreate.Designer.cs b/Migrations/20230621182622_InitialCreate.Designer.cs new file mode 100644 index 00000000..0478f458 --- /dev/null +++ b/Migrations/20230621182622_InitialCreate.Designer.cs @@ -0,0 +1,51 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TodoApiDTO.Data; + +#nullable disable + +namespace TodoApiDTO.Migrations +{ + [DbContext(typeof(TodoContext))] + [Migration("20230621182622_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("TodoApi.Models.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20230621182622_InitialCreate.cs b/Migrations/20230621182622_InitialCreate.cs new file mode 100644 index 00000000..7d1c20ca --- /dev/null +++ b/Migrations/20230621182622_InitialCreate.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TodoApiDTO.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TodoItems", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: true), + IsComplete = table.Column(type: "bit", nullable: false), + Secret = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TodoItems", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TodoItems"); + } + } +} diff --git a/Migrations/TodoContextModelSnapshot.cs b/Migrations/TodoContextModelSnapshot.cs new file mode 100644 index 00000000..bd70a378 --- /dev/null +++ b/Migrations/TodoContextModelSnapshot.cs @@ -0,0 +1,48 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TodoApiDTO.Data; + +#nullable disable + +namespace TodoApiDTO.Migrations +{ + [DbContext(typeof(TodoContext))] + partial class TodoContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("TodoApi.Models.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Startup.cs b/Startup.cs index 6958ef8f..c2cde98e 100644 --- a/Startup.cs +++ b/Startup.cs @@ -30,7 +30,8 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { services.AddDbContext(opt => - opt.UseInMemoryDatabase("TodoList")); + opt.UseSqlServer(Configuration.GetConnectionString("VelvetechDatabase"))); + services.AddControllers(); services.AddSwaggerGen(options => diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index 5194dd98..5605b3b4 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -5,6 +5,14 @@ True + + 1701;1702;CS1591; + + + + 1701;1702;CS1591; + + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -12,6 +20,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/appsettings.json b/appsettings.json index d9d9a9bf..222d0e99 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "VelvetechDatabase": "Server=localhost; Database=default; User Id=sa; Password=DiwhAWmXFZ@b3jXA8T.A; TrustServerCertificate=True" + }, "Logging": { "LogLevel": { "Default": "Information", From c8e38c82a04453946deca9aa8486b3ed10f7a3d1 Mon Sep 17 00:00:00 2001 From: Kirill Taran Date: Wed, 21 Jun 2023 23:50:55 +0400 Subject: [PATCH 6/8] Data layer separeted --- {Models => Data}/TodoContext.cs | 2 +- {Models => Data}/TodoItem.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename {Models => Data}/TodoContext.cs (91%) rename {Models => Data}/TodoItem.cs (89%) diff --git a/Models/TodoContext.cs b/Data/TodoContext.cs similarity index 91% rename from Models/TodoContext.cs rename to Data/TodoContext.cs index 6e59e363..9e4306eb 100644 --- a/Models/TodoContext.cs +++ b/Data/TodoContext.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace TodoApi.Models +namespace TodoApiDTO.Data { public class TodoContext : DbContext { diff --git a/Models/TodoItem.cs b/Data/TodoItem.cs similarity index 89% rename from Models/TodoItem.cs rename to Data/TodoItem.cs index 1f6e5465..1974ba16 100644 --- a/Models/TodoItem.cs +++ b/Data/TodoItem.cs @@ -1,4 +1,4 @@ -namespace TodoApi.Models +namespace TodoApiDTO.Data { #region snippet public class TodoItem From 8fc7a83849d07f4fc722b4923837aa1b383d23c3 Mon Sep 17 00:00:00 2001 From: Kirill Taran Date: Wed, 21 Jun 2023 23:51:59 +0400 Subject: [PATCH 7/8] Business logic moved away from the controller --- Controllers/TodoItemsController.cs | 89 +++++++++---------------- Services/ITodoService.cs | 15 +++++ Services/TodoService.cs | 103 +++++++++++++++++++++++++++++ Startup.cs | 5 +- TodoApiDTO.csproj | 1 + 5 files changed, 155 insertions(+), 58 deletions(-) create mode 100644 Services/ITodoService.cs create mode 100644 Services/TodoService.cs diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index 044b5b87..111717b9 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -1,10 +1,9 @@ +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using System.Collections.Generic; -using System.Linq; -using System.Net.Sockets; using System.Threading.Tasks; using TodoApi.Models; +using TodoApiDTO.Services; namespace TodoApi.Controllers { @@ -13,11 +12,11 @@ namespace TodoApi.Controllers [ApiController] public class TodoItemsController : ControllerBase { - private readonly TodoContext _context; + private readonly ITodoService _service; - public TodoItemsController(TodoContext context) + public TodoItemsController(ITodoService service) { - _context = context; + _service = service; } /// @@ -25,11 +24,10 @@ public TodoItemsController(TodoContext context) /// /// [HttpGet] - public async Task>> GetTodoItems() + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List))] + public async Task GetTodoItems() { - return await _context.TodoItems - .Select(x => ItemToDTO(x)) - .ToListAsync(); + return Ok(await _service.Get()); } /// @@ -38,16 +36,18 @@ public async Task>> GetTodoItems() /// Item ID /// [HttpGet("{id}")] - public async Task> GetTodoItem(long id) + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TodoItemDTO))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); - - if (todoItem == null) + var todoItem = await _service.Get(id); + + if (todoItem is null) { return NotFound(); } - return ItemToDTO(todoItem); + return Ok(todoItem); } /// @@ -57,6 +57,9 @@ public async Task> GetTodoItem(long id) /// Modified item /// [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO) { if (id != todoItemDTO.Id) @@ -64,20 +67,9 @@ public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO return BadRequest(); } - var todoItem = await _context.TodoItems.FindAsync(id); - if (todoItem == null) - { - return NotFound(); - } - - todoItem.Name = todoItemDTO.Name; - todoItem.IsComplete = todoItemDTO.IsComplete; - - try - { - await _context.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) + var isUpdated = await _service.Update(id, todoItemDTO); + + if (!isUpdated) { return NotFound(); } @@ -91,21 +83,16 @@ public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO /// New item /// [HttpPost] - public async Task> CreateTodoItem(TodoItemDTO todoItemDTO) + [ProducesResponseType(StatusCodes.Status201Created, Type = typeof(TodoItemDTO))] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CreateTodoItem(TodoItemDTO todoItemDTO) { - var todoItem = new TodoItem - { - IsComplete = todoItemDTO.IsComplete, - Name = todoItemDTO.Name - }; - - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); - + var todoItem = await _service.Create(todoItemDTO); + return CreatedAtAction( nameof(GetTodoItem), new { id = todoItem.Id }, - ItemToDTO(todoItem)); + todoItem); } /// @@ -114,30 +101,18 @@ public async Task> CreateTodoItem(TodoItemDTO todoItem /// Item ID /// [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); - - if (todoItem == null) + var isDeleted = await _service.Delete(id); + + if (!isDeleted) { return NotFound(); } - _context.TodoItems.Remove(todoItem); - await _context.SaveChangesAsync(); - return NoContent(); } - - private bool TodoItemExists(long id) => - _context.TodoItems.Any(e => e.Id == id); - - private static TodoItemDTO ItemToDTO(TodoItem todoItem) => - new TodoItemDTO - { - Id = todoItem.Id, - Name = todoItem.Name, - IsComplete = todoItem.IsComplete - }; } } diff --git a/Services/ITodoService.cs b/Services/ITodoService.cs new file mode 100644 index 00000000..22e8baf3 --- /dev/null +++ b/Services/ITodoService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApi.Models; + +namespace TodoApiDTO.Services +{ + public interface ITodoService + { + Task Create(TodoItemDTO todoItemDTO); + Task Delete(long id); + Task> Get(); + Task Get(long id); + Task Update(long id, TodoItemDTO todoItemDTO); + } +} \ No newline at end of file diff --git a/Services/TodoService.cs b/Services/TodoService.cs new file mode 100644 index 00000000..5b26ef74 --- /dev/null +++ b/Services/TodoService.cs @@ -0,0 +1,103 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TodoApi.Models; +using TodoApiDTO.Data; + +namespace TodoApiDTO.Services +{ + public class TodoService : ITodoService + { + private readonly TodoContext _context; + + public TodoService(TodoContext context) + { + _context = context; + } + + public async Task Create(TodoItemDTO todoItemDTO) + { + var todoItem = new TodoItem + { + IsComplete = todoItemDTO.IsComplete, + Name = todoItemDTO.Name + }; + + _context.TodoItems.Add(todoItem); + await _context.SaveChangesAsync(); + + return ItemToDTO(todoItem); + } + + public async Task Delete(long id) + { + var todoItem = await _context.TodoItems.FindAsync(id); + + if (todoItem is null) + { + return false; + } + + _context.TodoItems.Remove(todoItem); + await _context.SaveChangesAsync(); + + return true; + } + + public async Task> Get() + { + return await _context.TodoItems + .Select(x => ItemToDTO(x)) + .ToListAsync(); + } + + public async Task Get(long id) + { + var todoItem = await _context.TodoItems.FindAsync(id); + + if (todoItem is null) + { + return default; + } + + return ItemToDTO(todoItem); + } + + public async Task Update(long id, TodoItemDTO todoItemDTO) + { + var todoItem = await _context.TodoItems.FindAsync(id); + + if (todoItem is null) + { + return false; + } + + todoItem.Name = todoItemDTO.Name; + todoItem.IsComplete = todoItemDTO.IsComplete; + + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) + { + return false; + } + + return true; + } + + private bool TodoItemExists(long id) => + _context.TodoItems.Any(e => e.Id == id); + + private static TodoItemDTO ItemToDTO(TodoItem todoItem) => + new() + { + Id = todoItem.Id, + Name = todoItem.Name, + IsComplete = todoItem.IsComplete + }; + } +} \ No newline at end of file diff --git a/Startup.cs b/Startup.cs index c2cde98e..2fa66a88 100644 --- a/Startup.cs +++ b/Startup.cs @@ -13,7 +13,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using TodoApi.Models; +using TodoApiDTO.Data; +using TodoApiDTO.Services; namespace TodoApi { @@ -32,6 +33,8 @@ public void ConfigureServices(IServiceCollection services) services.AddDbContext(opt => opt.UseSqlServer(Configuration.GetConnectionString("VelvetechDatabase"))); + services.AddTransient(); + services.AddControllers(); services.AddSwaggerGen(options => diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index 5605b3b4..4d0eabfd 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -2,6 +2,7 @@ net7.0 + enable True From 8c63e54c2b6ecd04bcab0a328e4f2c327fa38cea Mon Sep 17 00:00:00 2001 From: Kirill Taran Date: Tue, 27 Jun 2023 18:53:09 +0400 Subject: [PATCH 8/8] Tests added --- Data/TodoItem.cs | 12 -- TodoApiDTO.sln | 20 ++- .../Controllers}/TodoItemsController.cs | 8 +- {Data => TodoApiDTO/Data}/TodoContext.cs | 5 +- TodoApiDTO/Data/TodoItem.cs | 14 ++ .../20230621182622_InitialCreate.Designer.cs | 0 .../20230621182622_InitialCreate.cs | 0 .../Migrations}/TodoContextModelSnapshot.cs | 0 {Models => TodoApiDTO/Models}/TodoItemDTO.cs | 2 +- Program.cs => TodoApiDTO/Program.cs | 0 .../Properties}/launchSettings.json | 0 README.md => TodoApiDTO/README.md | 0 .../Services}/ITodoService.cs | 0 .../Services}/TodoService.cs | 4 +- Startup.cs => TodoApiDTO/Startup.cs | 0 .../TodoApiDTO.csproj | 0 .../appsettings.Development.json | 0 .../appsettings.json | 0 TodoApiDTOTests/TodoApiDTOTests.csproj | 32 ++++ TodoApiDTOTests/TodoServiceTests.cs | 167 ++++++++++++++++++ 20 files changed, 236 insertions(+), 28 deletions(-) delete mode 100644 Data/TodoItem.cs rename {Controllers => TodoApiDTO/Controllers}/TodoItemsController.cs (94%) rename {Data => TodoApiDTO/Data}/TodoContext.cs (70%) create mode 100644 TodoApiDTO/Data/TodoItem.cs rename {Migrations => TodoApiDTO/Migrations}/20230621182622_InitialCreate.Designer.cs (100%) rename {Migrations => TodoApiDTO/Migrations}/20230621182622_InitialCreate.cs (100%) rename {Migrations => TodoApiDTO/Migrations}/TodoContextModelSnapshot.cs (100%) rename {Models => TodoApiDTO/Models}/TodoItemDTO.cs (81%) rename Program.cs => TodoApiDTO/Program.cs (100%) rename {Properties => TodoApiDTO/Properties}/launchSettings.json (100%) rename README.md => TodoApiDTO/README.md (100%) rename {Services => TodoApiDTO/Services}/ITodoService.cs (100%) rename {Services => TodoApiDTO/Services}/TodoService.cs (97%) rename Startup.cs => TodoApiDTO/Startup.cs (100%) rename TodoApiDTO.csproj => TodoApiDTO/TodoApiDTO.csproj (100%) rename appsettings.Development.json => TodoApiDTO/appsettings.Development.json (100%) rename appsettings.json => TodoApiDTO/appsettings.json (100%) create mode 100644 TodoApiDTOTests/TodoApiDTOTests.csproj create mode 100644 TodoApiDTOTests/TodoServiceTests.cs diff --git a/Data/TodoItem.cs b/Data/TodoItem.cs deleted file mode 100644 index 1974ba16..00000000 --- a/Data/TodoItem.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TodoApiDTO.Data -{ - #region snippet - public class TodoItem - { - public long Id { get; set; } - public string Name { get; set; } - public bool IsComplete { get; set; } - public string Secret { get; set; } - } - #endregion -} \ No newline at end of file diff --git a/TodoApiDTO.sln b/TodoApiDTO.sln index e49c182b..931e90c2 100644 --- a/TodoApiDTO.sln +++ b/TodoApiDTO.sln @@ -1,9 +1,11 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30002.166 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDTO", "TodoApiDTO.csproj", "{623124F9-F5BA-42DD-BC26-A1720774229C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApiDTOTests", "TodoApiDTOTests\TodoApiDTOTests.csproj", "{B16AF459-6CB0-4F47-9040-2BBF1D7E857B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDTO", "TodoApiDTO\TodoApiDTO.csproj", "{AB010941-7483-4C99-BE32-505E505A154D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +13,14 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {623124F9-F5BA-42DD-BC26-A1720774229C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {623124F9-F5BA-42DD-BC26-A1720774229C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {623124F9-F5BA-42DD-BC26-A1720774229C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {623124F9-F5BA-42DD-BC26-A1720774229C}.Release|Any CPU.Build.0 = Release|Any CPU + {B16AF459-6CB0-4F47-9040-2BBF1D7E857B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B16AF459-6CB0-4F47-9040-2BBF1D7E857B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B16AF459-6CB0-4F47-9040-2BBF1D7E857B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B16AF459-6CB0-4F47-9040-2BBF1D7E857B}.Release|Any CPU.Build.0 = Release|Any CPU + {AB010941-7483-4C99-BE32-505E505A154D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB010941-7483-4C99-BE32-505E505A154D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB010941-7483-4C99-BE32-505E505A154D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB010941-7483-4C99-BE32-505E505A154D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Controllers/TodoItemsController.cs b/TodoApiDTO/Controllers/TodoItemsController.cs similarity index 94% rename from Controllers/TodoItemsController.cs rename to TodoApiDTO/Controllers/TodoItemsController.cs index 111717b9..88d6352e 100644 --- a/Controllers/TodoItemsController.cs +++ b/TodoApiDTO/Controllers/TodoItemsController.cs @@ -67,9 +67,9 @@ public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO return BadRequest(); } - var isUpdated = await _service.Update(id, todoItemDTO); + var wasUpdated = await _service.Update(id, todoItemDTO); - if (!isUpdated) + if (!wasUpdated) { return NotFound(); } @@ -105,9 +105,9 @@ public async Task CreateTodoItem(TodoItemDTO todoItemDTO) [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteTodoItem(long id) { - var isDeleted = await _service.Delete(id); + var wasDeleted = await _service.Delete(id); - if (!isDeleted) + if (!wasDeleted) { return NotFound(); } diff --git a/Data/TodoContext.cs b/TodoApiDTO/Data/TodoContext.cs similarity index 70% rename from Data/TodoContext.cs rename to TodoApiDTO/Data/TodoContext.cs index 9e4306eb..fb376a91 100644 --- a/Data/TodoContext.cs +++ b/TodoApiDTO/Data/TodoContext.cs @@ -4,11 +4,14 @@ namespace TodoApiDTO.Data { public class TodoContext : DbContext { + public TodoContext() { } + + public TodoContext(DbContextOptions options) : base(options) { } - public DbSet TodoItems { get; set; } + public virtual DbSet TodoItems { get; set; } } } \ No newline at end of file diff --git a/TodoApiDTO/Data/TodoItem.cs b/TodoApiDTO/Data/TodoItem.cs new file mode 100644 index 00000000..da684609 --- /dev/null +++ b/TodoApiDTO/Data/TodoItem.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace TodoApiDTO.Data +{ + #region snippet + public class TodoItem + { + public long Id { get; set; } + public string? Name { get; set; } + public bool IsComplete { get; set; } + public string? Secret { get; set; } + } + #endregion +} \ No newline at end of file diff --git a/Migrations/20230621182622_InitialCreate.Designer.cs b/TodoApiDTO/Migrations/20230621182622_InitialCreate.Designer.cs similarity index 100% rename from Migrations/20230621182622_InitialCreate.Designer.cs rename to TodoApiDTO/Migrations/20230621182622_InitialCreate.Designer.cs diff --git a/Migrations/20230621182622_InitialCreate.cs b/TodoApiDTO/Migrations/20230621182622_InitialCreate.cs similarity index 100% rename from Migrations/20230621182622_InitialCreate.cs rename to TodoApiDTO/Migrations/20230621182622_InitialCreate.cs diff --git a/Migrations/TodoContextModelSnapshot.cs b/TodoApiDTO/Migrations/TodoContextModelSnapshot.cs similarity index 100% rename from Migrations/TodoContextModelSnapshot.cs rename to TodoApiDTO/Migrations/TodoContextModelSnapshot.cs diff --git a/Models/TodoItemDTO.cs b/TodoApiDTO/Models/TodoItemDTO.cs similarity index 81% rename from Models/TodoItemDTO.cs rename to TodoApiDTO/Models/TodoItemDTO.cs index e66a500a..eb928cf7 100644 --- a/Models/TodoItemDTO.cs +++ b/TodoApiDTO/Models/TodoItemDTO.cs @@ -4,7 +4,7 @@ public class TodoItemDTO { public long Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } public bool IsComplete { get; set; } } #endregion diff --git a/Program.cs b/TodoApiDTO/Program.cs similarity index 100% rename from Program.cs rename to TodoApiDTO/Program.cs diff --git a/Properties/launchSettings.json b/TodoApiDTO/Properties/launchSettings.json similarity index 100% rename from Properties/launchSettings.json rename to TodoApiDTO/Properties/launchSettings.json diff --git a/README.md b/TodoApiDTO/README.md similarity index 100% rename from README.md rename to TodoApiDTO/README.md diff --git a/Services/ITodoService.cs b/TodoApiDTO/Services/ITodoService.cs similarity index 100% rename from Services/ITodoService.cs rename to TodoApiDTO/Services/ITodoService.cs diff --git a/Services/TodoService.cs b/TodoApiDTO/Services/TodoService.cs similarity index 97% rename from Services/TodoService.cs rename to TodoApiDTO/Services/TodoService.cs index 5b26ef74..be1c5d00 100644 --- a/Services/TodoService.cs +++ b/TodoApiDTO/Services/TodoService.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -68,7 +67,6 @@ public async Task> Get() public async Task Update(long id, TodoItemDTO todoItemDTO) { var todoItem = await _context.TodoItems.FindAsync(id); - if (todoItem is null) { return false; diff --git a/Startup.cs b/TodoApiDTO/Startup.cs similarity index 100% rename from Startup.cs rename to TodoApiDTO/Startup.cs diff --git a/TodoApiDTO.csproj b/TodoApiDTO/TodoApiDTO.csproj similarity index 100% rename from TodoApiDTO.csproj rename to TodoApiDTO/TodoApiDTO.csproj diff --git a/appsettings.Development.json b/TodoApiDTO/appsettings.Development.json similarity index 100% rename from appsettings.Development.json rename to TodoApiDTO/appsettings.Development.json diff --git a/appsettings.json b/TodoApiDTO/appsettings.json similarity index 100% rename from appsettings.json rename to TodoApiDTO/appsettings.json diff --git a/TodoApiDTOTests/TodoApiDTOTests.csproj b/TodoApiDTOTests/TodoApiDTOTests.csproj new file mode 100644 index 00000000..c2b9ed2f --- /dev/null +++ b/TodoApiDTOTests/TodoApiDTOTests.csproj @@ -0,0 +1,32 @@ + + + + net7.0 + enable + enable + + false + true + Library + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/TodoApiDTOTests/TodoServiceTests.cs b/TodoApiDTOTests/TodoServiceTests.cs new file mode 100644 index 00000000..81994f35 --- /dev/null +++ b/TodoApiDTOTests/TodoServiceTests.cs @@ -0,0 +1,167 @@ +using Microsoft.EntityFrameworkCore; +using Moq; +using Moq.EntityFrameworkCore; +using TodoApi.Models; +using TodoApiDTO.Data; +using TodoApiDTO.Services; +using Xunit; + +namespace TodoApiDTOTests +{ + public class TodoServiceTests :IDisposable + { + private readonly Mock _context; + + private static readonly List _todos = new() + { + new() + { + Id = 202 + }, + new() + { + Id = 302 + }, + new() + { + Id = 402 + } + }; + + public TodoServiceTests() + { + var contextMock = new Mock(); + contextMock.Setup(x => x.TodoItems).ReturnsDbSet(_todos); + contextMock.Setup(x => x.TodoItems.FindAsync(It.IsAny())) + .ReturnsAsync((arr) => { + var id = (long)arr![0]!; // it's safe, because we always have a long in our cases + return _todos.FirstOrDefault(x => x.Id == id); + }); + + _context = contextMock; + } + + [Theory] + [InlineData(false, null)] + [InlineData(false, "")] + [InlineData(true, "my new todo")] + public async Task Create_ShouldCreateNewItems(bool isComplete, string name) + { + var dto = new TodoItemDTO + { + IsComplete = isComplete, + Name = name + }; + var sut = new TodoService(_context.Object); + var item = await sut.Create(dto); + Assert.Equal(isComplete, item.IsComplete); + Assert.Equal(name, item.Name); + _context.Verify(x => x.TodoItems.Add(It.IsAny()), Times.Once); + _context.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Delete_ShouldNotDeleteUnexistingTodo() + { + var sut = new TodoService(_context.Object); + var wasDeleted = await sut.Delete(201); + + Assert.False(wasDeleted); + _context.Verify(x => x.TodoItems.Remove(It.IsAny()), Times.Never); + _context.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task Delete_ShouldDeleteExistingTodo() + { + var sut = new TodoService(_context.Object); + var wasDeleted = await sut.Delete(202); + + Assert.True(wasDeleted); + _context.Verify(x => x.TodoItems.Remove(It.IsAny()), Times.Once); + _context.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Get_ShouldGetAllItems() + { + var sut = new TodoService(_context.Object); + var items = await sut.Get(); + + Assert.NotEmpty(items); + Assert.Equal(_todos.Count, items.Count); + _context.Verify(x => x.TodoItems, Times.Once); + } + + [Fact] + public async Task Get_ShouldReturnDefaultWhenSpecificIdWasNotFound() + { + var sut = new TodoService(_context.Object); + var item = await sut.Get(301); + + Assert.Null(item); + _context.Verify(x => x.TodoItems, Times.Once); + } + + [Fact] + public async Task Get_ShouldGetSpecificItemById() + { + var sut = new TodoService(_context.Object); + var item = await sut.Get(302); + + Assert.NotNull(item); + Assert.Equal(302, item.Id); + _context.Verify(x => x.TodoItems, Times.Once); + } + + [Fact] + public async Task Update_ShouldReturnFalseWhenSpecificIdWasNotFound() + { + var sut = new TodoService(_context.Object); + var wasUpdated = await sut.Update(401, new TodoItemDTO()); + + Assert.False(wasUpdated); + _context.Verify(x => x.TodoItems.Update(It.IsAny()), Times.Never); + _context.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task Update_ShouldThrowDbUpdateConcurrencyExceptionOnExceptionWhenItemExists() + { + _context.Setup(x => x.SaveChangesAsync(It.IsAny())) + .ThrowsAsync(new DbUpdateConcurrencyException()); + + var sut = new TodoService(_context.Object); + + await Assert.ThrowsAsync(async () => await sut.Update(402, new TodoItemDTO())); + _context.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Update_ShouldReturnFalseOnDbUpdateConcurrencyException() + { + _context.Setup(x => x.SaveChangesAsync(It.IsAny())) + .ThrowsAsync(new DbUpdateConcurrencyException()); + _context.Setup(x => x.TodoItems.FindAsync(It.IsAny())) + .ReturnsAsync(new TodoItem { Id = 403 }); + + var sut = new TodoService(_context.Object); + var wasUpdated = await sut.Update(403, new TodoItemDTO()); + + Assert.False(wasUpdated); + _context.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Update_ShouldUpdateSpecificIdItem() + { + var sut = new TodoService(_context.Object); + var wasUpdated = await sut.Update(402, new TodoItemDTO()); + + Assert.True(wasUpdated); + _context.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Once); + } + + public void Dispose() { } + } +} \ No newline at end of file