diff --git a/AdvancedTodoList.Core/Dtos/TodoListDtos.cs b/AdvancedTodoList.Core/Dtos/TodoListDtos.cs
index e30e653..9627710 100644
--- a/AdvancedTodoList.Core/Dtos/TodoListDtos.cs
+++ b/AdvancedTodoList.Core/Dtos/TodoListDtos.cs
@@ -8,4 +8,4 @@ public record TodoListCreateDto(string Name, string Description);
///
/// DTO for a full view of a to-do list.
///
-public record TodoListGetByIdDto(string Id, string Name, string Description);
+public record TodoListGetByIdDto(string Id, string Name, string Description, ApplicationUserPreviewDto Owner);
diff --git a/AdvancedTodoList.Core/Dtos/TodoListItemDtos.cs b/AdvancedTodoList.Core/Dtos/TodoListItemDtos.cs
index e44c370..8491ed7 100644
--- a/AdvancedTodoList.Core/Dtos/TodoListItemDtos.cs
+++ b/AdvancedTodoList.Core/Dtos/TodoListItemDtos.cs
@@ -18,7 +18,7 @@ public record TodoItemUpdateStateDto(TodoItemState State);
public record TodoItemGetByIdDto(
int Id, string TodoListId, string Name,
string Description, DateTime? DeadlineDate,
- TodoItemState State
+ TodoItemState State, ApplicationUserPreviewDto Owner
);
///
diff --git a/AdvancedTodoList.Core/Models/TodoLists/Members/RolePermissions.cs b/AdvancedTodoList.Core/Models/TodoLists/Members/RolePermissions.cs
index edbcb9d..424e1d6 100644
--- a/AdvancedTodoList.Core/Models/TodoLists/Members/RolePermissions.cs
+++ b/AdvancedTodoList.Core/Models/TodoLists/Members/RolePermissions.cs
@@ -20,4 +20,10 @@ public record struct RolePermissions(
bool RemoveMembers = false,
bool AssignRoles = false,
bool EditRoles = false
- );
+ )
+{
+ ///
+ /// Instance of a structure with all permissions.
+ ///
+ public static readonly RolePermissions All = new(true, true, true, true, true, true, true, true);
+}
diff --git a/AdvancedTodoList.Core/Models/TodoLists/TodoItem.cs b/AdvancedTodoList.Core/Models/TodoLists/TodoItem.cs
index 262f1a2..a19dd58 100644
--- a/AdvancedTodoList.Core/Models/TodoLists/TodoItem.cs
+++ b/AdvancedTodoList.Core/Models/TodoLists/TodoItem.cs
@@ -1,4 +1,5 @@
-using System.ComponentModel.DataAnnotations;
+using AdvancedTodoList.Core.Models.Auth;
+using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AdvancedTodoList.Core.Models.TodoLists;
@@ -37,6 +38,20 @@ public class TodoItem : IEntity, ITodoListDependant
///
[ForeignKey(nameof(TodoList))]
public required string TodoListId { get; set; }
+ ///
+ /// Navigation property to the to-do list associated with this to-do item.
+ ///
+ public TodoList TodoList { get; set; } = null!;
+
+ ///
+ /// Foreign key referencing the user who created this item.
+ ///
+ [ForeignKey(nameof(Owner))]
+ public required string? OwnerId { get; set; } = null!;
+ ///
+ /// Navigation property to the user who created this item.
+ ///
+ public ApplicationUser? Owner { get; set; }
///
/// Maximum allowed length of .
@@ -46,11 +61,6 @@ public class TodoItem : IEntity, ITodoListDependant
/// 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!;
}
///
diff --git a/AdvancedTodoList.Core/Models/TodoLists/TodoList.cs b/AdvancedTodoList.Core/Models/TodoLists/TodoList.cs
index f115df4..b8b5dd8 100644
--- a/AdvancedTodoList.Core/Models/TodoLists/TodoList.cs
+++ b/AdvancedTodoList.Core/Models/TodoLists/TodoList.cs
@@ -1,4 +1,5 @@
-using System.ComponentModel.DataAnnotations;
+using AdvancedTodoList.Core.Models.Auth;
+using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AdvancedTodoList.Core.Models.TodoLists;
@@ -26,6 +27,16 @@ public class TodoList : IEntity
[MaxLength(DescriptionMaxLength)]
public required string Description { get; set; } = null!;
+ ///
+ /// Foreign key referencing the user who created this to-do list.
+ ///
+ [ForeignKey(nameof(Owner))]
+ public required string? OwnerId { get; set; } = null!;
+ ///
+ /// Navigation property to the user who created this to-do list.
+ ///
+ public ApplicationUser? Owner { get; set; }
+
///
/// Maximum allowed length of .
///
diff --git a/AdvancedTodoList.Core/Services/ITodoItemsService.cs b/AdvancedTodoList.Core/Services/ITodoItemsService.cs
index e32e3f8..84bcf9c 100644
--- a/AdvancedTodoList.Core/Services/ITodoItemsService.cs
+++ b/AdvancedTodoList.Core/Services/ITodoItemsService.cs
@@ -38,13 +38,14 @@ public interface ITodoItemsService
///
/// The ID of the to-do list to associate the item with.
/// The DTO containing information for creating the to-do list item.
+ /// ID of the user who creates the to-do list item.
///
/// A task representing the asynchronous operation.
/// The task result contains the created mapped to
/// or if to-do list with ID
/// does not exist.
///
- public Task CreateAsync(string todoListId, TodoItemCreateDto dto);
+ public Task CreateAsync(string todoListId, TodoItemCreateDto dto, string callerId);
///
/// Edits a to-do list item asynchronously.
diff --git a/AdvancedTodoList.Core/Services/ITodoListsService.cs b/AdvancedTodoList.Core/Services/ITodoListsService.cs
index b39d8b1..fd5762e 100644
--- a/AdvancedTodoList.Core/Services/ITodoListsService.cs
+++ b/AdvancedTodoList.Core/Services/ITodoListsService.cs
@@ -22,13 +22,17 @@ public interface ITodoListsService
///
/// Creates a new to-do list asynchronously.
///
+ ///
+ /// This method should also create an "Owner" role with all permissions and assign the caller to it.
+ ///
/// The DTO containing information for creating the to-do list.
+ /// ID of the user who creates the to-do list.
///
/// A task representing the asynchronous operation.
/// The task result contains the created mapped to
/// .
///
- public Task CreateAsync(TodoListCreateDto dto);
+ public Task CreateAsync(TodoListCreateDto dto, string callerId);
///
/// Edits a to-do list asynchronously.
diff --git a/AdvancedTodoList.Infrastructure/Migrations/20240302182741_AddListsAndItemsOwners.Designer.cs b/AdvancedTodoList.Infrastructure/Migrations/20240302182741_AddListsAndItemsOwners.Designer.cs
new file mode 100644
index 0000000..39a33fb
--- /dev/null
+++ b/AdvancedTodoList.Infrastructure/Migrations/20240302182741_AddListsAndItemsOwners.Designer.cs
@@ -0,0 +1,544 @@
+//
+using System;
+using System.Collections.Generic;
+using AdvancedTodoList.Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace AdvancedTodoList.Infrastructure.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20240302182741_AddListsAndItemsOwners")]
+ partial class AddListsAndItemsOwners
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("AdvancedTodoList.Core.Models.Auth.ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("int");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("FirstName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("bit");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("bit");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("bit");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex")
+ .HasFilter("[NormalizedUserName] IS NOT NULL");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("AdvancedTodoList.Core.Models.Auth.UserRefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Token")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ValidTo")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("UserRefreshTokens");
+ });
+
+ modelBuilder.Entity("AdvancedTodoList.Core.Models.TodoLists.Members.TodoListMember", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("RoleId")
+ .HasColumnType("int");
+
+ b.Property("TodoListId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.HasIndex("TodoListId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("TodoListsMembers");
+ });
+
+ modelBuilder.Entity("AdvancedTodoList.Core.Models.TodoLists.Members.TodoListRole", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Priority")
+ .HasColumnType("int");
+
+ b.Property("TodoListId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.ComplexProperty>("Permissions", "AdvancedTodoList.Core.Models.TodoLists.Members.TodoListRole.Permissions#RolePermissions", b1 =>
+ {
+ b1.Property("AddItems")
+ .HasColumnType("bit");
+
+ b1.Property("AddMembers")
+ .HasColumnType("bit");
+
+ b1.Property("AssignRoles")
+ .HasColumnType("bit");
+
+ b1.Property("DeleteItems")
+ .HasColumnType("bit");
+
+ b1.Property("EditItems")
+ .HasColumnType("bit");
+
+ b1.Property("EditRoles")
+ .HasColumnType("bit");
+
+ b1.Property("RemoveMembers")
+ .HasColumnType("bit");
+
+ b1.Property("SetItemsState")
+ .HasColumnType("bit");
+ });
+
+ b.HasKey("Id");
+
+ b.HasIndex("TodoListId");
+
+ b.ToTable("TodoListRoles");
+ });
+
+ modelBuilder.Entity("AdvancedTodoList.Core.Models.TodoLists.TodoItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("DeadlineDate")
+ .HasColumnType("datetime2");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("OwnerId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("State")
+ .HasColumnType("tinyint");
+
+ b.Property("TodoListId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.HasIndex("TodoListId");
+
+ b.ToTable("TodoItems");
+ });
+
+ modelBuilder.Entity("AdvancedTodoList.Core.Models.TodoLists.TodoList", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(25000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("OwnerId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("TodoLists");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex")
+ .HasFilter("[NormalizedName] IS NOT NULL");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ClaimValue")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderKey")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("RoleId")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("LoginProvider")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Value")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("AdvancedTodoList.Core.Models.Auth.UserRefreshToken", b =>
+ {
+ b.HasOne("AdvancedTodoList.Core.Models.Auth.ApplicationUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("AdvancedTodoList.Core.Models.TodoLists.Members.TodoListMember", b =>
+ {
+ b.HasOne("AdvancedTodoList.Core.Models.TodoLists.Members.TodoListRole", "Role")
+ .WithMany()
+ .HasForeignKey("RoleId");
+
+ b.HasOne("AdvancedTodoList.Core.Models.TodoLists.TodoList", "TodoList")
+ .WithMany()
+ .HasForeignKey("TodoListId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("AdvancedTodoList.Core.Models.Auth.ApplicationUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Role");
+
+ b.Navigation("TodoList");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("AdvancedTodoList.Core.Models.TodoLists.Members.TodoListRole", b =>
+ {
+ b.HasOne("AdvancedTodoList.Core.Models.TodoLists.TodoList", "TodoList")
+ .WithMany()
+ .HasForeignKey("TodoListId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("TodoList");
+ });
+
+ modelBuilder.Entity("AdvancedTodoList.Core.Models.TodoLists.TodoItem", b =>
+ {
+ b.HasOne("AdvancedTodoList.Core.Models.Auth.ApplicationUser", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId");
+
+ b.HasOne("AdvancedTodoList.Core.Models.TodoLists.TodoList", "TodoList")
+ .WithMany("TodoItems")
+ .HasForeignKey("TodoListId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Owner");
+
+ b.Navigation("TodoList");
+ });
+
+ modelBuilder.Entity("AdvancedTodoList.Core.Models.TodoLists.TodoList", b =>
+ {
+ b.HasOne("AdvancedTodoList.Core.Models.Auth.ApplicationUser", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("AdvancedTodoList.Core.Models.Auth.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("AdvancedTodoList.Core.Models.Auth.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("AdvancedTodoList.Core.Models.Auth.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("AdvancedTodoList.Core.Models.Auth.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("AdvancedTodoList.Core.Models.TodoLists.TodoList", b =>
+ {
+ b.Navigation("TodoItems");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/AdvancedTodoList.Infrastructure/Migrations/20240302182741_AddListsAndItemsOwners.cs b/AdvancedTodoList.Infrastructure/Migrations/20240302182741_AddListsAndItemsOwners.cs
new file mode 100644
index 0000000..4185255
--- /dev/null
+++ b/AdvancedTodoList.Infrastructure/Migrations/20240302182741_AddListsAndItemsOwners.cs
@@ -0,0 +1,78 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace AdvancedTodoList.Infrastructure.Migrations
+{
+ ///
+ public partial class AddListsAndItemsOwners : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "OwnerId",
+ table: "TodoLists",
+ type: "nvarchar(450)",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "OwnerId",
+ table: "TodoItems",
+ type: "nvarchar(450)",
+ nullable: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_TodoLists_OwnerId",
+ table: "TodoLists",
+ column: "OwnerId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_TodoItems_OwnerId",
+ table: "TodoItems",
+ column: "OwnerId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_TodoItems_AspNetUsers_OwnerId",
+ table: "TodoItems",
+ column: "OwnerId",
+ principalTable: "AspNetUsers",
+ principalColumn: "Id");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_TodoLists_AspNetUsers_OwnerId",
+ table: "TodoLists",
+ column: "OwnerId",
+ principalTable: "AspNetUsers",
+ principalColumn: "Id");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_TodoItems_AspNetUsers_OwnerId",
+ table: "TodoItems");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_TodoLists_AspNetUsers_OwnerId",
+ table: "TodoLists");
+
+ migrationBuilder.DropIndex(
+ name: "IX_TodoLists_OwnerId",
+ table: "TodoLists");
+
+ migrationBuilder.DropIndex(
+ name: "IX_TodoItems_OwnerId",
+ table: "TodoItems");
+
+ migrationBuilder.DropColumn(
+ name: "OwnerId",
+ table: "TodoLists");
+
+ migrationBuilder.DropColumn(
+ name: "OwnerId",
+ table: "TodoItems");
+ }
+ }
+}
diff --git a/AdvancedTodoList.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/AdvancedTodoList.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
index 74abd52..6f44eb9 100644
--- a/AdvancedTodoList.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
+++ b/AdvancedTodoList.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -229,6 +229,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
+ b.Property("OwnerId")
+ .HasColumnType("nvarchar(450)");
+
b.Property("State")
.HasColumnType("tinyint");
@@ -238,6 +241,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasKey("Id");
+ b.HasIndex("OwnerId");
+
b.HasIndex("TodoListId");
b.ToTable("TodoItems");
@@ -259,8 +264,13 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
+ b.Property("OwnerId")
+ .HasColumnType("nvarchar(450)");
+
b.HasKey("Id");
+ b.HasIndex("OwnerId");
+
b.ToTable("TodoLists");
});
@@ -446,15 +456,30 @@ protected override void BuildModel(ModelBuilder modelBuilder)
modelBuilder.Entity("AdvancedTodoList.Core.Models.TodoLists.TodoItem", b =>
{
+ b.HasOne("AdvancedTodoList.Core.Models.Auth.ApplicationUser", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId");
+
b.HasOne("AdvancedTodoList.Core.Models.TodoLists.TodoList", "TodoList")
.WithMany("TodoItems")
.HasForeignKey("TodoListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
+ b.Navigation("Owner");
+
b.Navigation("TodoList");
});
+ modelBuilder.Entity("AdvancedTodoList.Core.Models.TodoLists.TodoList", b =>
+ {
+ b.HasOne("AdvancedTodoList.Core.Models.Auth.ApplicationUser", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId");
+
+ b.Navigation("Owner");
+ });
+
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
diff --git a/AdvancedTodoList.Infrastructure/Services/TodoItemsService.cs b/AdvancedTodoList.Infrastructure/Services/TodoItemsService.cs
index c1f3aca..c09f521 100644
--- a/AdvancedTodoList.Infrastructure/Services/TodoItemsService.cs
+++ b/AdvancedTodoList.Infrastructure/Services/TodoItemsService.cs
@@ -1,17 +1,25 @@
using AdvancedTodoList.Core.Dtos;
using AdvancedTodoList.Core.Models.TodoLists;
using AdvancedTodoList.Core.Pagination;
+using AdvancedTodoList.Core.Repositories;
using AdvancedTodoList.Core.Services;
using AdvancedTodoList.Infrastructure.Specifications;
+using Mapster;
namespace AdvancedTodoList.Infrastructure.Services;
///
/// A service that manages to-do lists items.
///
-public class TodoItemsService(ITodoListDependantEntitiesService helperService) : ITodoItemsService
+public class TodoItemsService(
+ ITodoListDependantEntitiesService helperService,
+ IRepository repository,
+ IEntityExistenceChecker existenceChecker
+ ) : ITodoItemsService
{
private readonly ITodoListDependantEntitiesService _helperService = helperService;
+ private readonly IRepository _repository = repository;
+ private readonly IEntityExistenceChecker _existenceChecker = existenceChecker;
///
/// Retrieves a page of to-do list items of the list with the specified ID.
@@ -39,9 +47,15 @@ public class TodoItemsService(ITodoListDependantEntitiesService h
/// a object if the specified ID is found;
/// otherwise, returns .
///
- public Task GetByIdAsync(string todoListId, int itemId)
+ public async Task GetByIdAsync(string todoListId, int itemId)
{
- return _helperService.GetByIdAsync(todoListId, itemId);
+ TodoItemAggregateSpecification specification = new(itemId);
+ // Get the aggregate
+ var dto = await _repository.GetAggregateAsync(specification);
+ // Check if it's valid
+ if (dto == null || dto.TodoListId != todoListId) return null;
+
+ return dto;
}
///
@@ -55,9 +69,22 @@ public class TodoItemsService(ITodoListDependantEntitiesService h
/// or if to-do list with ID
/// does not exist.
///
- public Task CreateAsync(string todoListId, TodoItemCreateDto dto)
+ public async Task CreateAsync(string todoListId, TodoItemCreateDto dto, string callerId)
{
- return _helperService.CreateAsync(todoListId, dto);
+ // Check if to-do list exists
+ if (!await _existenceChecker.ExistsAsync(todoListId))
+ return null;
+
+ // Create the item
+ var todoItem = dto.Adapt();
+ // Set the foreign key
+ todoItem.TodoListId = todoListId;
+ // Set the owner
+ todoItem.OwnerId = callerId;
+ // Save it
+ await _repository.AddAsync(todoItem);
+ // Map it to DTO and return
+ return todoItem.Adapt();
}
///
diff --git a/AdvancedTodoList.Infrastructure/Services/TodoListsService.cs b/AdvancedTodoList.Infrastructure/Services/TodoListsService.cs
index ff3614f..aa3a304 100644
--- a/AdvancedTodoList.Infrastructure/Services/TodoListsService.cs
+++ b/AdvancedTodoList.Infrastructure/Services/TodoListsService.cs
@@ -1,7 +1,9 @@
using AdvancedTodoList.Core.Dtos;
using AdvancedTodoList.Core.Models.TodoLists;
+using AdvancedTodoList.Core.Models.TodoLists.Members;
using AdvancedTodoList.Core.Repositories;
using AdvancedTodoList.Core.Services;
+using AdvancedTodoList.Infrastructure.Specifications;
using Mapster;
namespace AdvancedTodoList.Infrastructure.Services;
@@ -9,9 +11,17 @@ namespace AdvancedTodoList.Infrastructure.Services;
///
/// A service that manages to-do lists.
///
-public class TodoListsService(IRepository repository) : ITodoListsService
+public class TodoListsService(
+ IRepository todoListsRepository,
+ IRepository rolesRepository,
+ ITodoListMembersRepository membersRepository,
+ IUnitOfWork unitOfWork
+ ) : ITodoListsService
{
- private readonly IRepository _repository = repository;
+ private readonly IRepository _todoListsRepository = todoListsRepository;
+ private readonly IRepository _rolesRepository = rolesRepository;
+ private readonly IRepository _membersRepository = membersRepository;
+ private readonly IUnitOfWork _unitOfWork = unitOfWork;
///
/// Retrieves a to-do list by its ID asynchronously.
@@ -22,29 +32,69 @@ public class TodoListsService(IRepository repository) : ITodoL
/// a object if the specified ID is found;
/// otherwise, returns .
///
- public async Task GetByIdAsync(string id)
+ public Task GetByIdAsync(string id)
{
- var todoList = await _repository.GetByIdAsync(id);
- if (todoList == null) return null;
-
- return todoList.Adapt();
+ TodoListAggregateSpecification specification = new(id);
+ return _todoListsRepository.GetAggregateAsync(specification);
}
///
/// Creates a new to-do list asynchronously.
///
+ ///
+ /// This method also creates an "Owner" role with all permissions and assigns the caller to it.
+ ///
/// The DTO containing information for creating the to-do list.
+ /// ID of the user who creates the to-do list.
///
/// A task representing the asynchronous operation.
/// The task result contains the created mapped to
/// .
///
- public async Task CreateAsync(TodoListCreateDto dto)
+ public async Task CreateAsync(TodoListCreateDto dto, string callerId)
{
- // Map DTO to model
+ // Map DTO to the model
var todoList = dto.Adapt();
- // Add model to the database
- await _repository.AddAsync(todoList);
+ // Set the owner
+ todoList.OwnerId = callerId;
+
+ // Begin a transaction
+ await _unitOfWork.BeginTransactionAsync();
+
+ try
+ {
+ // Add the list to the database
+ await _todoListsRepository.AddAsync(todoList);
+
+ // Create an "Owner" role
+ TodoListRole ownerRole = new()
+ {
+ Name = "Owner",
+ Priority = 0,
+ TodoListId = todoList.Id,
+ Permissions = RolePermissions.All
+ };
+ await _rolesRepository.AddAsync(ownerRole);
+
+ // Assign the caller to it
+ TodoListMember member = new()
+ {
+ UserId = callerId,
+ TodoListId = todoList.Id,
+ RoleId = ownerRole.Id
+ };
+ await _membersRepository.AddAsync(member);
+ }
+ catch (Exception)
+ {
+ // Rollback in a case of error
+ await _unitOfWork.RollbackAsync();
+ throw;
+ }
+
+ // Commit changes
+ await _unitOfWork.CommitAsync();
+
// Return DTO of created model
return todoList.Adapt();
}
@@ -62,13 +112,13 @@ public async Task CreateAsync(TodoListCreateDto dto)
public async Task EditAsync(string id, TodoListCreateDto dto)
{
// Get the model
- var todoList = await _repository.GetByIdAsync(id);
+ var todoList = await _todoListsRepository.GetByIdAsync(id);
// Return false if the model doesn't exist
if (todoList == null) return false;
// Update the model
dto.Adapt(todoList);
- await _repository.UpdateAsync(todoList);
+ await _todoListsRepository.UpdateAsync(todoList);
return true;
}
@@ -85,12 +135,12 @@ public async Task EditAsync(string id, TodoListCreateDto dto)
public async Task DeleteAsync(string id)
{
// Get the model
- var todoList = await _repository.GetByIdAsync(id);
+ var todoList = await _todoListsRepository.GetByIdAsync(id);
// Return false if the model doesn't exist
if (todoList == null) return false;
// Delete the model
- await _repository.DeleteAsync(todoList);
+ await _todoListsRepository.DeleteAsync(todoList);
return true;
}
diff --git a/AdvancedTodoList.Infrastructure/Specifications/GetByIdSpecification.cs b/AdvancedTodoList.Infrastructure/Specifications/GetByIdSpecification.cs
index c6bd4ff..1a0c20c 100644
--- a/AdvancedTodoList.Infrastructure/Specifications/GetByIdSpecification.cs
+++ b/AdvancedTodoList.Infrastructure/Specifications/GetByIdSpecification.cs
@@ -17,7 +17,7 @@ public class GetByIdSpecification(TKey id) : ISpecification
/// The unique identifier to filter by.
///
- protected TKey Id { get; } = id;
+ public TKey Id { get; } = id;
///
/// Gets the criteria expression that defines the filtering conditions.
diff --git a/AdvancedTodoList.Infrastructure/Specifications/TodoItemAggregateSpecification.cs b/AdvancedTodoList.Infrastructure/Specifications/TodoItemAggregateSpecification.cs
new file mode 100644
index 0000000..a8b80f3
--- /dev/null
+++ b/AdvancedTodoList.Infrastructure/Specifications/TodoItemAggregateSpecification.cs
@@ -0,0 +1,19 @@
+using AdvancedTodoList.Core.Models.TodoLists;
+using System.Linq.Expressions;
+
+namespace AdvancedTodoList.Infrastructure.Specifications;
+
+///
+/// Represents a specification used for obtaining a to-do list item aggregate.
+///
+/// The unique identifier of the to-do list item to obtain.
+public class TodoItemAggregateSpecification(int id) : GetByIdSpecification(id)
+{
+ ///
+ /// Gets the list of include expressions specifying an owner.
+ ///
+ public override List>> Includes =>
+ [
+ x => x.Owner
+ ];
+}
diff --git a/AdvancedTodoList.Infrastructure/Specifications/TodoListAggregateSpecification.cs b/AdvancedTodoList.Infrastructure/Specifications/TodoListAggregateSpecification.cs
new file mode 100644
index 0000000..5aab3e5
--- /dev/null
+++ b/AdvancedTodoList.Infrastructure/Specifications/TodoListAggregateSpecification.cs
@@ -0,0 +1,19 @@
+using AdvancedTodoList.Core.Models.TodoLists;
+using System.Linq.Expressions;
+
+namespace AdvancedTodoList.Infrastructure.Specifications;
+
+///
+/// Represents a specification used for obtaining a to-do list aggregate.
+///
+/// The unique identifier of the to-do list to obtain.
+public class TodoListAggregateSpecification(string id) : GetByIdSpecification(id)
+{
+ ///
+ /// Gets the list of include expressions specifying an owner.
+ ///
+ public override List>> Includes =>
+ [
+ x => x.Owner
+ ];
+}
diff --git a/AdvancedTodoList.IntegrationTests/Endpoints/TodoItemsEndpointsTests.cs b/AdvancedTodoList.IntegrationTests/Endpoints/TodoItemsEndpointsTests.cs
index b91afbc..5c3129f 100644
--- a/AdvancedTodoList.IntegrationTests/Endpoints/TodoItemsEndpointsTests.cs
+++ b/AdvancedTodoList.IntegrationTests/Endpoints/TodoItemsEndpointsTests.cs
@@ -101,7 +101,8 @@ public async Task GetTodoItemById_ElementExists_ReturnsElement()
// Arrange
string testListId = "TestId";
int testItemId = 777;
- TodoItemGetByIdDto testDto = new(testItemId, testListId, "Test todo item", "...", null, TodoItemState.Active);
+ TodoItemGetByIdDto testDto = new(testItemId, testListId, "Test todo item",
+ "...", null, TodoItemState.Active, new("Id", "User"));
WebApplicationFactory.TodoItemsService
.GetByIdAsync(testListId, testItemId)
@@ -158,8 +159,9 @@ public async Task PostTodoItem_ValidCall_Succeeds()
string listId = "ListId";
TodoItemCreateDto dto = new("Item", "...", DateTime.MaxValue);
WebApplicationFactory.TodoItemsService
- .CreateAsync(listId, dto)
- .Returns(new TodoItemGetByIdDto(500, "TodoListId", "", "", DateTime.UtcNow, TodoItemState.Active));
+ .CreateAsync(listId, dto, TestUserId)
+ .Returns(new TodoItemGetByIdDto(500, "TodoListId", "", "", DateTime.UtcNow,
+ TodoItemState.Active, new("Id", "User")));
using HttpClient client = CreateAuthorizedHttpClient();
// Act: send the request
@@ -170,7 +172,7 @@ public async Task PostTodoItem_ValidCall_Succeeds()
// Assert that create method was called
await WebApplicationFactory.TodoItemsService
.Received()
- .CreateAsync(listId, dto);
+ .CreateAsync(listId, dto, TestUserId);
}
[Test]
@@ -180,7 +182,7 @@ public async Task PostTodoItem_TodoListDoesNotExist_Returns404()
string listId = "ListId";
TodoItemCreateDto dto = new("Item", "...", DateTime.MaxValue);
WebApplicationFactory.TodoItemsService
- .CreateAsync(listId, dto)
+ .CreateAsync(listId, dto, TestUserId)
.ReturnsNull();
using HttpClient client = CreateAuthorizedHttpClient();
diff --git a/AdvancedTodoList.IntegrationTests/Endpoints/TodoListsEndpointsTests.cs b/AdvancedTodoList.IntegrationTests/Endpoints/TodoListsEndpointsTests.cs
index 2775dd7..e4c5917 100644
--- a/AdvancedTodoList.IntegrationTests/Endpoints/TodoListsEndpointsTests.cs
+++ b/AdvancedTodoList.IntegrationTests/Endpoints/TodoListsEndpointsTests.cs
@@ -13,7 +13,7 @@ public async Task GetTodoListById_ElementExists_ReturnsElement()
{
// Arrange
string testId = "TestId";
- TodoListGetByIdDto testDto = new(testId, "Test todo list", "");
+ TodoListGetByIdDto testDto = new(testId, "Test todo list", "", new("Id", "User"));
WebApplicationFactory.TodoListsService
.GetByIdAsync(testId)
.Returns(testDto);
@@ -66,8 +66,8 @@ public async Task PostTodoList_ValidCall_Succeeds()
{
// Arrange
WebApplicationFactory.TodoListsService
- .CreateAsync(Arg.Any())
- .Returns(new TodoListGetByIdDto("Id", "", ""));
+ .CreateAsync(Arg.Any(), Arg.Any())
+ .Returns(new TodoListGetByIdDto("Id", "", "", new("Id", "User")));
TodoListCreateDto dto = new("Test", string.Empty);
using HttpClient client = CreateAuthorizedHttpClient();
@@ -79,7 +79,7 @@ public async Task PostTodoList_ValidCall_Succeeds()
// Assert that create method was called
await WebApplicationFactory.TodoListsService
.Received()
- .CreateAsync(dto);
+ .CreateAsync(dto, TestUserId);
}
[Test]
diff --git a/AdvancedTodoList.IntegrationTests/Factories/BusinessLogicWebApplicationFactory.cs b/AdvancedTodoList.IntegrationTests/Factories/BusinessLogicWebApplicationFactory.cs
index a7fd930..e6779a4 100644
--- a/AdvancedTodoList.IntegrationTests/Factories/BusinessLogicWebApplicationFactory.cs
+++ b/AdvancedTodoList.IntegrationTests/Factories/BusinessLogicWebApplicationFactory.cs
@@ -22,7 +22,9 @@ public class BusinessLogicWebApplicationFactory : WebApplicationFactory
public ITodoListDependantEntitiesService TodoRolesHelperService { get; set; } = null!;
public ITodoListDependantEntitiesService TodoMembersHelperService { get; set; } = null!;
public IUserRefreshTokensRepository RefreshTokensRepository { get; private set; } = null!;
+ public IRepository TodoListRolesRepository { get; private set; } = null!;
public ITodoListMembersRepository TodoListMembersRepository { get; private set; } = null!;
+ public IUnitOfWork UnitOfWork { get; private set; } = null!;
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
@@ -35,8 +37,14 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
TodoRolesHelperService = Substitute.For>();
TodoMembersHelperService = Substitute.For>();
RefreshTokensRepository = Substitute.For();
+ TodoListRolesRepository = Substitute.For>();
TodoListMembersRepository = Substitute.For();
+ UnitOfWork = Substitute.For();
+ UnitOfWork.BeginTransactionAsync().Returns(Task.CompletedTask);
+ UnitOfWork.CommitAsync().Returns(Task.CompletedTask);
+ UnitOfWork.RollbackAsync().Returns(Task.CompletedTask);
+
builder.ConfigureTestServices(services =>
{
services.AddScoped(_ => EntityExistenceChecker);
@@ -47,7 +55,9 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
services.AddScoped(_ => TodoRolesHelperService);
services.AddScoped(_ => TodoMembersHelperService);
services.AddScoped(_ => RefreshTokensRepository);
+ services.AddScoped(_ => TodoListRolesRepository);
services.AddScoped(_ => TodoListMembersRepository);
+ services.AddScoped(_ => UnitOfWork);
});
}
}
diff --git a/AdvancedTodoList.IntegrationTests/Repositories/TodoItemsRepositoryTests.cs b/AdvancedTodoList.IntegrationTests/Repositories/TodoItemsRepositoryTests.cs
index ed6d7c2..25736ec 100644
--- a/AdvancedTodoList.IntegrationTests/Repositories/TodoItemsRepositoryTests.cs
+++ b/AdvancedTodoList.IntegrationTests/Repositories/TodoItemsRepositoryTests.cs
@@ -1,4 +1,6 @@
-using AdvancedTodoList.Core.Models.TodoLists;
+using AdvancedTodoList.Core.Dtos;
+using AdvancedTodoList.Core.Models.TodoLists;
+using AdvancedTodoList.Infrastructure.Specifications;
using AdvancedTodoList.IntegrationTests.Utils;
namespace AdvancedTodoList.IntegrationTests.Repositories;
@@ -38,4 +40,28 @@ protected override async Task CreateTestEntityAsync()
await DbContext.SaveChangesAsync();
return TestModels.CreateTestTodoItem(todoList.Id);
}
+
+ [Test]
+ public async Task GetAggregateAsync_TodoListAggregateSpecification_IncludesOwner()
+ {
+ // Arrange
+ var owner = TestModels.CreateTestUser();
+ DbContext.Add(owner);
+ await DbContext.SaveChangesAsync();
+ var todoList = TestModels.CreateTestTodoList();
+ DbContext.Add(todoList);
+ await DbContext.SaveChangesAsync();
+ var todoItem = TestModels.CreateTestTodoItem(todoList.Id, owner.Id);
+ DbContext.Add(todoItem);
+ await DbContext.SaveChangesAsync();
+ TodoItemAggregateSpecification specification = new(todoItem.Id);
+
+ // Act
+ var aggregate = await Repository.GetAggregateAsync(specification);
+
+ // Assert
+ Assert.That(aggregate, Is.Not.Null);
+ Assert.That(aggregate.Owner, Is.Not.Null);
+ Assert.That(aggregate.Owner.Id, Is.EqualTo(owner.Id));
+ }
}
diff --git a/AdvancedTodoList.IntegrationTests/Repositories/TodoListRepositoryTests.cs b/AdvancedTodoList.IntegrationTests/Repositories/TodoListRepositoryTests.cs
index a667f2e..1ddbbcd 100644
--- a/AdvancedTodoList.IntegrationTests/Repositories/TodoListRepositoryTests.cs
+++ b/AdvancedTodoList.IntegrationTests/Repositories/TodoListRepositoryTests.cs
@@ -1,4 +1,6 @@
-using AdvancedTodoList.Core.Models.TodoLists;
+using AdvancedTodoList.Core.Dtos;
+using AdvancedTodoList.Core.Models.TodoLists;
+using AdvancedTodoList.Infrastructure.Specifications;
using AdvancedTodoList.IntegrationTests.Utils;
namespace AdvancedTodoList.IntegrationTests.Repositories;
@@ -30,4 +32,25 @@ protected override Task CreateTestEntityAsync()
{
return Task.FromResult(TestModels.CreateTestTodoList());
}
+
+ [Test]
+ public async Task GetAggregateAsync_TodoListAggregateSpecification_IncludesOwner()
+ {
+ // Arrange
+ var owner = TestModels.CreateTestUser();
+ DbContext.Add(owner);
+ await DbContext.SaveChangesAsync();
+ var todoList = TestModels.CreateTestTodoList(owner.Id);
+ DbContext.Add(todoList);
+ await DbContext.SaveChangesAsync();
+ TodoListAggregateSpecification specification = new(todoList.Id);
+
+ // Act
+ var aggregate = await Repository.GetAggregateAsync(specification);
+
+ // Assert
+ Assert.That(aggregate, Is.Not.Null);
+ Assert.That(aggregate.Owner, Is.Not.Null);
+ Assert.That(aggregate.Owner.Id, Is.EqualTo(owner.Id));
+ }
}
diff --git a/AdvancedTodoList.IntegrationTests/Services/TodoItemsServiceTests.cs b/AdvancedTodoList.IntegrationTests/Services/TodoItemsServiceTests.cs
index 7bbcb3a..82ca82d 100644
--- a/AdvancedTodoList.IntegrationTests/Services/TodoItemsServiceTests.cs
+++ b/AdvancedTodoList.IntegrationTests/Services/TodoItemsServiceTests.cs
@@ -18,6 +18,64 @@ public void SetUp()
_service = ServiceScope.ServiceProvider.GetService()!;
}
+ [Test]
+ public async Task GetByIdAsync_EntityExists_AppliesTodoListAggregateSpecification()
+ {
+ // Arrange
+ int todoItemId = 123;
+ string todoListId = "TodoListId";
+ TodoItemGetByIdDto dto = new(todoItemId, todoListId, "Name", "Description", null,
+ TodoItemState.Active, new("User", "Name"));
+ WebApplicationFactory.TodoItemsRepository
+ .GetAggregateAsync(Arg.Any>())
+ .Returns(dto);
+
+ // Act
+ var result = await _service.GetByIdAsync(todoListId, todoItemId);
+
+ // Assert that valid specification was applied
+ await WebApplicationFactory.TodoItemsRepository
+ .Received()
+ .GetAggregateAsync(
+ Arg.Is(x => x.Id == todoItemId));
+ }
+
+ [Test]
+ public async Task GetByIdAsync_InvalidTodoListId_ReturnsNull()
+ {
+ // Arrange
+ int todoItemId = 123;
+ string todoListId = "TodoListId";
+ TodoItemGetByIdDto dto = new(todoItemId, todoListId, "Name", "Description", null,
+ TodoItemState.Active, new("User", "Name"));
+ WebApplicationFactory.TodoItemsRepository
+ .GetAggregateAsync(Arg.Any>())
+ .Returns(dto);
+
+ // Act
+ var result = await _service.GetByIdAsync("Wrong list ID", todoItemId);
+
+ // Assert
+ Assert.That(result, Is.Null);
+ }
+
+ [Test]
+ public async Task GetByIdAsync_EntityDoesNotExist_ReturnsNull()
+ {
+ // Arrange
+ string todoListId = "ID";
+ int itemId = 123;
+ WebApplicationFactory.TodoItemsRepository
+ .GetAggregateAsync(Arg.Any>())
+ .ReturnsNull();
+
+ // Act
+ var result = await _service.GetByIdAsync(todoListId, itemId);
+
+ // Assert
+ Assert.That(result, Is.Null);
+ }
+
[Test]
public async Task GetItemsOfListAsync_ListExists_AppliesTodoItemsSpecification()
{
@@ -40,5 +98,48 @@ await WebApplicationFactory.TodoItemsHelperService
Arg.Any());
}
+ [Test]
+ public async Task CreateAsync_TodoListExists_AddsEntityToDb()
+ {
+ // Arrange: initialize a DTO
+ string todoListId = "ID";
+ string callerId = "CallerId";
+ TodoItemCreateDto dto = new("Name", "Description", DateTime.UtcNow);
+ WebApplicationFactory.EntityExistenceChecker
+ .ExistsAsync(todoListId)
+ .Returns(true);
+ WebApplicationFactory.TodoItemsRepository
+ .AddAsync(Arg.Any())
+ .Returns(Task.FromResult);
+
+ // Act: call the method
+ var result = await _service.CreateAsync(todoListId, dto, callerId);
+
+ // Assert
+ Assert.That(result, Is.Not.Null);
+ await WebApplicationFactory.TodoItemsRepository
+ .Received()
+ .AddAsync(Arg.Is(x =>
+ x.Name == dto.Name && x.Description == dto.Description && x.OwnerId == callerId));
+ }
+
+ [Test]
+ public async Task CreateAsync_TodoListDoesNotExist_ReturnsNull()
+ {
+ // Arrange: initialize a DTO
+ string todoListId = "ID";
+ string callerId = "CallerId";
+ TodoItemCreateDto dto = new("Name", "Description", DateTime.UtcNow);
+ WebApplicationFactory.EntityExistenceChecker
+ .ExistsAsync(todoListId)
+ .Returns(false);
+
+ // Act: call the method
+ var result = await _service.CreateAsync(todoListId, dto, callerId);
+
+ // Assert
+ Assert.That(result, Is.Null);
+ }
+
// Tests for other methods are useless, because they are just wrappers.
}
diff --git a/AdvancedTodoList.IntegrationTests/Services/TodoListsServiceTests.cs b/AdvancedTodoList.IntegrationTests/Services/TodoListsServiceTests.cs
index eced50d..e0b9d56 100644
--- a/AdvancedTodoList.IntegrationTests/Services/TodoListsServiceTests.cs
+++ b/AdvancedTodoList.IntegrationTests/Services/TodoListsServiceTests.cs
@@ -1,9 +1,12 @@
using AdvancedTodoList.Core.Dtos;
using AdvancedTodoList.Core.Models.TodoLists;
+using AdvancedTodoList.Core.Models.TodoLists.Members;
using AdvancedTodoList.Core.Services;
+using AdvancedTodoList.Core.Specifications;
+using AdvancedTodoList.Infrastructure.Specifications;
using AdvancedTodoList.IntegrationTests.Fixtures;
using AdvancedTodoList.IntegrationTests.Utils;
-using Mapster;
+using NSubstitute.ExceptionExtensions;
namespace AdvancedTodoList.IntegrationTests.Services;
@@ -19,20 +22,23 @@ public void SetUp()
}
[Test]
- public async Task GetByIdAsync_EntityExists_ReturnsCorrectEntity()
+ public async Task GetByIdAsync_EntityExists_AppliesTodoListAggregateSpecification()
{
// Arrange
- TodoList todoList = TestModels.CreateTestTodoList();
+ string todoListId = "TodoListId";
+ TodoListGetByIdDto dto = new("Id", "name", "", new("Id", "User"));
WebApplicationFactory.TodoListsRepository
- .GetByIdAsync(todoList.Id)
- .Returns(todoList);
+ .GetAggregateAsync(Arg.Any>())
+ .Returns(dto);
// Act
- var result = await _service.GetByIdAsync(todoList.Id);
+ var result = await _service.GetByIdAsync(todoListId);
- // Assert that returned DTO matches
- var expectedResult = todoList.Adapt();
- Assert.That(result, Is.EqualTo(expectedResult));
+ // Assert that valid specification was applied
+ await WebApplicationFactory.TodoListsRepository
+ .Received()
+ .GetAggregateAsync(
+ Arg.Is(x => x.Id == todoListId));
}
[Test]
@@ -41,7 +47,7 @@ public async Task GetByIdAsync_EntityDoesNotExist_ReturnsNull()
// Arrange
string todoListId = "ID";
WebApplicationFactory.TodoListsRepository
- .GetByIdAsync(todoListId)
+ .GetAggregateAsync(Arg.Any>())
.ReturnsNull();
// Act
@@ -54,19 +60,65 @@ public async Task GetByIdAsync_EntityDoesNotExist_ReturnsNull()
[Test]
public async Task CreateAsync_AddsEntityToDb()
{
- // Arrange: initialize a DTO
+ // Arrange
+ string callerId = "CallerId";
+ string listId = "ListId";
+ int ownerRoleId = 777;
TodoListCreateDto dto = new("Test entity", "...");
WebApplicationFactory.TodoListsRepository
.AddAsync(Arg.Any())
+ .Returns(Task.FromResult)
+ .AndDoes(x => ((TodoList)x[0]).Id = listId);
+ WebApplicationFactory.TodoListRolesRepository
+ .AddAsync(Arg.Any())
+ .Returns(Task.FromResult)
+ .AndDoes(x => ((TodoListRole)x[0]).Id = ownerRoleId);
+ WebApplicationFactory.TodoListMembersRepository
+ .AddAsync(Arg.Any())
.Returns(Task.FromResult);
- // Act: call the method
- var result = await _service.CreateAsync(dto);
+ // Act
+ var result = await _service.CreateAsync(dto, callerId);
- // Assert that method was called
+ // Assert that list was created
await WebApplicationFactory.TodoListsRepository
.Received()
- .AddAsync(Arg.Is(x => x.Name == dto.Name && x.Description == dto.Description));
+ .AddAsync(Arg.Is(x => x.Name == dto.Name &&
+ x.Description == dto.Description && x.OwnerId == callerId));
+ // Assert that "Owner" role was created
+ await WebApplicationFactory.TodoListRolesRepository
+ .Received()
+ .AddAsync(Arg.Is(x => x.Priority == 0 &&
+ x.TodoListId == listId && x.Permissions == RolePermissions.All));
+ // Assert that the caller was assigned to the "Owner" role
+ await WebApplicationFactory.TodoListMembersRepository
+ .Received()
+ .AddAsync(Arg.Is(x => x.TodoListId == listId &&
+ x.UserId == callerId && x.RoleId == ownerRoleId));
+ // Assert that changes were commited
+ await WebApplicationFactory.UnitOfWork
+ .Received()
+ .CommitAsync();
+ }
+
+ [Test]
+ public async Task CreateAsync_OnException_RethrowsAndRollbacksChanges()
+ {
+ // Arrange
+ string callerId = "CallerId";
+ TodoListCreateDto dto = new("Test entity", "...");
+ WebApplicationFactory.TodoListsRepository
+ .AddAsync(Arg.Any())
+ .Throws();
+
+ // Act/Assert
+ Assert.ThrowsAsync(() => _service.CreateAsync(dto, callerId));
+
+ // Assert
+ await WebApplicationFactory.UnitOfWork
+ .Received()
+ .RollbackAsync();
+
}
[Test]
diff --git a/AdvancedTodoList.IntegrationTests/Utils/TestModels.cs b/AdvancedTodoList.IntegrationTests/Utils/TestModels.cs
index e20810d..efeb7f0 100644
--- a/AdvancedTodoList.IntegrationTests/Utils/TestModels.cs
+++ b/AdvancedTodoList.IntegrationTests/Utils/TestModels.cs
@@ -15,18 +15,20 @@ public static class TestModels
///
/// Creates and returns a valid model of a to-do list which can be added to the DB.
///
- public static TodoList CreateTestTodoList() => new()
+ public static TodoList CreateTestTodoList(string? ownerId = null) => new()
{
Name = "Name",
- Description = "Description"
+ Description = "Description",
+ OwnerId = ownerId
};
///
/// Creates and returns a valid model of a to-do list item which can be added to the DB.
///
- public static TodoItem CreateTestTodoItem(string todoListId) => new()
+ public static TodoItem CreateTestTodoItem(string todoListId, string? ownerId = null) => new()
{
Name = "Name",
Description = "Description",
+ OwnerId = ownerId,
State = TodoItemState.Completed,
DeadlineDate = DateTime.UtcNow.AddDays(365),
TodoListId = todoListId
diff --git a/AdvancedTodoList/Controllers/TodoItemsController.cs b/AdvancedTodoList/Controllers/TodoItemsController.cs
index 358603f..667f292 100644
--- a/AdvancedTodoList/Controllers/TodoItemsController.cs
+++ b/AdvancedTodoList/Controllers/TodoItemsController.cs
@@ -1,6 +1,7 @@
using AdvancedTodoList.Core.Dtos;
using AdvancedTodoList.Core.Pagination;
using AdvancedTodoList.Core.Services;
+using AdvancedTodoList.Extensions;
using Mapster;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -73,7 +74,7 @@ public async Task GetTodoItemByIdAsync(
public async Task PostTodoItemAsync(
[FromRoute] string listId, [FromBody] TodoItemCreateDto dto)
{
- var item = await _rolesService.CreateAsync(listId, dto);
+ var item = await _rolesService.CreateAsync(listId, dto, User.GetUserId()!);
if (item == null) return NotFound();
var routeValues = new { listId, itemId = item.Id };
diff --git a/AdvancedTodoList/Controllers/TodoListsController.cs b/AdvancedTodoList/Controllers/TodoListsController.cs
index 3b7d025..240424f 100644
--- a/AdvancedTodoList/Controllers/TodoListsController.cs
+++ b/AdvancedTodoList/Controllers/TodoListsController.cs
@@ -1,5 +1,6 @@
using AdvancedTodoList.Core.Dtos;
using AdvancedTodoList.Core.Services;
+using AdvancedTodoList.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -8,9 +9,12 @@ namespace AdvancedTodoList.Controllers;
[Authorize]
[ApiController]
[Route("api/todo")]
-public class TodoListsController(ITodoListsService todoListsService) : ControllerBase
+public class TodoListsController(
+ ITodoListsService todoListsService,
+ ILogger logger) : ControllerBase
{
private readonly ITodoListsService _todoListsService = todoListsService;
+ private readonly ILogger _logger = logger;
///
/// Gets a to-do list by its ID.
@@ -41,7 +45,7 @@ public async Task GetTodoListByIdAsync([FromRoute] string listId)
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task PostTodoListAsync([FromBody] TodoListCreateDto dto)
{
- var list = await _todoListsService.CreateAsync(dto);
+ var list = await _todoListsService.CreateAsync(dto, User.GetUserId()!);
var routeValues = new { listId = list.Id };
return CreatedAtRoute(nameof(GetTodoListByIdAsync), routeValues, list);
}