diff --git a/AdvancedTodoList.Core/Dtos/TodoListDtos.cs b/AdvancedTodoList.Core/Dtos/TodoListDtos.cs
index a5f3adc..e30e653 100644
--- a/AdvancedTodoList.Core/Dtos/TodoListDtos.cs
+++ b/AdvancedTodoList.Core/Dtos/TodoListDtos.cs
@@ -1,7 +1,11 @@
namespace AdvancedTodoList.Core.Dtos;
-public record TodoListItemView(int Id);
+///
+/// DTO for creating/editing a to-do list.
+///
+public record TodoListCreateDto(string Name, string Description);
-public record TodoListCreateDto(string Name);
-
-public record TodoListGetByIdDto(string Id, string Name, TodoListItemView[] TodoItems);
+///
+/// DTO for a full view of a to-do list.
+///
+public record TodoListGetByIdDto(string Id, string Name, string Description);
diff --git a/AdvancedTodoList.Core/Dtos/TodoListItemDtos.cs b/AdvancedTodoList.Core/Dtos/TodoListItemDtos.cs
new file mode 100644
index 0000000..e44c370
--- /dev/null
+++ b/AdvancedTodoList.Core/Dtos/TodoListItemDtos.cs
@@ -0,0 +1,30 @@
+using AdvancedTodoList.Core.Models.TodoLists;
+
+namespace AdvancedTodoList.Core.Dtos;
+
+///
+/// DTO for creating/editing a to-do list item.
+///
+public record TodoItemCreateDto(string Name, string Description, DateTime? DeadlineDate);
+
+///
+/// DTO for changing the state of a to-do list item.
+///
+public record TodoItemUpdateStateDto(TodoItemState State);
+
+///
+/// DTO for a full view of a to-do list item.
+///
+public record TodoItemGetByIdDto(
+ int Id, string TodoListId, string Name,
+ string Description, DateTime? DeadlineDate,
+ TodoItemState State
+ );
+
+///
+/// DTO for a partial view of a to-do list item.
+///
+public record TodoItemPreviewDto(
+ int Id, string TodoListId, string Name,
+ DateTime? DeadlineDate, TodoItemState State
+ );
\ No newline at end of file
diff --git a/AdvancedTodoList.Core/Mapping/MappingGlobalSettings.cs b/AdvancedTodoList.Core/Mapping/MappingGlobalSettings.cs
new file mode 100644
index 0000000..98e2378
--- /dev/null
+++ b/AdvancedTodoList.Core/Mapping/MappingGlobalSettings.cs
@@ -0,0 +1,19 @@
+using Mapster;
+
+namespace AdvancedTodoList.Core.Mapping;
+
+///
+/// Class that defines global mapping settings.
+///
+public static class MappingGlobalSettings
+{
+ ///
+ /// Apply global mapping settings.
+ ///
+ public static void Apply()
+ {
+ // Convert null strings into empty strings and trim strings
+ TypeAdapterConfig.GlobalSettings.Default
+ .AddDestinationTransform((string? dest) => dest != null ? dest.Trim() : string.Empty);
+ }
+}
diff --git a/AdvancedTodoList.Core/Models/ApplicationUser.cs b/AdvancedTodoList.Core/Models/ApplicationUser.cs
index 0cde393..81bfee3 100644
--- a/AdvancedTodoList.Core/Models/ApplicationUser.cs
+++ b/AdvancedTodoList.Core/Models/ApplicationUser.cs
@@ -2,6 +2,6 @@
namespace AdvancedTodoList.Core.Models;
-public class ApplicationUser : IdentityUser
+public class ApplicationUser : IdentityUser, IEntity
{
}
diff --git a/AdvancedTodoList.Core/Models/IEntity.cs b/AdvancedTodoList.Core/Models/IEntity.cs
new file mode 100644
index 0000000..7a31f73
--- /dev/null
+++ b/AdvancedTodoList.Core/Models/IEntity.cs
@@ -0,0 +1,10 @@
+namespace AdvancedTodoList.Core.Models;
+
+///
+/// An interface that represents an entity with an ID property.
+///
+/// Type of the entity ID.
+public interface IEntity where TId : IEquatable
+{
+ TId Id { get; }
+}
diff --git a/AdvancedTodoList.Core/Models/TodoLists/TodoItem.cs b/AdvancedTodoList.Core/Models/TodoLists/TodoItem.cs
index fc95179..537c82f 100644
--- a/AdvancedTodoList.Core/Models/TodoLists/TodoItem.cs
+++ b/AdvancedTodoList.Core/Models/TodoLists/TodoItem.cs
@@ -6,27 +6,27 @@ namespace AdvancedTodoList.Core.Models.TodoLists;
///
/// Represents a to-do list item entity.
///
-public class TodoItem
+public class TodoItem : IEntity
{
- ///
- /// An unique identifier for the to-do list item.
- ///
- [Key]
- public int Id { get; set; }
- ///
- /// Name (title) of the to-do item.
- ///
- [MaxLength(NameMaxLength)]
- public string Name { get; set; } = null!;
- ///
- /// Description of the to-do item.
- ///
- [MaxLength(DescriptionMaxLength)]
- public string Description { get; set; } = null!;
- ///
- /// Current state of the to-do item.
- ///
- public TodoItemState State { get; set; }
+ ///
+ /// An unique identifier for the to-do list item.
+ ///
+ [Key]
+ public int Id { get; set; }
+ ///
+ /// Name (title) of the to-do item.
+ ///
+ [MaxLength(NameMaxLength)]
+ public string Name { get; set; } = null!;
+ ///
+ /// Description of the to-do item.
+ ///
+ [MaxLength(DescriptionMaxLength)]
+ public string Description { get; set; } = null!;
+ ///
+ /// Current state of the to-do item.
+ ///
+ public TodoItemState State { get; set; }
///
/// Deadline date for the todo item. Can be null.
///
@@ -42,15 +42,15 @@ public class TodoItem
/// Maximum allowed length of .
///
public const int NameMaxLength = 100;
- ///
- /// Maximum allowed length of .
- ///
- public const int DescriptionMaxLength = 10_000;
+ ///
+ /// Maximum allowed length of .
+ ///
+ public const int DescriptionMaxLength = 10_000;
- ///
- /// To-do list associated with this to-do item.
- ///
- public TodoList TodoList { get; set; } = null!;
+ ///
+ /// To-do list associated with this to-do item.
+ ///
+ public TodoList TodoList { get; set; } = null!;
}
///
diff --git a/AdvancedTodoList.Core/Models/TodoLists/TodoList.cs b/AdvancedTodoList.Core/Models/TodoLists/TodoList.cs
index 596625d..9aa22bd 100644
--- a/AdvancedTodoList.Core/Models/TodoLists/TodoList.cs
+++ b/AdvancedTodoList.Core/Models/TodoLists/TodoList.cs
@@ -6,20 +6,20 @@ namespace AdvancedTodoList.Core.Models.TodoLists;
///
/// Represents a to-do list entity.
///
-public class TodoList
+public class TodoList : IEntity
{
- ///
- /// An unique identifier for the to-do list.
- ///
- [Key]
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public string Id { get; set; } = null!;
+ ///
+ /// An unique identifier for the to-do list.
+ ///
+ [Key]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public string Id { get; set; } = null!;
- ///
- /// Name (title) of the to-do list.
- ///
- [MaxLength(NameMaxLength)]
- public string Name { get; set; } = null!;
+ ///
+ /// Name (title) of the to-do list.
+ ///
+ [MaxLength(NameMaxLength)]
+ public string Name { get; set; } = null!;
///
/// Description of the to-do list.
///
diff --git a/AdvancedTodoList.Core/Services/IEntityExistenceChecker.cs b/AdvancedTodoList.Core/Services/IEntityExistenceChecker.cs
new file mode 100644
index 0000000..1893c06
--- /dev/null
+++ b/AdvancedTodoList.Core/Services/IEntityExistenceChecker.cs
@@ -0,0 +1,25 @@
+using AdvancedTodoList.Core.Models;
+
+namespace AdvancedTodoList.Core.Services;
+
+///
+/// An interface for the service that checks whether an entity with an ID exists.
+///
+public interface IEntityExistenceChecker
+{
+ ///
+ /// Asynchronously checks whether an entity of type with an ID
+ /// of type exists.
+ ///
+ /// Type of the entity.
+ /// Type which ID of the entity has.
+ /// ID of the entity which existence is checked.
+ ///
+ /// A task representing the asynchronous operation. The task result contains
+ /// if entity with the given ID exists; otherwise
+ /// .
+ ///
+ Task ExistsAsync(TId id)
+ where TEntity : class, IEntity
+ where TId : IEquatable;
+}
diff --git a/AdvancedTodoList.Core/Services/ITodoItemsService.cs b/AdvancedTodoList.Core/Services/ITodoItemsService.cs
new file mode 100644
index 0000000..8aa275b
--- /dev/null
+++ b/AdvancedTodoList.Core/Services/ITodoItemsService.cs
@@ -0,0 +1,91 @@
+using AdvancedTodoList.Core.Dtos;
+using AdvancedTodoList.Core.Models.TodoLists;
+
+namespace AdvancedTodoList.Core.Services;
+
+///
+/// Interface for a service that manages to-do list items.
+///
+public interface ITodoItemsService
+{
+ ///
+ /// Retrieves to-do list items of the list with the specified ID.
+ ///
+ ///
+ /// Does not throw exceptions if ID is invalid.
+ ///
+ /// The ID of the to-do list which items will be retrieved.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains a collection of objects.
+ ///
+ public Task> GetItemsOfListAsync(string id);
+
+ ///
+ /// Retrieves a to-do list ID of the to-do list item.
+ ///
+ /// ID of the to-do list item.
+ ///
+ /// A task representing the asynchronous operation. The task result contains
+ /// an ID of the to-do list which owns a to-do list item with the specified ID if it's found;
+ /// otherwise, returns .
+ ///
+ public Task GetTodoListByIdAsync(int id);
+
+ ///
+ /// Retrieves a to-do list item by its ID asynchronously.
+ ///
+ /// The ID of the to-do list item to retrieve.
+ ///
+ /// A task representing the asynchronous operation. The task result contains
+ /// a object if the specified ID is found;
+ /// otherwise, returns .
+ ///
+ public Task GetByIdAsync(int id);
+
+ ///
+ /// Creates a new to-do list item asynchronously.
+ ///
+ /// The ID of the to-do list to associate the item with.
+ /// The DTO containing information for creating the to-do list item.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains the created .
+ ///
+ public Task CreateAsync(string todoListId, TodoItemCreateDto dto);
+
+ ///
+ /// Edits a to-do list item asynchronously.
+ ///
+ /// The ID of the to-do list item to edit.
+ /// The DTO containing information for editing the to-do list item.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains on success;
+ /// otherwise if the entity was not found.
+ ///
+ public Task EditAsync(int id, TodoItemCreateDto dto);
+
+ ///
+ /// Updates the state of a to-do list item asynchronously.
+ ///
+ /// The ID of the to-do list item to update the state.
+ /// The DTO containing information for updating the state of the to-do list item.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains on success;
+ /// otherwise if the entity was not found.
+ ///
+ public Task UpdateStateAsync(int id, TodoItemUpdateStateDto dto);
+
+ ///
+ /// Deletes a to-do list item asynchronously.
+ ///
+ /// The ID of the to-do list item to delete.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains on success;
+ /// otherwise if the entity was not found.
+ ///
+ public Task DeleteAsync(int id);
+}
diff --git a/AdvancedTodoList.Core/Services/ITodoListsService.cs b/AdvancedTodoList.Core/Services/ITodoListsService.cs
index 6dd3516..2a13f0d 100644
--- a/AdvancedTodoList.Core/Services/ITodoListsService.cs
+++ b/AdvancedTodoList.Core/Services/ITodoListsService.cs
@@ -3,12 +3,15 @@
namespace AdvancedTodoList.Core.Services;
+///
+/// Interface for a service that manages to-do lists.
+///
public interface ITodoListsService
{
///
- /// Retrieves a to-do list by its unique identifier asynchronously.
+ /// Retrieves a to-do list by its ID asynchronously.
///
- /// The unique identifier of the to-do list to retrieve.
+ /// The ID of the to-do list to retrieve.
///
/// A task representing the asynchronous operation. The task result contains
/// a object if the specified ID is found;
@@ -19,10 +22,33 @@ public interface ITodoListsService
///
/// Creates a new to-do list asynchronously.
///
- /// The data transfer object containing information for creating the todo list.
+ /// The DTO containing information for creating the to-do list.
///
/// A task representing the asynchronous operation.
/// The task result contains the created .
///
public Task CreateAsync(TodoListCreateDto dto);
+
+ ///
+ /// Edits a to-do list asynchronously.
+ ///
+ /// The ID of the to-do list to edit.
+ /// The DTO containing information for editing the to-do list.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains on success;
+ /// otherwise if entity was not found.
+ ///
+ public Task EditAsync(string id, TodoListCreateDto dto);
+
+ ///
+ /// Deletes a to-do list asynchronously.
+ ///
+ /// The ID of the to-do list to edit.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains on success;
+ /// otherwise if entity was not found.
+ ///
+ public Task DeleteAsync(string id);
}
diff --git a/AdvancedTodoList.Core/Validation/TodoItemCreateDtoValidator.cs b/AdvancedTodoList.Core/Validation/TodoItemCreateDtoValidator.cs
new file mode 100644
index 0000000..fb1f535
--- /dev/null
+++ b/AdvancedTodoList.Core/Validation/TodoItemCreateDtoValidator.cs
@@ -0,0 +1,28 @@
+using AdvancedTodoList.Core.Dtos;
+using FluentValidation;
+
+namespace AdvancedTodoList.Core.Validation;
+
+///
+/// Validator class for
+///
+public class TodoItemCreateDtoValidator : AbstractValidator
+{
+ public TodoItemCreateDtoValidator()
+ {
+ // Name is required
+ RuleFor(x => x.Name)
+ .NotEmpty()
+ .WithErrorCode(ValidationErrorCodes.PropertyRequired);
+
+ // Description is not null
+ RuleFor(x => x.Description)
+ .NotNull()
+ .WithErrorCode(ValidationErrorCodes.PropertyRequired);
+
+ // Deadline date should be after the current date
+ RuleFor(x => x.DeadlineDate)
+ .GreaterThan(DateTime.UtcNow)
+ .WithErrorCode(ValidationErrorCodes.PropertyOutOfRange);
+ }
+}
diff --git a/AdvancedTodoList.Core/Validation/TodoItemUpdateStateDtoValidator.cs b/AdvancedTodoList.Core/Validation/TodoItemUpdateStateDtoValidator.cs
new file mode 100644
index 0000000..a97368a
--- /dev/null
+++ b/AdvancedTodoList.Core/Validation/TodoItemUpdateStateDtoValidator.cs
@@ -0,0 +1,16 @@
+using AdvancedTodoList.Core.Dtos;
+using AdvancedTodoList.Core.Models.TodoLists;
+using FluentValidation;
+
+namespace AdvancedTodoList.Core.Validation;
+
+public class TodoItemUpdateStateDtoValidator : AbstractValidator
+{
+ public TodoItemUpdateStateDtoValidator()
+ {
+ RuleFor(x => x.State)
+ .Must(s => s >= TodoItemState.Active && s <= TodoItemState.Skipped)
+ .WithErrorCode(ValidationErrorCodes.PropertyOutOfRange)
+ .WithMessage(x => $"{(int)x.State} is invalid value for {{PropertyName}}");
+ }
+}
diff --git a/AdvancedTodoList.Core/Validation/TodoListCreateDtoValidator.cs b/AdvancedTodoList.Core/Validation/TodoListCreateDtoValidator.cs
new file mode 100644
index 0000000..fee5486
--- /dev/null
+++ b/AdvancedTodoList.Core/Validation/TodoListCreateDtoValidator.cs
@@ -0,0 +1,20 @@
+using AdvancedTodoList.Core.Dtos;
+using FluentValidation;
+
+namespace AdvancedTodoList.Core.Validation;
+
+public class TodoListCreateDtoValidator : AbstractValidator
+{
+ public TodoListCreateDtoValidator()
+ {
+ // Name is required
+ RuleFor(x => x.Name)
+ .NotEmpty()
+ .WithErrorCode(ValidationErrorCodes.PropertyRequired);
+
+ // Description is not null
+ RuleFor(x => x.Description)
+ .NotNull()
+ .WithErrorCode(ValidationErrorCodes.PropertyRequired);
+ }
+}
diff --git a/AdvancedTodoList.Core/Validation/ValidationErrorCodes.cs b/AdvancedTodoList.Core/Validation/ValidationErrorCodes.cs
new file mode 100644
index 0000000..8a37569
--- /dev/null
+++ b/AdvancedTodoList.Core/Validation/ValidationErrorCodes.cs
@@ -0,0 +1,24 @@
+namespace AdvancedTodoList.Core.Validation;
+
+///
+/// Class that contains validation error codes.
+///
+public static class ValidationErrorCodes
+{
+ ///
+ /// Required property is null or empty.
+ ///
+ public const string PropertyRequired = "100";
+ ///
+ /// Length of the property exceeds maximum possible value.
+ ///
+ public const string PropertyTooLong = "200";
+ ///
+ /// Value of the property is out of range.
+ ///
+ public const string PropertyOutOfRange = "300";
+ ///
+ /// Property is invalid foreign key.
+ ///
+ public const string InvalidForeignKey = "400";
+}
diff --git a/AdvancedTodoList.Infrastructure/Migrations/20240206171516_InitialSetup.cs b/AdvancedTodoList.Infrastructure/Migrations/20240206171516_InitialSetup.cs
index 4ec6ea2..b25b781 100644
--- a/AdvancedTodoList.Infrastructure/Migrations/20240206171516_InitialSetup.cs
+++ b/AdvancedTodoList.Infrastructure/Migrations/20240206171516_InitialSetup.cs
@@ -1,267 +1,265 @@
-using System;
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
-namespace AdvancedTodoList.Infrastructure.Migrations
+namespace AdvancedTodoList.Infrastructure.Migrations;
+
+///
+public partial class InitialSetup : Migration
{
- ///
- public partial class InitialSetup : Migration
- {
- ///
- protected override void Up(MigrationBuilder migrationBuilder)
- {
- migrationBuilder.CreateTable(
- name: "AspNetRoles",
- columns: table => new
- {
- Id = table.Column(type: "nvarchar(450)", nullable: false),
- Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
- NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
- ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_AspNetRoles", x => x.Id);
- });
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "AspNetRoles",
+ columns: table => new
+ {
+ Id = table.Column(type: "nvarchar(450)", nullable: false),
+ Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetRoles", x => x.Id);
+ });
- migrationBuilder.CreateTable(
- name: "AspNetUsers",
- columns: table => new
- {
- Id = table.Column(type: "nvarchar(450)", nullable: false),
- UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
- NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
- Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
- NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
- EmailConfirmed = table.Column(type: "bit", nullable: false),
- PasswordHash = table.Column(type: "nvarchar(max)", nullable: true),
- SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true),
- ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true),
- PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true),
- PhoneNumberConfirmed = table.Column(type: "bit", nullable: false),
- TwoFactorEnabled = table.Column(type: "bit", nullable: false),
- LockoutEnd = table.Column(type: "datetimeoffset", nullable: true),
- LockoutEnabled = table.Column(type: "bit", nullable: false),
- AccessFailedCount = table.Column(type: "int", nullable: false)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_AspNetUsers", x => x.Id);
- });
+ migrationBuilder.CreateTable(
+ name: "AspNetUsers",
+ columns: table => new
+ {
+ Id = table.Column(type: "nvarchar(450)", nullable: false),
+ UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ EmailConfirmed = table.Column(type: "bit", nullable: false),
+ PasswordHash = table.Column(type: "nvarchar(max)", nullable: true),
+ SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true),
+ ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true),
+ PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true),
+ PhoneNumberConfirmed = table.Column(type: "bit", nullable: false),
+ TwoFactorEnabled = table.Column(type: "bit", nullable: false),
+ LockoutEnd = table.Column(type: "datetimeoffset", nullable: true),
+ LockoutEnabled = table.Column(type: "bit", nullable: false),
+ AccessFailedCount = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUsers", x => x.Id);
+ });
- migrationBuilder.CreateTable(
- name: "TodoLists",
- columns: table => new
- {
- Id = table.Column(type: "nvarchar(450)", nullable: false),
- Name = table.Column(type: "nvarchar(max)", nullable: false)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_TodoLists", x => x.Id);
- });
+ migrationBuilder.CreateTable(
+ name: "TodoLists",
+ columns: table => new
+ {
+ Id = table.Column(type: "nvarchar(450)", nullable: false),
+ Name = table.Column(type: "nvarchar(max)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_TodoLists", x => x.Id);
+ });
- migrationBuilder.CreateTable(
- name: "AspNetRoleClaims",
- columns: table => new
- {
- Id = table.Column(type: "int", nullable: false)
- .Annotation("SqlServer:Identity", "1, 1"),
- RoleId = table.Column(type: "nvarchar(450)", nullable: false),
- ClaimType = table.Column(type: "nvarchar(max)", nullable: true),
- ClaimValue = table.Column(type: "nvarchar(max)", nullable: true)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
- table.ForeignKey(
- name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
- column: x => x.RoleId,
- principalTable: "AspNetRoles",
- principalColumn: "Id",
- onDelete: ReferentialAction.Cascade);
- });
+ migrationBuilder.CreateTable(
+ name: "AspNetRoleClaims",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ RoleId = table.Column(type: "nvarchar(450)", nullable: false),
+ ClaimType = table.Column(type: "nvarchar(max)", nullable: true),
+ ClaimValue = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
+ column: x => x.RoleId,
+ principalTable: "AspNetRoles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
- migrationBuilder.CreateTable(
- name: "AspNetUserClaims",
- columns: table => new
- {
- Id = table.Column(type: "int", nullable: false)
- .Annotation("SqlServer:Identity", "1, 1"),
- UserId = table.Column(type: "nvarchar(450)", nullable: false),
- ClaimType = table.Column(type: "nvarchar(max)", nullable: true),
- ClaimValue = table.Column(type: "nvarchar(max)", nullable: true)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
- table.ForeignKey(
- name: "FK_AspNetUserClaims_AspNetUsers_UserId",
- column: x => x.UserId,
- principalTable: "AspNetUsers",
- principalColumn: "Id",
- onDelete: ReferentialAction.Cascade);
- });
+ migrationBuilder.CreateTable(
+ name: "AspNetUserClaims",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ UserId = table.Column(type: "nvarchar(450)", nullable: false),
+ ClaimType = table.Column(type: "nvarchar(max)", nullable: true),
+ ClaimValue = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AspNetUserClaims_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
- migrationBuilder.CreateTable(
- name: "AspNetUserLogins",
- columns: table => new
- {
- LoginProvider = table.Column(type: "nvarchar(450)", nullable: false),
- ProviderKey = table.Column(type: "nvarchar(450)", nullable: false),
- ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true),
- UserId = table.Column(type: "nvarchar(450)", nullable: false)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
- table.ForeignKey(
- name: "FK_AspNetUserLogins_AspNetUsers_UserId",
- column: x => x.UserId,
- principalTable: "AspNetUsers",
- principalColumn: "Id",
- onDelete: ReferentialAction.Cascade);
- });
+ migrationBuilder.CreateTable(
+ name: "AspNetUserLogins",
+ columns: table => new
+ {
+ LoginProvider = table.Column(type: "nvarchar(450)", nullable: false),
+ ProviderKey = table.Column(type: "nvarchar(450)", nullable: false),
+ ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true),
+ UserId = table.Column(type: "nvarchar(450)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
+ table.ForeignKey(
+ name: "FK_AspNetUserLogins_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
- migrationBuilder.CreateTable(
- name: "AspNetUserRoles",
- columns: table => new
- {
- UserId = table.Column(type: "nvarchar(450)", nullable: false),
- RoleId = table.Column(type: "nvarchar(450)", nullable: false)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
- table.ForeignKey(
- name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
- column: x => x.RoleId,
- principalTable: "AspNetRoles",
- principalColumn: "Id",
- onDelete: ReferentialAction.Cascade);
- table.ForeignKey(
- name: "FK_AspNetUserRoles_AspNetUsers_UserId",
- column: x => x.UserId,
- principalTable: "AspNetUsers",
- principalColumn: "Id",
- onDelete: ReferentialAction.Cascade);
- });
+ migrationBuilder.CreateTable(
+ name: "AspNetUserRoles",
+ columns: table => new
+ {
+ UserId = table.Column(type: "nvarchar(450)", nullable: false),
+ RoleId = table.Column(type: "nvarchar(450)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
+ table.ForeignKey(
+ name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
+ column: x => x.RoleId,
+ principalTable: "AspNetRoles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_AspNetUserRoles_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
- migrationBuilder.CreateTable(
- name: "AspNetUserTokens",
- columns: table => new
- {
- UserId = table.Column(type: "nvarchar(450)", nullable: false),
- LoginProvider = table.Column(type: "nvarchar(450)", nullable: false),
- Name = table.Column(type: "nvarchar(450)", nullable: false),
- Value = table.Column(type: "nvarchar(max)", nullable: true)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
- table.ForeignKey(
- name: "FK_AspNetUserTokens_AspNetUsers_UserId",
- column: x => x.UserId,
- principalTable: "AspNetUsers",
- principalColumn: "Id",
- onDelete: ReferentialAction.Cascade);
- });
+ migrationBuilder.CreateTable(
+ name: "AspNetUserTokens",
+ columns: table => new
+ {
+ UserId = table.Column(type: "nvarchar(450)", nullable: false),
+ LoginProvider = table.Column(type: "nvarchar(450)", nullable: false),
+ Name = table.Column(type: "nvarchar(450)", nullable: false),
+ Value = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
+ table.ForeignKey(
+ name: "FK_AspNetUserTokens_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
- migrationBuilder.CreateTable(
- name: "TodoItems",
- columns: table => new
- {
- Id = table.Column(type: "int", nullable: false)
- .Annotation("SqlServer:Identity", "1, 1"),
- Name = table.Column(type: "nvarchar(max)", nullable: false),
- TodoListId = table.Column(type: "nvarchar(450)", nullable: false)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_TodoItems", x => x.Id);
- table.ForeignKey(
- name: "FK_TodoItems_TodoLists_TodoListId",
- column: x => x.TodoListId,
- principalTable: "TodoLists",
- principalColumn: "Id",
- onDelete: ReferentialAction.Cascade);
- });
+ migrationBuilder.CreateTable(
+ name: "TodoItems",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Name = table.Column(type: "nvarchar(max)", nullable: false),
+ TodoListId = table.Column(type: "nvarchar(450)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_TodoItems", x => x.Id);
+ table.ForeignKey(
+ name: "FK_TodoItems_TodoLists_TodoListId",
+ column: x => x.TodoListId,
+ principalTable: "TodoLists",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
- migrationBuilder.CreateIndex(
- name: "IX_AspNetRoleClaims_RoleId",
- table: "AspNetRoleClaims",
- column: "RoleId");
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetRoleClaims_RoleId",
+ table: "AspNetRoleClaims",
+ column: "RoleId");
- migrationBuilder.CreateIndex(
- name: "RoleNameIndex",
- table: "AspNetRoles",
- column: "NormalizedName",
- unique: true,
- filter: "[NormalizedName] IS NOT NULL");
+ migrationBuilder.CreateIndex(
+ name: "RoleNameIndex",
+ table: "AspNetRoles",
+ column: "NormalizedName",
+ unique: true,
+ filter: "[NormalizedName] IS NOT NULL");
- migrationBuilder.CreateIndex(
- name: "IX_AspNetUserClaims_UserId",
- table: "AspNetUserClaims",
- column: "UserId");
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserClaims_UserId",
+ table: "AspNetUserClaims",
+ column: "UserId");
- migrationBuilder.CreateIndex(
- name: "IX_AspNetUserLogins_UserId",
- table: "AspNetUserLogins",
- column: "UserId");
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserLogins_UserId",
+ table: "AspNetUserLogins",
+ column: "UserId");
- migrationBuilder.CreateIndex(
- name: "IX_AspNetUserRoles_RoleId",
- table: "AspNetUserRoles",
- column: "RoleId");
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserRoles_RoleId",
+ table: "AspNetUserRoles",
+ column: "RoleId");
- migrationBuilder.CreateIndex(
- name: "EmailIndex",
- table: "AspNetUsers",
- column: "NormalizedEmail");
+ migrationBuilder.CreateIndex(
+ name: "EmailIndex",
+ table: "AspNetUsers",
+ column: "NormalizedEmail");
- migrationBuilder.CreateIndex(
- name: "UserNameIndex",
- table: "AspNetUsers",
- column: "NormalizedUserName",
- unique: true,
- filter: "[NormalizedUserName] IS NOT NULL");
+ migrationBuilder.CreateIndex(
+ name: "UserNameIndex",
+ table: "AspNetUsers",
+ column: "NormalizedUserName",
+ unique: true,
+ filter: "[NormalizedUserName] IS NOT NULL");
- migrationBuilder.CreateIndex(
- name: "IX_TodoItems_TodoListId",
- table: "TodoItems",
- column: "TodoListId");
- }
+ migrationBuilder.CreateIndex(
+ name: "IX_TodoItems_TodoListId",
+ table: "TodoItems",
+ column: "TodoListId");
+ }
- ///
- protected override void Down(MigrationBuilder migrationBuilder)
- {
- migrationBuilder.DropTable(
- name: "AspNetRoleClaims");
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "AspNetRoleClaims");
- migrationBuilder.DropTable(
- name: "AspNetUserClaims");
+ migrationBuilder.DropTable(
+ name: "AspNetUserClaims");
- migrationBuilder.DropTable(
- name: "AspNetUserLogins");
+ migrationBuilder.DropTable(
+ name: "AspNetUserLogins");
- migrationBuilder.DropTable(
- name: "AspNetUserRoles");
+ migrationBuilder.DropTable(
+ name: "AspNetUserRoles");
- migrationBuilder.DropTable(
- name: "AspNetUserTokens");
+ migrationBuilder.DropTable(
+ name: "AspNetUserTokens");
- migrationBuilder.DropTable(
- name: "TodoItems");
+ migrationBuilder.DropTable(
+ name: "TodoItems");
- migrationBuilder.DropTable(
- name: "AspNetRoles");
+ migrationBuilder.DropTable(
+ name: "AspNetRoles");
- migrationBuilder.DropTable(
- name: "AspNetUsers");
+ migrationBuilder.DropTable(
+ name: "AspNetUsers");
- migrationBuilder.DropTable(
- name: "TodoLists");
- }
- }
+ migrationBuilder.DropTable(
+ name: "TodoLists");
+ }
}
diff --git a/AdvancedTodoList.Infrastructure/Services/EntityExistenceChecker.cs b/AdvancedTodoList.Infrastructure/Services/EntityExistenceChecker.cs
new file mode 100644
index 0000000..0aefb2a
--- /dev/null
+++ b/AdvancedTodoList.Infrastructure/Services/EntityExistenceChecker.cs
@@ -0,0 +1,33 @@
+using AdvancedTodoList.Core.Models;
+using AdvancedTodoList.Core.Services;
+using AdvancedTodoList.Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+
+namespace AdvancedTodoList.Infrastructure.Services;
+
+///
+/// A service that checks whether an entity with an ID exists.
+///
+public class EntityExistenceChecker(ApplicationDbContext dbContext) : IEntityExistenceChecker
+{
+ private readonly ApplicationDbContext _dbContext = dbContext;
+
+ ///
+ /// Asynchronously checks whether an entity of type with an ID
+ /// of type exists.
+ ///
+ /// Type of the entity.
+ /// Type which ID of the entity has.
+ /// ID of the entity which existence is checked.
+ ///
+ /// A task representing the asynchronous operation. The task result contains
+ /// if entity with the given ID exists; otherwise
+ /// .
+ ///
+ public async Task ExistsAsync(TId id)
+ where TEntity : class, IEntity
+ where TId : IEquatable
+ {
+ return await _dbContext.Set().AnyAsync(x => x.Id.Equals(id));
+ }
+}
diff --git a/AdvancedTodoList.Infrastructure/Services/TodoItemsService.cs b/AdvancedTodoList.Infrastructure/Services/TodoItemsService.cs
new file mode 100644
index 0000000..e151a1c
--- /dev/null
+++ b/AdvancedTodoList.Infrastructure/Services/TodoItemsService.cs
@@ -0,0 +1,160 @@
+using AdvancedTodoList.Core.Dtos;
+using AdvancedTodoList.Core.Models.TodoLists;
+using AdvancedTodoList.Core.Services;
+using AdvancedTodoList.Infrastructure.Data;
+using Mapster;
+using Microsoft.EntityFrameworkCore;
+
+namespace AdvancedTodoList.Infrastructure.Services;
+
+///
+/// A service that manages to-do lists items.
+///
+public class TodoItemsService(ApplicationDbContext dbContext) : ITodoItemsService
+{
+ private readonly ApplicationDbContext _dbContext = dbContext;
+
+ ///
+ /// Retrieves to-do list items of the list with the specified ID.
+ ///
+ ///
+ /// Does not throw exceptions if ID is invalid.
+ ///
+ /// The ID of the to-do list which items will be retrieved.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains a collection of objects.
+ ///
+ public async Task> GetItemsOfListAsync(string id)
+ {
+ return await _dbContext.TodoItems
+ .Where(x => x.TodoListId == id)
+ .ProjectToType()
+ .ToListAsync();
+ }
+
+ ///
+ /// Retrieves a to-do list ID of the to-do list item.
+ ///
+ /// ID of the to-do list item.
+ ///
+ /// A task representing the asynchronous operation. The task result contains
+ /// an ID of the to-do list which owns a to-do list item with the specified ID if it's found;
+ /// otherwise, returns .
+ ///
+ public async Task GetTodoListByIdAsync(int id)
+ {
+ return await _dbContext.TodoItems
+ .AsNoTracking()
+ .Where(x => x.Id == id)
+ .Select(x => x.TodoListId)
+ .FirstOrDefaultAsync();
+ }
+
+ ///
+ /// Retrieves a to-do list item by its ID asynchronously.
+ ///
+ /// The ID of the to-do list item to retrieve.
+ ///
+ /// A task representing the asynchronous operation. The task result contains
+ /// a object if the specified ID is found;
+ /// otherwise, returns .
+ ///
+ public async Task GetByIdAsync(int id)
+ {
+ return await _dbContext.TodoItems
+ .AsNoTracking()
+ .Where(x => x.Id == id)
+ .ProjectToType()
+ .FirstOrDefaultAsync();
+ }
+
+ ///
+ /// Creates a new to-do list item asynchronously.
+ ///
+ /// The ID of the to-do list to associate the item with.
+ /// The DTO containing information for creating the to-do list item.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains the created .
+ ///
+ public async Task CreateAsync(string todoListId, TodoItemCreateDto dto)
+ {
+ var item = dto.Adapt();
+ item.TodoListId = todoListId;
+ _dbContext.TodoItems.Add(item);
+ await _dbContext.SaveChangesAsync();
+ return item;
+ }
+
+ ///
+ /// Edits a to-do list item asynchronously.
+ ///
+ /// The ID of the to-do list item to edit.
+ /// The DTO containing information for editing the to-do list item.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains on success;
+ /// otherwise if the entity was not found.
+ ///
+ public async Task EditAsync(int id, TodoItemCreateDto dto)
+ {
+ var item = await _dbContext.TodoItems
+ .Where(x => x.Id == id)
+ .FirstOrDefaultAsync();
+ // To-do item does not exist - return false
+ if (item == null) return false;
+
+ dto.Adapt(item);
+ await _dbContext.SaveChangesAsync();
+
+ return true;
+ }
+
+ ///
+ /// Updates the state of a to-do list item asynchronously.
+ ///
+ /// The ID of the to-do list item to update the state.
+ /// The DTO containing information for updating the state of the to-do list item.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains on success;
+ /// otherwise if the entity was not found.
+ ///
+ public async Task UpdateStateAsync(int id, TodoItemUpdateStateDto dto)
+ {
+ var item = await _dbContext.TodoItems
+ .Where(x => x.Id == id)
+ .FirstOrDefaultAsync();
+ // To-do item does not exist - return false
+ if (item == null) return false;
+
+ item.State = dto.State;
+ await _dbContext.SaveChangesAsync();
+
+ return true;
+ }
+
+ ///
+ /// Deletes a to-do list item asynchronously.
+ ///
+ /// The ID of the to-do list item to delete.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains on success;
+ /// otherwise if the entity was not found.
+ ///
+ public async Task DeleteAsync(int id)
+ {
+ var item = await _dbContext.TodoItems
+ .Where(x => x.Id == id)
+ .FirstOrDefaultAsync();
+ // To-do item does not exist - return false
+ if (item == null) return false;
+
+ _dbContext.TodoItems.Remove(item);
+ await _dbContext.SaveChangesAsync();
+
+ return true;
+ }
+}
diff --git a/AdvancedTodoList.Infrastructure/Services/TodoListsService.cs b/AdvancedTodoList.Infrastructure/Services/TodoListsService.cs
index b328bea..f133267 100644
--- a/AdvancedTodoList.Infrastructure/Services/TodoListsService.cs
+++ b/AdvancedTodoList.Infrastructure/Services/TodoListsService.cs
@@ -7,14 +7,17 @@
namespace AdvancedTodoList.Infrastructure.Services;
+///
+/// A service that manages to-do lists.
+///
public class TodoListsService(ApplicationDbContext dbContext) : ITodoListsService
{
private readonly ApplicationDbContext _dbContext = dbContext;
///
- /// Retrieves a to-do list by its unique identifier asynchronously.
+ /// Retrieves a to-do list by its ID asynchronously.
///
- /// The unique identifier of the todo list to retrieve.
+ /// The ID of the to-do list to retrieve.
///
/// A task representing the asynchronous operation. The task result contains
/// a object if the specified ID is found;
@@ -22,21 +25,17 @@ public class TodoListsService(ApplicationDbContext dbContext) : ITodoListsServic
///
public async Task GetByIdAsync(string id)
{
- var list = await _dbContext.TodoLists
- .Include(x => x.TodoItems)
+ return await _dbContext.TodoLists
.AsNoTracking()
.Where(x => x.Id == id)
+ .ProjectToType()
.FirstOrDefaultAsync();
- // Todo list does not exist - return null
- if (list == null) return null;
-
- return list.Adapt();
}
///
/// Creates a new to-do list asynchronously.
///
- /// The data transfer object containing information for creating the to-do list.
+ /// The DTO containing information for creating the to-do list.
///
/// A task representing the asynchronous operation.
/// The task result contains the created .
@@ -44,9 +43,55 @@ public class TodoListsService(ApplicationDbContext dbContext) : ITodoListsServic
public async Task CreateAsync(TodoListCreateDto dto)
{
var list = dto.Adapt();
- _dbContext.Add(list);
+ _dbContext.TodoLists.Add(list);
await _dbContext.SaveChangesAsync();
return list;
}
+ ///
+ /// Edits a to-do list asynchronously.
+ ///
+ /// The ID of the to-do list to edit.
+ /// The DTO containing information for editing the to-do list.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains on success;
+ /// otherwise if entity was not found.
+ ///
+ public async Task EditAsync(string id, TodoListCreateDto dto)
+ {
+ var entity = await _dbContext.TodoLists
+ .Where(x => x.Id == id)
+ .FirstOrDefaultAsync();
+ // To-do list does not exist - return false
+ if (entity == null) return false;
+
+ dto.Adapt(entity);
+ await _dbContext.SaveChangesAsync();
+
+ return true;
+ }
+
+ ///
+ /// Deletes a to-do list asynchronously.
+ ///
+ /// The ID of the to-do list to edit.
+ ///
+ /// A task representing the asynchronous operation.
+ /// The task result contains on success;
+ /// otherwise if entity was not found.
+ ///
+ public async Task DeleteAsync(string id)
+ {
+ var entity = await _dbContext.TodoLists
+ .Where(x => x.Id == id)
+ .FirstOrDefaultAsync();
+ // To-do list does not exist - return false
+ if (entity == null) return false;
+
+ _dbContext.TodoLists.Remove(entity);
+ await _dbContext.SaveChangesAsync();
+
+ return true;
+ }
}
diff --git a/AdvancedTodoList.IntegrationTests/IntegrationTest.cs b/AdvancedTodoList.IntegrationTests/IntegrationTest.cs
index dabb93e..2c35c17 100644
--- a/AdvancedTodoList.IntegrationTests/IntegrationTest.cs
+++ b/AdvancedTodoList.IntegrationTests/IntegrationTest.cs
@@ -1,7 +1,6 @@
using AdvancedTodoList.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
-using Testcontainers.MsSql;
namespace AdvancedTodoList.IntegrationTests;
@@ -11,7 +10,7 @@ namespace AdvancedTodoList.IntegrationTests;
public abstract class IntegrationTest
{
private static bool s_migrated = false;
- private static MsSqlContainer s_testDbContainer;
+
protected static TestingWebApplicationFactory WebApplicationFactory { get; private set; }
protected IServiceScopeFactory ScopeFactory { get; private set; }
protected IServiceScope ServiceScope { get; private set; }
@@ -20,6 +19,10 @@ public abstract class IntegrationTest
[SetUp]
public async Task SetUpServices()
{
+ // Configure web application factory
+ WebApplicationFactory = new TestingWebApplicationFactory(IntegrationTestsSetup.TestDbContainer);
+ WebApplicationFactory.Server.PreserveExecutionContext = true;
+
// Get services needed for integration testing
ScopeFactory = WebApplicationFactory.Services.GetService()!;
ServiceScope = ScopeFactory.CreateScope();
@@ -38,25 +41,6 @@ public async Task TearDownServicesAsync()
// Dispose resources
await DbContext.DisposeAsync();
ServiceScope.Dispose();
- }
-
- [OneTimeSetUp]
- public static async Task SetUpIntegrationTestAsync()
- {
- // Initialize and start a container with test DB
- s_testDbContainer = new MsSqlBuilder().Build();
- await s_testDbContainer.StartAsync();
-
- // Configure web application factory
- WebApplicationFactory = new TestingWebApplicationFactory(s_testDbContainer);
- WebApplicationFactory.Server.PreserveExecutionContext = true;
- }
-
- [OneTimeTearDown]
- public static async Task TearDownIntegrationTestAsync()
- {
- // Stop the DB container
- await s_testDbContainer.StopAsync();
await WebApplicationFactory.DisposeAsync();
}
diff --git a/AdvancedTodoList.IntegrationTests/IntegrationTestsSetup.cs b/AdvancedTodoList.IntegrationTests/IntegrationTestsSetup.cs
new file mode 100644
index 0000000..41816a9
--- /dev/null
+++ b/AdvancedTodoList.IntegrationTests/IntegrationTestsSetup.cs
@@ -0,0 +1,30 @@
+using Testcontainers.MsSql;
+
+namespace AdvancedTodoList.IntegrationTests;
+
+///
+/// Class that sets up integration testing environment.
+///
+[SetUpFixture]
+public static class IntegrationTestsSetup
+{
+ ///
+ /// Test container that contains a database.
+ ///
+ public static MsSqlContainer TestDbContainer { get; private set; }
+
+ [OneTimeSetUp]
+ public static async Task SetUpIntegrationTests()
+ {
+ // Initialize and start a container with test DB
+ TestDbContainer = new MsSqlBuilder().Build();
+ await TestDbContainer.StartAsync();
+ }
+
+ [OneTimeTearDown]
+ public static async Task TearDownIntegrationTests()
+ {
+ // Stop the DB container
+ await TestDbContainer.StopAsync();
+ }
+}
diff --git a/AdvancedTodoList.IntegrationTests/Services/TodoItemsServiceTests.cs b/AdvancedTodoList.IntegrationTests/Services/TodoItemsServiceTests.cs
new file mode 100644
index 0000000..45acc54
--- /dev/null
+++ b/AdvancedTodoList.IntegrationTests/Services/TodoItemsServiceTests.cs
@@ -0,0 +1,263 @@
+using AdvancedTodoList.Core.Dtos;
+using AdvancedTodoList.Core.Models.TodoLists;
+using AdvancedTodoList.Core.Services;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace AdvancedTodoList.IntegrationTests.Services;
+
+///
+/// Tests for endpoints 'api/todo'
+///
+public class TodoItemsServiceTests : IntegrationTest
+{
+ private ITodoItemsService _service;
+
+ // Create a test to-do list and adds it to the DB
+ private async Task CreateTestListAsync()
+ {
+ TodoList testList = new()
+ {
+ Name = "Test list",
+ Description = "A test list for creating a test list item"
+ };
+ DbContext.TodoLists.Add(testList);
+ await DbContext.SaveChangesAsync();
+ DbContext.ChangeTracker.Clear();
+
+ return testList;
+ }
+
+ // Create a test to-do list item and adds it to the DB
+ private async Task CreateTestItemAsync()
+ {
+ TodoList testList = await CreateTestListAsync();
+
+ TodoItem testItem = new()
+ {
+ Name = "Test list item",
+ Description = "Make something cool",
+ DeadlineDate = null,
+ State = TodoItemState.Active,
+ TodoListId = testList.Id
+ };
+ DbContext.TodoItems.Add(testItem);
+ await DbContext.SaveChangesAsync();
+
+ DbContext.ChangeTracker.Clear();
+
+ return testItem;
+ }
+
+ [SetUp]
+ public void SetUp()
+ {
+ _service = ServiceScope.ServiceProvider.GetService()!;
+ }
+
+ [Test]
+ public async Task GetItemsOfListAsync_ListExists_ReturnsEmptyCollection()
+ {
+ // Arrange
+ var list = await CreateTestListAsync();
+ var fakeList = await CreateTestListAsync();
+ TodoItem[] items =
+ [
+ new TodoItem() { Name = "A", Description = "1", DeadlineDate = null, State = TodoItemState.Active, TodoListId = list.Id },
+ new TodoItem() { Name = "B", Description = "2", DeadlineDate = DateTime.UtcNow, State = TodoItemState.Skipped, TodoListId = list.Id },
+ new TodoItem() { Name = "C", Description = "3", DeadlineDate = null, State = TodoItemState.Completed, TodoListId = list.Id },
+ ];
+ DbContext.TodoItems.AddRange(items);
+ // Add one "fake" item
+ TodoItem fakeItem = new() { Name = "Fake", Description = "", DeadlineDate = null, State = TodoItemState.Active, TodoListId = fakeList.Id };
+ await DbContext.SaveChangesAsync();
+
+ // Act
+ var result = await _service.GetItemsOfListAsync(list.Id);
+
+ // Assert
+ Assert.That(result.Select(x => x.Id), Is.EquivalentTo(items.Select(x => x.Id)));
+ }
+
+ [Test]
+ public async Task GetItemsOfListAsync_WrongId_ReturnsEmptyCollection()
+ {
+ // Act
+ var result = await _service.GetItemsOfListAsync("_");
+
+ // Assert
+ Assert.That(result, Is.Empty);
+ }
+
+ [Test]
+ public async Task GetTodoListByIdAsync_EntityExists_ReturnsCorrectEntity()
+ {
+ // Arrange: add test item to the DB
+ var testItem = await CreateTestItemAsync();
+
+ // Act: try to obtain a test item's DTO by its ID
+ string? result = await _service.GetTodoListByIdAsync(testItem.Id);
+
+ // Assert that returned ID matches
+ Assert.That(result, Is.EqualTo(testItem.TodoListId));
+ }
+
+ [Test]
+ public async Task GetTodoListByIdAsync_EntityDoesNotExist_ReturnsNull()
+ {
+ // Act: try to obtain a test item with non-existent ID
+ var result = await _service.GetTodoListByIdAsync(-1);
+
+ // Assert that null is returned
+ Assert.That(result, Is.Null);
+ }
+
+ [Test]
+ public async Task GetByIdAsync_EntityExists_ReturnsCorrectEntity()
+ {
+ // Arrange: add test item to the DB
+ var testItem = await CreateTestItemAsync();
+
+ // Act: try to obtain a test item's DTO by its ID
+ var result = await _service.GetByIdAsync(testItem.Id);
+
+ // Assert that returned DTO matches
+ Assert.That(result, Is.Not.Null);
+ Assert.Multiple(() =>
+ {
+ Assert.That(result.Id, Is.EqualTo(testItem.Id));
+ Assert.That(result.Name, Is.EqualTo(testItem.Name));
+ });
+ }
+
+ [Test]
+ public async Task GetByIdAsync_EntityDoesNotExist_ReturnsNull()
+ {
+ // Act: try to obtain a test item with non-existent ID
+ var result = await _service.GetByIdAsync(-1);
+
+ // Assert that null is returned
+ Assert.That(result, Is.Null);
+ }
+
+ [Test]
+ public async Task CreateAsync_AddsEntityToDb()
+ {
+ // Arrange
+ var list = await CreateTestListAsync();
+ TodoItemCreateDto dto = new("Test entity", "...", null);
+
+ // Act: call the method
+ var result = await _service.CreateAsync(list.Id, dto);
+
+ // Assert that entity was added to the DB
+ var foundEntity = await DbContext.TodoItems
+ .AsNoTracking()
+ .Where(x => x.Id == result.Id)
+ .SingleOrDefaultAsync();
+ Assert.That(foundEntity, Is.Not.Null);
+ Assert.That(foundEntity.TodoListId, Is.EqualTo(list.Id));
+ }
+
+ [Test]
+ public async Task EditAsync_EntityExists_Succeeds()
+ {
+ // Arrange: add test item to the DB
+ var testItem = await CreateTestItemAsync();
+ // Test edit DTO
+ TodoItemCreateDto dto = new("Edited name", "Edited description", DateTime.UtcNow);
+
+ // Act: edit the item
+ bool result = await _service.EditAsync(testItem.Id, dto);
+
+ // Assert that result is true
+ Assert.That(result);
+ // Assert that item was updated
+ var updatedItem = await DbContext.TodoItems
+ .AsNoTracking()
+ .Where(x => x.Id == testItem.Id)
+ .FirstAsync();
+ Assert.Multiple(() =>
+ {
+ Assert.That(updatedItem.Name, Is.EqualTo(dto.Name));
+ Assert.That(updatedItem.Description, Is.EqualTo(dto.Description));
+ Assert.That(updatedItem.DeadlineDate, Is.EqualTo(dto.DeadlineDate));
+ });
+ }
+
+ [Test]
+ public async Task EditAsync_EntityDoesNotExist_ReturnsFalse()
+ {
+ // Arrange: make a DTO
+ TodoItemCreateDto dto = new("This", "should not be used if code works properly.", null);
+
+ // Act: try to edit a non-existent item
+ bool result = await _service.EditAsync(-1, dto);
+
+ // Assert that result is false
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public async Task UpdateStateAsync_EntityExists_Succeeds()
+ {
+ // Arrange: add test item to the DB
+ var testItem = await CreateTestItemAsync();
+ // Test DTO
+ TodoItemUpdateStateDto dto = new(TodoItemState.Completed);
+
+ // Act: update the state of the item
+ bool result = await _service.UpdateStateAsync(testItem.Id, dto);
+
+ // Assert that result is true
+ Assert.That(result);
+ // Assert that item's state was updated
+ var updatedItem = await DbContext.TodoItems
+ .AsNoTracking()
+ .Where(x => x.Id == testItem.Id)
+ .FirstAsync();
+ Assert.That(updatedItem.State, Is.EqualTo(dto.State));
+ }
+
+ [Test]
+ public async Task UpdateStateAsync_EntityDoesNotExist_ReturnsFalse()
+ {
+ // Arrange: make a DTO
+ TodoItemUpdateStateDto dto = new(TodoItemState.Skipped);
+
+ // Act: try to update the state of a non-existent item
+ bool result = await _service.UpdateStateAsync(-1, dto);
+
+ // Assert that result is false
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public async Task DeleteAsync_EntityExists_Succeeds()
+ {
+ // Arrange: add test item to the DB
+ var testItem = await CreateTestItemAsync();
+
+ // Act: delete the item
+ bool result = await _service.DeleteAsync(testItem.Id);
+
+ // Assert that result is true
+ Assert.That(result);
+ // Assert that an item was deleted
+ var actualList = await DbContext.TodoItems
+ .AsNoTracking()
+ .Where(x => x.Id == testItem.Id)
+ .FirstOrDefaultAsync();
+ Assert.That(actualList, Is.Null);
+ }
+
+ [Test]
+ public async Task DeleteAsync_EntityDoesNotExist_ReturnsFalse()
+ {
+ // Act: try to delete a non-existent item
+ bool result = await _service.DeleteAsync(-1);
+
+ // Assert that result is false
+ Assert.That(result, Is.False);
+ }
+}
diff --git a/AdvancedTodoList.IntegrationTests/Services/TodoListServiceTests.cs b/AdvancedTodoList.IntegrationTests/Services/TodoListServiceTests.cs
deleted file mode 100644
index 8f488a4..0000000
--- a/AdvancedTodoList.IntegrationTests/Services/TodoListServiceTests.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using AdvancedTodoList.Core.Dtos;
-using AdvancedTodoList.Core.Models.TodoLists;
-using AdvancedTodoList.Core.Services;
-using Mapster;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace AdvancedTodoList.IntegrationTests.Services;
-
-[TestFixture]
-public class TodoListServiceTests : IntegrationTest
-{
- private ITodoListsService _service;
-
- [SetUp]
- public void SetUp()
- {
- _service = ServiceScope.ServiceProvider.GetService()!;
- }
-
- [Test]
- public async Task GetByIdAsync_EntityExists_ReturnsCorrectEntity()
- {
- // Arrange: add test list to the DB
- TodoList testList = new()
- {
- Name = "Test list"
- };
- DbContext.TodoLists.Add(testList);
- await DbContext.SaveChangesAsync();
-
- // Act: try to obtain a test list's DTO by its ID
- var result = await _service.GetByIdAsync(testList.Id);
-
- // Assert that returned DTO matches
- Assert.That(result, Is.Not.Null);
- Assert.Multiple(() =>
- {
- Assert.That(result.Id, Is.EqualTo(testList.Id));
- Assert.That(result.Name, Is.EqualTo(testList.Name));
- });
- }
-
- [Test]
- public async Task GetByIdAsync_EntityDoesNotExist_ReturnsNull()
- {
- // Act: try to obtain a test list with non existent ID
- var result = await _service.GetByIdAsync("_");
-
- // Assert that null is returned
- Assert.That(result, Is.Null);
- }
-
- [Test]
- public async Task CreateAsync_AddsEntityToDb()
- {
- // Arrange: initialize a DTO
- TodoListCreateDto dto = new("Test entity");
-
- // Act: call the method
- var result = await _service.CreateAsync(dto);
-
- // Assert that entity was added to the DB
- var foundEntity = await DbContext.TodoLists
- .AsNoTracking()
- .Where(x => x.Id == result.Id)
- .SingleOrDefaultAsync();
- Assert.That(foundEntity, Is.Not.Null);
- }
-}
diff --git a/AdvancedTodoList.IntegrationTests/Services/TodoListsServiceTests.cs b/AdvancedTodoList.IntegrationTests/Services/TodoListsServiceTests.cs
new file mode 100644
index 0000000..29ca05e
--- /dev/null
+++ b/AdvancedTodoList.IntegrationTests/Services/TodoListsServiceTests.cs
@@ -0,0 +1,146 @@
+using AdvancedTodoList.Core.Dtos;
+using AdvancedTodoList.Core.Models.TodoLists;
+using AdvancedTodoList.Core.Services;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace AdvancedTodoList.IntegrationTests.Services;
+
+[TestFixture]
+public class TodoListsServiceTests : IntegrationTest
+{
+ private ITodoListsService _service;
+
+ // Create a test to-do list and adds it to the DB
+ private async Task CreateTestListAsync()
+ {
+ TodoList testList = new()
+ {
+ Name = "Test list",
+ Description = ""
+ };
+ DbContext.TodoLists.Add(testList);
+ await DbContext.SaveChangesAsync();
+ DbContext.ChangeTracker.Clear();
+
+ return testList;
+ }
+
+ [SetUp]
+ public void SetUp()
+ {
+ _service = ServiceScope.ServiceProvider.GetService()!;
+ }
+
+ [Test]
+ public async Task GetByIdAsync_EntityExists_ReturnsCorrectEntity()
+ {
+ // Arrange: add test list to the DB
+ var testList = await CreateTestListAsync();
+
+ // Act: try to obtain a test list's DTO by its ID
+ var result = await _service.GetByIdAsync(testList.Id);
+
+ // Assert that returned DTO matches
+ Assert.That(result, Is.Not.Null);
+ Assert.Multiple(() =>
+ {
+ Assert.That(result.Id, Is.EqualTo(testList.Id));
+ Assert.That(result.Name, Is.EqualTo(testList.Name));
+ });
+ }
+
+ [Test]
+ public async Task GetByIdAsync_EntityDoesNotExist_ReturnsNull()
+ {
+ // Act: try to obtain a test list with non-existent ID
+ var result = await _service.GetByIdAsync("_");
+
+ // Assert that null is returned
+ Assert.That(result, Is.Null);
+ }
+
+ [Test]
+ public async Task CreateAsync_AddsEntityToDb()
+ {
+ // Arrange: initialize a DTO
+ TodoListCreateDto dto = new("Test entity", "...");
+
+ // Act: call the method
+ var result = await _service.CreateAsync(dto);
+
+ // Assert that entity was added to the DB
+ var foundEntity = await DbContext.TodoLists
+ .AsNoTracking()
+ .Where(x => x.Id == result.Id)
+ .SingleOrDefaultAsync();
+ Assert.That(foundEntity, Is.Not.Null);
+ }
+
+ [Test]
+ public async Task EditAsync_EntityExists_Succeeds()
+ {
+ // Arrange: add test list to the DB
+ var testList = await CreateTestListAsync();
+ // Test edit DTO
+ TodoListCreateDto dto = new("Edited name", "Edited description");
+
+ // Act: edit the list
+ bool result = await _service.EditAsync(testList.Id, dto);
+
+ // Assert that result is true
+ Assert.That(result);
+ // Assert that list was updated
+ var updatedList = await DbContext.TodoLists
+ .AsNoTracking()
+ .Where(x => x.Id == testList.Id)
+ .FirstAsync();
+ Assert.Multiple(() =>
+ {
+ Assert.That(updatedList.Name, Is.EqualTo(dto.Name));
+ Assert.That(updatedList.Description, Is.EqualTo(dto.Description));
+ });
+ }
+
+ [Test]
+ public async Task EditAsync_EntityDoesNotExist_ReturnsFalse()
+ {
+ // Arrange: make a DTO
+ TodoListCreateDto dto = new("This", "should not be used if code works properly.");
+
+ // Act: try to edit a non-existent list
+ bool result = await _service.EditAsync("_", dto);
+
+ // Assert that result is false
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public async Task DeleteAsync_EntityExists_Succeeds()
+ {
+ // Arrange: add test list to the DB
+ var testList = await CreateTestListAsync();
+
+ // Act: delete the list
+ bool result = await _service.DeleteAsync(testList.Id);
+
+ // Assert that result is true
+ Assert.That(result);
+ // Assert that list was deleted
+ var actualList = await DbContext.TodoLists
+ .AsNoTracking()
+ .Where(x => x.Id == testList.Id)
+ .FirstOrDefaultAsync();
+ Assert.That(actualList, Is.Null);
+ }
+
+ [Test]
+ public async Task DeleteAsync_EntityDoesNotExist_ReturnsFalse()
+ {
+ // Act: try to delete a non-existent list
+ bool result = await _service.DeleteAsync("_");
+
+ // Assert that result is false
+ Assert.That(result, Is.False);
+ }
+}
diff --git a/AdvancedTodoList.RouteTests/GlobalUsings.cs b/AdvancedTodoList.RouteTests/GlobalUsings.cs
index 803d05e..3e4da1d 100644
--- a/AdvancedTodoList.RouteTests/GlobalUsings.cs
+++ b/AdvancedTodoList.RouteTests/GlobalUsings.cs
@@ -1,2 +1,2 @@
+global using NSubstitute;
global using NUnit.Framework;
-global using NSubstitute;
\ No newline at end of file
diff --git a/AdvancedTodoList.RouteTests/RouteTestsWebApplicationFactory.cs b/AdvancedTodoList.RouteTests/RouteTestsWebApplicationFactory.cs
index 034626e..50cbee3 100644
--- a/AdvancedTodoList.RouteTests/RouteTestsWebApplicationFactory.cs
+++ b/AdvancedTodoList.RouteTests/RouteTestsWebApplicationFactory.cs
@@ -1,27 +1,29 @@
using AdvancedTodoList.Core.Services;
-using AdvancedTodoList.Infrastructure.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
-using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
-using NSubstitute;
-using System.Data.Common;
namespace AdvancedTodoList.RouteTests;
public class RouteTestsWebApplicationFactory : WebApplicationFactory
{
public ITodoListsService TodoListsService { get; private set; } = null!;
+ public ITodoItemsService TodoItemsService { get; private set; } = null!;
+ public IEntityExistenceChecker EntityExistenceChecker { get; private set; } = null!;
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
- // Create mocks for services
+ // Create mocks for the services
TodoListsService = Substitute.For();
+ TodoItemsService = Substitute.For();
+ EntityExistenceChecker = Substitute.For();
builder.ConfigureTestServices(services =>
{
services.AddScoped(_ => TodoListsService);
+ services.AddScoped(_ => TodoItemsService);
+ services.AddScoped(_ => EntityExistenceChecker);
});
}
}
diff --git a/AdvancedTodoList.RouteTests/Tests/TodoItemsEndpointsTests.cs b/AdvancedTodoList.RouteTests/Tests/TodoItemsEndpointsTests.cs
new file mode 100644
index 0000000..4428075
--- /dev/null
+++ b/AdvancedTodoList.RouteTests/Tests/TodoItemsEndpointsTests.cs
@@ -0,0 +1,421 @@
+using AdvancedTodoList.Core.Dtos;
+using AdvancedTodoList.Core.Models.TodoLists;
+using AdvancedTodoList.RouteTests;
+using NSubstitute.ReturnsExtensions;
+using System.Net;
+using System.Net.Http.Json;
+
+namespace AdvancedTodoItem.RouteTests.Tests;
+
+///
+/// Tests for endpoints 'api/todo/{listId}/items'
+///
+public class TodoItemsEndpointsTests : RouteTest
+{
+ [Test]
+ public async Task GetTodoItems_ListExists_SucceedsAndReturnsItems()
+ {
+ // Arrange
+ string testListId = "TestId";
+ TodoItemPreviewDto[] collection =
+ [
+ new(124013, testListId, "1", null, TodoItemState.Active),
+ new(124124, testListId, "2", null, TodoItemState.Completed)
+ ];
+
+ WebApplicationFactory.EntityExistenceChecker
+ .ExistsAsync(testListId)
+ .Returns(true);
+ WebApplicationFactory.TodoItemsService
+ .GetItemsOfListAsync(testListId)
+ .Returns(collection);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.GetAsync($"api/todo/{testListId}/items/");
+
+ // Assert that response indicates success
+ result.EnsureSuccessStatusCode();
+ // Assert that valid collection was returned
+ var returnedCollection = await result.Content.ReadFromJsonAsync();
+ Assert.That(returnedCollection, Is.Not.Null);
+ Assert.That(returnedCollection, Is.EquivalentTo(collection));
+ }
+
+ [Test]
+ public async Task GetTodoItems_ListDoesNotExist_Returns404()
+ {
+ // Arrange
+ string testListId = "TestId";
+
+ WebApplicationFactory.EntityExistenceChecker
+ .ExistsAsync(testListId)
+ .Returns(false);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.GetAsync($"api/todo/{testListId}/items/");
+
+ // Assert that response code is 404
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
+ }
+
+ [Test]
+ public async Task GetTodoItemById_ElementExists_ReturnsElement()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 777;
+ TodoItemGetByIdDto testDto = new(testItemId, testListId, "Test todo item", "...", null, TodoItemState.Active);
+
+ WebApplicationFactory.TodoItemsService
+ .GetByIdAsync(testItemId)
+ .Returns(testDto);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.GetAsync($"api/todo/{testListId}/items/{testItemId}");
+
+ // Assert that response indicates success
+ result.EnsureSuccessStatusCode();
+ // Assert that valid object was returned
+ var returnedDto = await result.Content.ReadFromJsonAsync();
+ Assert.That(returnedDto, Is.Not.Null);
+ Assert.That(returnedDto, Is.EqualTo(testDto));
+ }
+
+ [Test]
+ public async Task GetTodoItemById_ElementDoesNotExist_Returns404()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 777;
+ WebApplicationFactory.TodoItemsService
+ .GetByIdAsync(testItemId)
+ .ReturnsNull();
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.GetAsync($"api/todo/{testListId}/items/{testItemId}");
+
+ // Assert that response code is 404
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
+ }
+
+ [Test]
+ public async Task GetTodoItemById_WrongTodoListId_Returns404()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 777;
+ TodoItemGetByIdDto testDto = new(testItemId, "RealListId", "Test todo item", "...", null, TodoItemState.Active);
+ WebApplicationFactory.TodoItemsService
+ .GetByIdAsync(testItemId)
+ .Returns(testDto);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.GetAsync($"api/todo/{testListId}/items/{testItemId}");
+
+ // Assert that response code is 404
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
+ }
+
+ [Test]
+ public async Task PostTodoItem_ValidCall_Succeeds()
+ {
+ // Arrange
+ string listId = "ListId";
+ WebApplicationFactory.EntityExistenceChecker
+ .ExistsAsync(listId)
+ .Returns(true);
+ WebApplicationFactory.TodoItemsService
+ .CreateAsync(Arg.Any(), Arg.Any())
+ .Returns(new TodoItem() { Id = 13480 });
+ TodoItemCreateDto dto = new("Item", "...", DateTime.MaxValue);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PostAsJsonAsync($"api/todo/{listId}/items", dto);
+
+ // Assert that response indicates success
+ result.EnsureSuccessStatusCode();
+ // Assert that create method was called
+ await WebApplicationFactory.TodoItemsService
+ .Received()
+ .CreateAsync(listId, dto);
+ }
+
+ [Test]
+ public async Task PostTodoItem_InvalidDto_Returns400()
+ {
+ // Arrange
+ string listId = "ListId";
+ TodoItemCreateDto invalidDto = new(string.Empty, string.Empty, DateTime.MinValue);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PostAsJsonAsync($"api/todo/{listId}/items", invalidDto);
+
+ // Assert that response code is 400
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
+ }
+
+ [Test]
+ public async Task PostTodoItem_TodoListDoesNotExist_Returns404()
+ {
+ // Arrange
+ string listId = "ListId";
+ WebApplicationFactory.EntityExistenceChecker
+ .ExistsAsync(listId)
+ .Returns(false);
+ TodoItemCreateDto dto = new("Item", "...", DateTime.MaxValue);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PostAsJsonAsync($"api/todo/{listId}/items", dto);
+
+ // Assert that response code is 404
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
+ }
+
+ [Test]
+ public async Task PutTodoItem_ElementExists_Succeeds()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 891349;
+ TodoItemCreateDto dto = new("Do nothing for entire day", "...", DateTime.MaxValue);
+
+ WebApplicationFactory.TodoItemsService
+ .GetTodoListByIdAsync(testItemId)
+ .Returns(testListId);
+ WebApplicationFactory.TodoItemsService
+ .EditAsync(testItemId, dto)
+ .Returns(true);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PutAsJsonAsync($"api/todo/{testListId}/items/{testItemId}", dto);
+
+ // Assert that response indicates success
+ result.EnsureSuccessStatusCode();
+ // Assert that edit was called
+ await WebApplicationFactory.TodoItemsService
+ .Received()
+ .EditAsync(testItemId, dto);
+ }
+
+ [Test]
+ public async Task PutTodoItem_InvalidDto_Returns400()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 891349;
+ TodoItemCreateDto invalidDto = new(string.Empty, string.Empty, DateTime.MinValue);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PutAsJsonAsync($"api/todo/{testListId}/items/{testItemId}", invalidDto);
+
+ // Assert that response code is 400
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
+ }
+
+ [Test]
+ public async Task PutTodoItem_WrongTodoListId_Returns404()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 891349;
+ TodoItemCreateDto dto = new("Do nothing for entire day", "...", DateTime.MaxValue);
+
+ WebApplicationFactory.TodoItemsService
+ .GetTodoListByIdAsync(testItemId)
+ .Returns("WrongId");
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PutAsJsonAsync($"api/todo/{testListId}/items/{testItemId}", dto);
+
+ // Assert that response code is 404
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
+ }
+
+ [Test]
+ public async Task PutTodoItem_ElementDoesNotExist_Returns404()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 12412;
+ TodoItemCreateDto dto = new("New name", "New description", null);
+
+ WebApplicationFactory.TodoItemsService
+ .GetTodoListByIdAsync(testItemId)
+ .Returns(testListId);
+ WebApplicationFactory.TodoItemsService
+ .EditAsync(testItemId, dto)
+ .Returns(false);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PutAsJsonAsync($"api/todo/{testListId}/items/{testItemId}", dto);
+
+ // Assert that response code is 404
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
+ }
+
+ [Test]
+ public async Task PutTodoItemState_ElementExists_Succeeds()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 891349;
+ TodoItemUpdateStateDto dto = new(TodoItemState.Completed);
+
+ WebApplicationFactory.TodoItemsService
+ .GetTodoListByIdAsync(testItemId)
+ .Returns(testListId);
+ WebApplicationFactory.TodoItemsService
+ .UpdateStateAsync(testItemId, dto)
+ .Returns(true);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PutAsJsonAsync($"api/todo/{testListId}/items/{testItemId}/state", dto);
+
+ // Assert that response indicates success
+ result.EnsureSuccessStatusCode();
+ // Assert that edit was called
+ await WebApplicationFactory.TodoItemsService
+ .Received()
+ .UpdateStateAsync(testItemId, dto);
+ }
+
+ [Test]
+ public async Task PutTodoItemState_InvalidDto_Returns400()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 891349;
+ TodoItemUpdateStateDto dto = new((TodoItemState)213);
+
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PutAsJsonAsync($"api/todo/{testListId}/items/{testItemId}/state", dto);
+
+ // Assert that response code is 400
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
+ }
+
+ [Test]
+ public async Task PutTodoItemState_WrongTodoListId_Returns404()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 891349;
+ TodoItemUpdateStateDto dto = new(TodoItemState.Skipped);
+
+ WebApplicationFactory.TodoItemsService
+ .GetTodoListByIdAsync(testItemId)
+ .Returns("WrongId");
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PutAsJsonAsync($"api/todo/{testListId}/items/{testItemId}/state", dto);
+
+ // Assert that response code is 404
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
+ }
+
+ [Test]
+ public async Task PutTodoItemState_ElementDoesNotExist_Returns404()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 12412;
+ TodoItemUpdateStateDto dto = new(TodoItemState.Skipped);
+
+ WebApplicationFactory.TodoItemsService
+ .GetTodoListByIdAsync(testItemId)
+ .Returns(testListId);
+ WebApplicationFactory.TodoItemsService
+ .UpdateStateAsync(testItemId, dto)
+ .Returns(false);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PutAsJsonAsync($"api/todo/{testListId}/items/{testItemId}/state", dto);
+
+ // Assert that response code is 404
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
+ }
+
+ [Test]
+ public async Task DeleteTodoItem_ElementExists_Succeeds()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 504030;
+
+ WebApplicationFactory.TodoItemsService
+ .GetTodoListByIdAsync(testItemId)
+ .Returns(testListId);
+ WebApplicationFactory.TodoItemsService
+ .DeleteAsync(testItemId)
+ .Returns(true);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.DeleteAsync($"api/todo/{testListId}/items/{testItemId}");
+
+ // Assert that response indicates success
+ result.EnsureSuccessStatusCode();
+ // Assert that delete was called
+ await WebApplicationFactory.TodoItemsService
+ .Received()
+ .DeleteAsync(testItemId);
+ }
+
+ [Test]
+ public async Task DeleteTodoItem_ElementDoesNotExist_Returns404()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 504030;
+
+ WebApplicationFactory.TodoItemsService
+ .GetTodoListByIdAsync(testItemId)
+ .Returns(testListId);
+ WebApplicationFactory.TodoItemsService
+ .DeleteAsync(testItemId)
+ .Returns(false);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.DeleteAsync($"api/todo/{testListId}/items/{testItemId}");
+
+ // Assert that response code is 404
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
+ }
+
+ [Test]
+ public async Task DeleteTodoItem_WrongTodoListId_Returns404()
+ {
+ // Arrange
+ string testListId = "TestId";
+ int testItemId = 504030;
+
+ WebApplicationFactory.TodoItemsService
+ .GetTodoListByIdAsync(testItemId)
+ .Returns("WrongId");
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.DeleteAsync($"api/todo/{testListId}/items/{testItemId}");
+
+ // Assert that response code is 404
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
+ }
+}
diff --git a/AdvancedTodoList.RouteTests/Tests/TodoListsEndpointsTests.cs b/AdvancedTodoList.RouteTests/Tests/TodoListsEndpointsTests.cs
index e3ba1b9..07888d4 100644
--- a/AdvancedTodoList.RouteTests/Tests/TodoListsEndpointsTests.cs
+++ b/AdvancedTodoList.RouteTests/Tests/TodoListsEndpointsTests.cs
@@ -14,11 +14,10 @@ public async Task GetTodoListById_ElementExists_ReturnsElement()
{
// Arrange
string testId = "TestId";
- TodoListGetByIdDto testDto = new(testId, "Test todo list", []);
+ TodoListGetByIdDto testDto = new(testId, "Test todo list", "");
WebApplicationFactory.TodoListsService
.GetByIdAsync(testId)
.Returns(testDto);
- TodoListCreateDto dto = new("Test");
using HttpClient client = WebApplicationFactory.CreateClient();
// Act: send the request
@@ -29,11 +28,7 @@ public async Task GetTodoListById_ElementExists_ReturnsElement()
// Assert that valid object was returned
var returnedDto = await result.Content.ReadFromJsonAsync();
Assert.That(returnedDto, Is.Not.Null);
- Assert.Multiple(() =>
- {
- Assert.That(returnedDto.Id, Is.EqualTo(testDto.Id));
- Assert.That(returnedDto.Name, Is.EqualTo(testDto.Name));
- });
+ Assert.That(returnedDto, Is.EqualTo(testDto));
}
[Test]
@@ -60,7 +55,7 @@ public async Task PostTodoList_ValidCall_Succeeds()
WebApplicationFactory.TodoListsService
.CreateAsync(Arg.Any())
.Returns(new TodoList() { Id = "TestID" });
- TodoListCreateDto dto = new("Test");
+ TodoListCreateDto dto = new("Test", string.Empty);
using HttpClient client = WebApplicationFactory.CreateClient();
// Act: send the request
@@ -73,4 +68,115 @@ await WebApplicationFactory.TodoListsService
.Received()
.CreateAsync(dto);
}
+
+ [Test]
+ public async Task PostTodoList_InvalidDto_Returns400()
+ {
+ // Arrange
+ TodoListCreateDto invalidDto = new(string.Empty, string.Empty);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PostAsJsonAsync("api/todo", invalidDto);
+
+ // Assert that response code is 400
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
+ }
+
+ [Test]
+ public async Task PutTodoList_ElementExists_Succeeds()
+ {
+ // Arrange
+ string testId = "TestId";
+ TodoListCreateDto dto = new("New name", "New description");
+
+ WebApplicationFactory.TodoListsService
+ .EditAsync(testId, dto)
+ .Returns(true);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PutAsJsonAsync($"api/todo/{testId}", dto);
+
+ // Assert that response indicates success
+ result.EnsureSuccessStatusCode();
+ // Assert that edit was called
+ await WebApplicationFactory.TodoListsService
+ .Received()
+ .EditAsync(testId, dto);
+ }
+
+ [Test]
+ public async Task PutTodoList_InvalidDto_Returns400()
+ {
+ // Arrange
+ string testId = "TestId";
+ TodoListCreateDto invalidDto = new(string.Empty, string.Empty);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PutAsJsonAsync($"api/todo/{testId}", invalidDto);
+
+ // Assert that response code is 400
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
+ }
+
+ [Test]
+ public async Task PutTodoList_ElementDoesNotExist_Returns404()
+ {
+ // Arrange
+ string testId = "TestId";
+ TodoListCreateDto dto = new("New name", "New description");
+
+ WebApplicationFactory.TodoListsService
+ .EditAsync(testId, dto)
+ .Returns(false);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.PutAsJsonAsync($"api/todo/{testId}", dto);
+
+ // Assert that response code is 404
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
+ }
+
+ [Test]
+ public async Task DeleteTodoList_ElementExists_Succeeds()
+ {
+ // Arrange
+ string testId = "TestId";
+
+ WebApplicationFactory.TodoListsService
+ .DeleteAsync(testId)
+ .Returns(true);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.DeleteAsync($"api/todo/{testId}");
+
+ // Assert that response indicates success
+ result.EnsureSuccessStatusCode();
+ // Assert that delete was called
+ await WebApplicationFactory.TodoListsService
+ .Received()
+ .DeleteAsync(testId);
+ }
+
+ [Test]
+ public async Task DeleteTodoList_ElementDoesNotExist_Returns404()
+ {
+ // Arrange
+ string testId = "TestId";
+
+ WebApplicationFactory.TodoListsService
+ .DeleteAsync(testId)
+ .Returns(false);
+ using HttpClient client = WebApplicationFactory.CreateClient();
+
+ // Act: send the request
+ var result = await client.DeleteAsync($"api/todo/{testId}");
+
+ // Assert that response code is 404
+ Assert.That(result.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
+ }
}
diff --git a/AdvancedTodoList.UnitTests/GlobalUsings.cs b/AdvancedTodoList.UnitTests/GlobalUsings.cs
index cefced4..97651f3 100644
--- a/AdvancedTodoList.UnitTests/GlobalUsings.cs
+++ b/AdvancedTodoList.UnitTests/GlobalUsings.cs
@@ -1 +1,3 @@
-global using NUnit.Framework;
\ No newline at end of file
+global using FluentValidation;
+global using FluentValidation.TestHelper;
+global using NUnit.Framework;
diff --git a/AdvancedTodoList.UnitTests/Mapping/InputSanitizingTests.cs b/AdvancedTodoList.UnitTests/Mapping/InputSanitizingTests.cs
new file mode 100644
index 0000000..acc520c
--- /dev/null
+++ b/AdvancedTodoList.UnitTests/Mapping/InputSanitizingTests.cs
@@ -0,0 +1,57 @@
+using AdvancedTodoList.Core.Mapping;
+using Mapster;
+
+namespace AdvancedTodoList.UnitTests.Mapping;
+
+[TestFixture]
+public class InputSanitizingTests
+{
+ private class Poco
+ {
+ public string? Text { get; set; }
+ public DateTime Date { get; set; }
+ public DateTime? NullableDate { get; set; }
+ }
+
+ private record TextDto(string Text);
+ private record DateDto(DateTime Date, DateTime? NullableDate);
+
+ [SetUp]
+ public void SetUp()
+ {
+ MappingGlobalSettings.Apply();
+ }
+
+ [Test]
+ public void NullString_MapsIntoEmptyString()
+ {
+ // Arrange
+ Poco poco = new()
+ {
+ Text = null
+ };
+
+ // Act
+ var dto = poco.Adapt();
+
+ // Assert
+ Assert.That(dto.Text, Is.EqualTo(string.Empty));
+ }
+
+ [Test]
+ public void String_MapsIntoTrimmedString()
+ {
+ // Arrange
+ string expectedText = "Lorem ipsum dolor sit amet, ...";
+ Poco poco = new()
+ {
+ Text = $"\t\r {expectedText} \t\t\r "
+ };
+
+ // Act
+ var dto = poco.Adapt();
+
+ // Assert
+ Assert.That(dto.Text, Is.EqualTo(expectedText));
+ }
+}
diff --git a/AdvancedTodoList.UnitTests/TestTests.cs b/AdvancedTodoList.UnitTests/TestTests.cs
deleted file mode 100644
index bc597ab..0000000
--- a/AdvancedTodoList.UnitTests/TestTests.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace AdvancedTodoList.UnitTests;
-
-public class Tests
-{
- [Test]
- public void TestTest()
- {
- Assert.Pass();
- }
-}
\ No newline at end of file
diff --git a/AdvancedTodoList.UnitTests/Validation/TodoItemCreateDtoValidatorTests.cs b/AdvancedTodoList.UnitTests/Validation/TodoItemCreateDtoValidatorTests.cs
new file mode 100644
index 0000000..5ee28ea
--- /dev/null
+++ b/AdvancedTodoList.UnitTests/Validation/TodoItemCreateDtoValidatorTests.cs
@@ -0,0 +1,93 @@
+using AdvancedTodoList.Core.Dtos;
+using AdvancedTodoList.Core.Validation;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+
+namespace AdvancedTodoList.UnitTests.Validation;
+
+[TestFixture]
+public class TodoItemCreateDtoValidatorTests
+{
+ private const string ValidName = "Valid";
+ private const string ValidDescription = "Valid description";
+ private static DateTime? ValidDeadline => DateTime.UtcNow + TimeSpan.FromDays(14);
+
+ [Test]
+ public void Dto_ValidData_Succeeds()
+ {
+ // Arrange
+ TodoItemCreateDtoValidator validator = new();
+ TodoItemCreateDto dto = new(ValidName, ValidDescription, ValidDeadline);
+
+ // Act
+ var result = validator.TestValidate(dto);
+
+ // Assert
+ result.ShouldNotHaveAnyValidationErrors();
+ }
+
+ [Test]
+ [TestCase("")]
+ [TestCase(" ")]
+ [TestCase("\r\t \t\t\r")]
+ [TestCase(null)]
+ public void Name_Empty_ReturnsPropertyRequiredError(string testCase)
+ {
+ // Arrange
+ TodoItemCreateDtoValidator validator = new();
+ TodoItemCreateDto dto = new(testCase, ValidDescription, ValidDeadline);
+
+ // Act
+ var result = validator.TestValidate(dto);
+
+ // Assert
+ result.ShouldHaveValidationErrorFor(x => x.Name)
+ .WithErrorCode(ValidationErrorCodes.PropertyRequired);
+ }
+
+ [Test]
+ public void Description_Null_ReturnsPropertyRequiredError()
+ {
+ // Arrange
+ TodoItemCreateDtoValidator validator = new();
+ TodoItemCreateDto dto = new(ValidName, null!, ValidDeadline);
+
+ // Act
+ var result = validator.TestValidate(dto);
+
+ // Assert
+ result.ShouldHaveValidationErrorFor(x => x.Description)
+ .WithErrorCode(ValidationErrorCodes.PropertyRequired);
+ }
+
+ [Test]
+ [TestCase("")]
+ [TestCase(" ")]
+ [TestCase("\r\t \t\t\r")]
+ public void Description_EmptyAndNotNull_IsAllowed(string testCase)
+ {
+ // Arrange
+ TodoItemCreateDtoValidator validator = new();
+ TodoItemCreateDto dto = new(ValidName, testCase, ValidDeadline);
+
+ // Act
+ var result = validator.TestValidate(dto);
+
+ // Assert
+ result.ShouldNotHaveValidationErrorFor(x => x.Description);
+ }
+
+ [Test]
+ public void DeadlineDate_BeforeCurrentDate_ReturnsPropertyOutOfRangeError()
+ {
+ // Arrange
+ TodoItemCreateDtoValidator validator = new();
+ TodoItemCreateDto dto = new(ValidName, ValidDescription, DateTime.UtcNow - TimeSpan.FromDays(1));
+
+ // Act
+ var result = validator.TestValidate(dto);
+
+ // Assert
+ result.ShouldHaveValidationErrorFor(x => x.DeadlineDate)
+ .WithErrorCode(ValidationErrorCodes.PropertyOutOfRange);
+ }
+}
diff --git a/AdvancedTodoList.UnitTests/Validation/TodoItemUpdateStateDtoValidatorTests.cs b/AdvancedTodoList.UnitTests/Validation/TodoItemUpdateStateDtoValidatorTests.cs
new file mode 100644
index 0000000..41ccf03
--- /dev/null
+++ b/AdvancedTodoList.UnitTests/Validation/TodoItemUpdateStateDtoValidatorTests.cs
@@ -0,0 +1,44 @@
+using AdvancedTodoList.Core.Dtos;
+using AdvancedTodoList.Core.Models.TodoLists;
+using AdvancedTodoList.Core.Validation;
+
+namespace AdvancedTodoList.UnitTests.Validation;
+
+[TestFixture]
+public class TodoItemUpdateStateDtoValidatorTests
+{
+ [Test]
+ [TestCase(TodoItemState.Active)]
+ [TestCase(TodoItemState.Completed)]
+ [TestCase(TodoItemState.Skipped)]
+ public void State_Valid_Succeeds(TodoItemState testCase)
+ {
+ // Arrange
+ TodoItemUpdateStateDtoValidator validator = new();
+ TodoItemUpdateStateDto dto = new(testCase);
+
+ // Act
+ var result = validator.TestValidate(dto);
+
+ // Assert
+ result.ShouldNotHaveAnyValidationErrors();
+ }
+
+ [Test]
+ [TestCase((TodoItemState)6)]
+ [TestCase((TodoItemState)124)]
+ [TestCase((TodoItemState)200)]
+ public void State_OutOfRange_ReturnsPropertyOutOfRangeError(TodoItemState testCase)
+ {
+ // Arrange
+ TodoItemUpdateStateDtoValidator validator = new();
+ TodoItemUpdateStateDto dto = new(testCase);
+
+ // Act
+ var result = validator.TestValidate(dto);
+
+ // Assert
+ result.ShouldHaveValidationErrorFor(x => x.State)
+ .WithErrorCode(ValidationErrorCodes.PropertyOutOfRange);
+ }
+}
diff --git a/AdvancedTodoList.UnitTests/Validation/TodoListCreateDtoValidatorTests.cs b/AdvancedTodoList.UnitTests/Validation/TodoListCreateDtoValidatorTests.cs
new file mode 100644
index 0000000..fc28885
--- /dev/null
+++ b/AdvancedTodoList.UnitTests/Validation/TodoListCreateDtoValidatorTests.cs
@@ -0,0 +1,76 @@
+using AdvancedTodoList.Core.Dtos;
+using AdvancedTodoList.Core.Validation;
+
+namespace AdvancedTodoList.UnitTests.Validation;
+
+[TestFixture]
+public class TodoListCreateDtoValidatorTests
+{
+ private const string ValidName = "Valid";
+ private const string ValidDescription = "Valid description";
+
+ [Test]
+ public void Dto_ValidData_Succeeds()
+ {
+ // Arrange
+ TodoListCreateDtoValidator validator = new();
+ TodoListCreateDto dto = new(ValidName, ValidDescription);
+
+ // Act
+ var result = validator.TestValidate(dto);
+
+ // Assert
+ result.ShouldNotHaveAnyValidationErrors();
+ }
+
+ [Test]
+ [TestCase("")]
+ [TestCase(" ")]
+ [TestCase("\r\t \t\t\r")]
+ [TestCase(null)]
+ public void Name_Empty_ReturnsPropertyRequiredError(string testCase)
+ {
+ // Arrange
+ TodoListCreateDtoValidator validator = new();
+ TodoListCreateDto dto = new(testCase, ValidDescription);
+
+ // Act
+ var result = validator.TestValidate(dto);
+
+ // Assert
+ result.ShouldHaveValidationErrorFor(x => x.Name)
+ .WithErrorCode(ValidationErrorCodes.PropertyRequired);
+ }
+
+ [Test]
+ public void Description_Null_ReturnsPropertyRequiredError()
+ {
+ // Arrange
+ TodoListCreateDtoValidator validator = new();
+ TodoListCreateDto dto = new(ValidName, null!);
+
+ // Act
+ var result = validator.TestValidate(dto);
+
+ // Assert
+ result.ShouldHaveValidationErrorFor(x => x.Description)
+ .WithErrorCode(ValidationErrorCodes.PropertyRequired);
+ }
+
+ [Test]
+ [TestCase("")]
+ [TestCase(" ")]
+ [TestCase("\r\t \t\t\r")]
+ public void Description_EmptyAndNotNull_IsAllowed(string testCase)
+ {
+ // Arrange
+ TodoListCreateDtoValidator validator = new();
+ TodoListCreateDto dto = new(ValidName, testCase);
+
+ // Act
+ var result = validator.TestValidate(dto);
+
+ // Assert
+ result.ShouldNotHaveValidationErrorFor(x => x.Description);
+ }
+}
diff --git a/AdvancedTodoList/AdvancedTodoList.csproj b/AdvancedTodoList/AdvancedTodoList.csproj
index 2fcb60e..3ec5a8b 100644
--- a/AdvancedTodoList/AdvancedTodoList.csproj
+++ b/AdvancedTodoList/AdvancedTodoList.csproj
@@ -8,10 +8,12 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/AdvancedTodoList/Controllers/TodoController.cs b/AdvancedTodoList/Controllers/TodoController.cs
new file mode 100644
index 0000000..e18730c
--- /dev/null
+++ b/AdvancedTodoList/Controllers/TodoController.cs
@@ -0,0 +1,200 @@
+using AdvancedTodoList.Core.Dtos;
+using AdvancedTodoList.Core.Models.TodoLists;
+using AdvancedTodoList.Core.Services;
+using Mapster;
+using Microsoft.AspNetCore.Mvc;
+
+namespace AdvancedTodoList.Controllers;
+
+[ApiController]
+[Route("api/todo")]
+public class TodoController(
+ ITodoListsService todoListsService, ITodoItemsService todoItemsService,
+ IEntityExistenceChecker existenceChecker) : ControllerBase
+{
+ private readonly ITodoListsService _todoListsService = todoListsService;
+ private readonly ITodoItemsService _todoItemsService = todoItemsService;
+ private readonly IEntityExistenceChecker _existenceChecker = existenceChecker;
+
+ #region Lists
+ ///
+ /// Gets a to-do list by its ID.
+ ///
+ /// ID of the to-do list to obtain.
+ /// Returns requested to-do list.
+ /// To-do list was not found.
+ [HttpGet("{listId}", Name = nameof(GetTodoListByIdAsync))]
+ [ProducesResponseType(typeof(TodoListGetByIdDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task GetTodoListByIdAsync([FromRoute] string listId)
+ {
+ var list = await _todoListsService.GetByIdAsync(listId);
+ return list != null ? Ok(list) : NotFound();
+ }
+
+ ///
+ /// Creates a new to-do list.
+ ///
+ /// Successfully created.
+ [HttpPost]
+ [ProducesResponseType(typeof(TodoListGetByIdDto), StatusCodes.Status201Created)]
+ public async Task PostTodoListAsync([FromBody] TodoListCreateDto dto)
+ {
+ var list = await _todoListsService.CreateAsync(dto);
+ var routeValues = new { listId = list.Id };
+ var body = list.Adapt();
+ return CreatedAtRoute(nameof(GetTodoListByIdAsync), routeValues, body);
+ }
+
+ ///
+ /// Updates a to-do list.
+ ///
+ /// Success.
+ /// To-do list was not found.
+ [HttpPut("{listId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task PutTodoListAsync(
+ [FromRoute] string listId, [FromBody] TodoListCreateDto dto)
+ {
+ bool result = await _todoListsService.EditAsync(listId, dto);
+ return result ? NoContent() : NotFound();
+ }
+
+ ///
+ /// Deletes a to-do list.
+ ///
+ /// Success.
+ /// To-do list was not found.
+ [HttpDelete("{listId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task DeleteTodoListAsync([FromRoute] string listId)
+ {
+ bool result = await _todoListsService.DeleteAsync(listId);
+ return result ? NoContent() : NotFound();
+ }
+ #endregion
+
+ #region Items
+ // Check if listId matches with an actual list ID
+ private async Task CheckListIdAsync(string listId, int itemId)
+ {
+ return listId == (await _todoItemsService.GetTodoListByIdAsync(itemId));
+ }
+
+ ///
+ /// Gets items of the to-do list with the specified ID.
+ ///
+ /// ID of the to-do list.
+ /// Returns items of the to-do list.
+ /// To-do list was not found.
+ [HttpGet("{listId}/items", Name = nameof(GetListItemsAsync))]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task GetListItemsAsync([FromRoute] string listId)
+ {
+ // Check if list exists
+ if (!await _existenceChecker.ExistsAsync(listId))
+ return NotFound();
+
+ return Ok(await _todoItemsService.GetItemsOfListAsync(listId));
+ }
+
+ ///
+ /// Gets a to-do list item by its ID.
+ ///
+ /// ID of the to-do list which contans the item to obtain.
+ /// ID of the to-do list item to obtain.
+ /// Returns requested to-do list item.
+ /// To-do list item was not found.
+ [HttpGet("{listId}/items/{itemId}", Name = nameof(GetTodoItemByIdAsync))]
+ [ProducesResponseType(typeof(TodoItemGetByIdDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task GetTodoItemByIdAsync(
+ [FromRoute] string listId, [FromRoute] int itemId)
+ {
+ var item = await _todoItemsService.GetByIdAsync(itemId);
+ // Check if item exists and has valid to-do list ID
+ if (item == null || item.TodoListId != listId) return NotFound();
+
+ return Ok(item);
+ }
+
+ ///
+ /// Creates a new to-do list item.
+ ///
+ /// ID of the to-do list which will contain the item.
+ ///
+ /// Successfully created.
+ /// To-do list was not found.
+ [HttpPost("{listId}/items")]
+ [ProducesResponseType(typeof(TodoItemGetByIdDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task PostTodoItemAsync(
+ [FromRoute] string listId, [FromBody] TodoItemCreateDto dto)
+ {
+ // Check if list exists
+ if (!await _existenceChecker.ExistsAsync(listId))
+ return NotFound();
+
+ var item = await _todoItemsService.CreateAsync(listId, dto);
+ var routeValues = new { listId, itemId = item.Id };
+ var body = item.Adapt();
+ return CreatedAtRoute(nameof(GetTodoItemByIdAsync), routeValues, body);
+ }
+
+ ///
+ /// Updates a to-do list item.
+ ///
+ /// Success.
+ /// To-do list item was not found.
+ [HttpPut("{listId}/items/{itemId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task PutTodoItemAsync(
+ [FromRoute] string listId, [FromRoute] int itemId,
+ [FromBody] TodoItemCreateDto dto)
+ {
+ if (!await CheckListIdAsync(listId, itemId)) return NotFound();
+
+ bool result = await _todoItemsService.EditAsync(itemId, dto);
+ return result ? NoContent() : NotFound();
+ }
+
+ ///
+ /// Updates a to-do list item's state.
+ ///
+ /// Success.
+ /// To-do list item was not found.
+ [HttpPut("{listId}/items/{itemId}/state")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task PutTodoItemStateAsync(
+ [FromRoute] string listId, [FromRoute] int itemId,
+ [FromBody] TodoItemUpdateStateDto dto)
+ {
+ if (!await CheckListIdAsync(listId, itemId)) return NotFound();
+
+ bool result = await _todoItemsService.UpdateStateAsync(itemId, dto);
+ return result ? NoContent() : NotFound();
+ }
+
+ ///
+ /// Deletes a to-do list item.
+ ///
+ /// Success.
+ /// To-do list item was not found.
+ [HttpDelete("{listId}/items/{itemId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task DeleteTodoListAsync(
+ [FromRoute] string listId, [FromRoute] int itemId)
+ {
+ if (!await CheckListIdAsync(listId, itemId)) return NotFound();
+
+ bool result = await _todoItemsService.DeleteAsync(itemId);
+ return result ? NoContent() : NotFound();
+ }
+ #endregion
+}
diff --git a/AdvancedTodoList/Controllers/TodoListsController.cs b/AdvancedTodoList/Controllers/TodoListsController.cs
deleted file mode 100644
index a1bf7d8..0000000
--- a/AdvancedTodoList/Controllers/TodoListsController.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using AdvancedTodoList.Core.Dtos;
-using AdvancedTodoList.Core.Services;
-using Mapster;
-using Microsoft.AspNetCore.Mvc;
-
-namespace AdvancedTodoList.Controllers;
-
-[ApiController]
-[Route("api/todo")]
-public class TodoListsController(ITodoListsService todoListsService) : ControllerBase
-{
- private readonly ITodoListsService _todoListsService = todoListsService;
-
- ///
- /// Gets a to-do list by its ID.
- ///
- /// ID of the to-do list to obtain.
- /// Returns requested to-do list.
- /// To-do list was not found.
- [HttpGet("{listId}", Name = nameof(GetTodoListByIdAsync))]
- [ProducesResponseType(typeof(TodoListGetByIdDto), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task GetTodoListByIdAsync(string listId)
- {
- var list = await _todoListsService.GetByIdAsync(listId);
- return list != null ? Ok(list) : NotFound();
- }
-
- ///
- /// Creates a new to-do list.
- ///
- /// Successfully created.
- [HttpPost]
- [ProducesResponseType(typeof(TodoListGetByIdDto), StatusCodes.Status201Created)]
- public async Task PostTodoListAsync([FromBody] TodoListCreateDto dto)
- {
- var list = await _todoListsService.CreateAsync(dto);
- var routeValues = new { listId = list.Id };
- var body = list.Adapt();
- return CreatedAtRoute(nameof(GetTodoListByIdAsync), routeValues, body);
- }
-}
diff --git a/AdvancedTodoList/Program.cs b/AdvancedTodoList/Program.cs
index e628eac..2593df0 100644
--- a/AdvancedTodoList/Program.cs
+++ b/AdvancedTodoList/Program.cs
@@ -1,9 +1,13 @@
+using AdvancedTodoList.Core.Mapping;
using AdvancedTodoList.Core.Models;
using AdvancedTodoList.Core.Services;
+using AdvancedTodoList.Core.Validation;
using AdvancedTodoList.Infrastructure.Data;
using AdvancedTodoList.Infrastructure.Services;
+using FluentValidation;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
+using SharpGrip.FluentValidation.AutoValidation.Mvc.Extensions;
using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
@@ -42,6 +46,27 @@
// Register application services
builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+// Apply mapping settings
+MappingGlobalSettings.Apply();
+
+// Add fluent validation
+ValidatorOptions.Global.LanguageManager.Enabled = false; // Disable localization
+builder.Services.AddValidatorsFromAssemblyContaining();
+
+// Enable auto validation by SharpGrip
+builder.Services.AddFluentValidationAutoValidation(configuration =>
+{
+ // Disable the built-in .NET model (data annotations) validation.
+ configuration.DisableBuiltInModelValidation = true;
+ // Enable validation for parameters bound from `BindingSource.Body` binding sources.
+ configuration.EnableBodyBindingSourceAutomaticValidation = true;
+ // Enable validation for parameters bound from `BindingSource.Query` binding sources.
+ configuration.EnableQueryBindingSourceAutomaticValidation = true;
+});
+
var app = builder.Build();