diff --git a/Models/TodoItem.cs b/Models/TodoItem.cs deleted file mode 100644 index 1f6e5465..00000000 --- a/Models/TodoItem.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TodoApi.Models -{ - #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/README.md b/README.md deleted file mode 100644 index 466e41fd..00000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# TodoService \ No newline at end of file diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj deleted file mode 100644 index bba6f6af..00000000 --- a/TodoApiDTO.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - netcoreapp3.1 - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - 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/TodoApiDTO/Controllers/TodoItemsController.cs b/TodoApiDTO/Controllers/TodoItemsController.cs new file mode 100644 index 00000000..88d6352e --- /dev/null +++ b/TodoApiDTO/Controllers/TodoItemsController.cs @@ -0,0 +1,118 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApi.Models; +using TodoApiDTO.Services; + +namespace TodoApi.Controllers +{ + [Route("api/[controller]")] + [Produces("application/json")] + [ApiController] + public class TodoItemsController : ControllerBase + { + private readonly ITodoService _service; + + public TodoItemsController(ITodoService service) + { + _service = service; + } + + /// + /// Gets ToDoItems. + /// + /// + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List))] + public async Task GetTodoItems() + { + return Ok(await _service.Get()); + } + + /// + /// Gets a specific ToDoItem. + /// + /// Item ID + /// + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TodoItemDTO))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetTodoItem(long id) + { + var todoItem = await _service.Get(id); + + if (todoItem is null) + { + return NotFound(); + } + + return Ok(todoItem); + } + + /// + /// Updates a specific ToDoItem. + /// + /// Item 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) + { + return BadRequest(); + } + + var wasUpdated = await _service.Update(id, todoItemDTO); + + if (!wasUpdated) + { + return NotFound(); + } + + return NoContent(); + } + + /// + /// Creates a new ToDoItem. + /// + /// New item + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created, Type = typeof(TodoItemDTO))] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CreateTodoItem(TodoItemDTO todoItemDTO) + { + var todoItem = await _service.Create(todoItemDTO); + + return CreatedAtAction( + nameof(GetTodoItem), + new { id = todoItem.Id }, + todoItem); + } + + /// + /// Deletes a spefific ToDoItem. + /// + /// Item ID + /// + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteTodoItem(long id) + { + var wasDeleted = await _service.Delete(id); + + if (!wasDeleted) + { + return NotFound(); + } + + return NoContent(); + } + } +} diff --git a/Models/TodoContext.cs b/TodoApiDTO/Data/TodoContext.cs similarity index 63% rename from Models/TodoContext.cs rename to TodoApiDTO/Data/TodoContext.cs index 6e59e363..fb376a91 100644 --- a/Models/TodoContext.cs +++ b/TodoApiDTO/Data/TodoContext.cs @@ -1,14 +1,17 @@ using Microsoft.EntityFrameworkCore; -namespace TodoApi.Models +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/TodoApiDTO/Migrations/20230621182622_InitialCreate.Designer.cs b/TodoApiDTO/Migrations/20230621182622_InitialCreate.Designer.cs new file mode 100644 index 00000000..0478f458 --- /dev/null +++ b/TodoApiDTO/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/TodoApiDTO/Migrations/20230621182622_InitialCreate.cs b/TodoApiDTO/Migrations/20230621182622_InitialCreate.cs new file mode 100644 index 00000000..7d1c20ca --- /dev/null +++ b/TodoApiDTO/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/TodoApiDTO/Migrations/TodoContextModelSnapshot.cs b/TodoApiDTO/Migrations/TodoContextModelSnapshot.cs new file mode 100644 index 00000000..bd70a378 --- /dev/null +++ b/TodoApiDTO/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/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 95% rename from Properties/launchSettings.json rename to TodoApiDTO/Properties/launchSettings.json index 6766196a..b9bf0083 100644 --- a/Properties/launchSettings.json +++ b/TodoApiDTO/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/TodoApiDTO/README.md b/TodoApiDTO/README.md new file mode 100644 index 00000000..551b2102 --- /dev/null +++ b/TodoApiDTO/README.md @@ -0,0 +1,6 @@ +# TodoService + +1. Äîáàâèòü ñâàããåð +2. Õðàíèòü ñïèñîê çàäà÷ â SQL Server (ñåé÷àñ èñïîëüçóåòñÿ InMemoryDB). Ñòðîêà ñîåäèíåíèÿ äîëæíà õðàíèòüñÿ â ôàéëå appsettings. +3. Ñäåëàòü ðåôàêòîðèíã - âûäåëèòü ñëîè Data Access Layer, Business Layer +4. Íàïèñàòü þíèò-òåñòû íà áèçíåñ îïåðàöèè diff --git a/TodoApiDTO/Services/ITodoService.cs b/TodoApiDTO/Services/ITodoService.cs new file mode 100644 index 00000000..22e8baf3 --- /dev/null +++ b/TodoApiDTO/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/Controllers/TodoItemsController.cs b/TodoApiDTO/Services/TodoService.cs similarity index 53% rename from Controllers/TodoItemsController.cs rename to TodoApiDTO/Services/TodoService.cs index 0ef138e7..be1c5d00 100644 --- a/Controllers/TodoItemsController.cs +++ b/TodoApiDTO/Services/TodoService.cs @@ -1,56 +1,75 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using TodoApi.Models; +using TodoApiDTO.Data; -namespace TodoApi.Controllers +namespace TodoApiDTO.Services { - [Route("api/[controller]")] - [ApiController] - public class TodoItemsController : ControllerBase + public class TodoService : ITodoService { private readonly TodoContext _context; - public TodoItemsController(TodoContext context) + public TodoService(TodoContext context) { _context = context; } - [HttpGet] - public async Task>> GetTodoItems() + 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(); } - [HttpGet("{id}")] - public async Task> GetTodoItem(long id) + public async Task Get(long id) { var todoItem = await _context.TodoItems.FindAsync(id); - if (todoItem == null) + if (todoItem is null) { - return NotFound(); + return default; } return ItemToDTO(todoItem); } - [HttpPut("{id}")] - public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO) + public async Task Update(long id, TodoItemDTO todoItemDTO) { - if (id != todoItemDTO.Id) - { - return BadRequest(); - } - var todoItem = await _context.TodoItems.FindAsync(id); - if (todoItem == null) + if (todoItem is null) { - return NotFound(); + return false; } todoItem.Name = todoItemDTO.Name; @@ -62,55 +81,21 @@ public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO } catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) { - return NotFound(); + return false; } - return NoContent(); - } - - [HttpPost] - public async Task> CreateTodoItem(TodoItemDTO todoItemDTO) - { - var todoItem = new TodoItem - { - IsComplete = todoItemDTO.IsComplete, - Name = todoItemDTO.Name - }; - - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); - - return CreatedAtAction( - nameof(GetTodoItem), - new { id = todoItem.Id }, - ItemToDTO(todoItem)); - } - - [HttpDelete("{id}")] - public async Task DeleteTodoItem(long id) - { - var todoItem = await _context.TodoItems.FindAsync(id); - - if (todoItem == null) - { - return NotFound(); - } - - _context.TodoItems.Remove(todoItem); - await _context.SaveChangesAsync(); - - return NoContent(); + return true; } private bool TodoItemExists(long id) => - _context.TodoItems.Any(e => e.Id == id); + _context.TodoItems.Any(e => e.Id == id); private static TodoItemDTO ItemToDTO(TodoItem todoItem) => - new TodoItemDTO + new() { Id = todoItem.Id, Name = todoItem.Name, IsComplete = todoItem.IsComplete - }; + }; } -} +} \ No newline at end of file diff --git a/Startup.cs b/TodoApiDTO/Startup.cs similarity index 71% rename from Startup.cs rename to TodoApiDTO/Startup.cs index bbfbc83d..2fa66a88 100644 --- a/Startup.cs +++ b/TodoApiDTO/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; @@ -11,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 { @@ -28,8 +31,17 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { services.AddDbContext(opt => - opt.UseInMemoryDatabase("TodoList")); + opt.UseSqlServer(Configuration.GetConnectionString("VelvetechDatabase"))); + + services.AddTransient(); + services.AddControllers(); + + 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. @@ -40,6 +52,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseDeveloperExceptionPage(); } + app.UseSwagger(); + app.UseSwaggerUI(); + app.UseHttpsRedirection(); app.UseRouting(); diff --git a/TodoApiDTO/TodoApiDTO.csproj b/TodoApiDTO/TodoApiDTO.csproj new file mode 100644 index 00000000..4d0eabfd --- /dev/null +++ b/TodoApiDTO/TodoApiDTO.csproj @@ -0,0 +1,33 @@ + + + + net7.0 + enable + True + + + + 1701;1702;CS1591; + + + + 1701;1702;CS1591; + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + 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 52% rename from appsettings.json rename to TodoApiDTO/appsettings.json index d9d9a9bf..222d0e99 100644 --- a/appsettings.json +++ b/TodoApiDTO/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", 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